@open-product-primer/cli 2.9.0 → 2.11.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.js +2 -0
- package/dist/commands/claude-mods.d.ts +2 -0
- package/dist/commands/claude-mods.js +45 -0
- package/dist/commands/init.js +17 -1
- package/dist/lib/claude-mods.d.ts +15 -0
- package/dist/lib/claude-mods.js +78 -0
- package/dist/lib/config-merge.js +2 -0
- package/dist/lib/detect.d.ts +2 -0
- package/dist/lib/detect.js +22 -0
- package/dist/lib/install-agent.d.ts +15 -1
- package/dist/lib/install-agent.js +183 -1
- package/dist/lib/templates.js +1 -0
- package/dist/lib/workflow-schema.d.ts +3 -0
- package/dist/lib/workflow-schema.js +6 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ const context_1 = require("./commands/context");
|
|
|
16
16
|
const list_1 = require("./commands/list");
|
|
17
17
|
const show_1 = require("./commands/show");
|
|
18
18
|
const status_1 = require("./commands/status");
|
|
19
|
+
const claude_mods_1 = require("./commands/claude-mods");
|
|
19
20
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
20
21
|
const program = new commander_1.Command();
|
|
21
22
|
program
|
|
@@ -33,4 +34,5 @@ program.addCommand((0, context_1.contextCommand)());
|
|
|
33
34
|
program.addCommand((0, list_1.listCommand)());
|
|
34
35
|
program.addCommand((0, show_1.showCommand)());
|
|
35
36
|
program.addCommand((0, status_1.statusCommand)());
|
|
37
|
+
program.addCommand((0, claude_mods_1.claudeModsCommand)());
|
|
36
38
|
program.parse();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.claudeModsCommand = claudeModsCommand;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const detect_1 = require("../lib/detect");
|
|
10
|
+
const install_agent_1 = require("../lib/install-agent");
|
|
11
|
+
function claudeModsCommand() {
|
|
12
|
+
return new commander_1.Command('claude-mods')
|
|
13
|
+
.description('Select which Claude Code function-hook mods are installed')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
const projectRoot = process.cwd();
|
|
16
|
+
const agents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
17
|
+
if (!agents || !agents.includes('claude')) {
|
|
18
|
+
console.error(chalk_1.default.red('Claude Code is not installed in this project.') +
|
|
19
|
+
' Run ' +
|
|
20
|
+
chalk_1.default.cyan('oprim init') +
|
|
21
|
+
' and select Claude Code first.');
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const previousMods = (0, detect_1.readClaudeModsFromConfig)(projectRoot);
|
|
26
|
+
const selectedMods = await (0, install_agent_1.promptClaudeModsSelection)(previousMods);
|
|
27
|
+
if (selectedMods.length > 0 && !(0, install_agent_1.isFunctionHooksActive)(projectRoot)) {
|
|
28
|
+
const enable = await (0, install_agent_1.promptEnableFunctionHooks)();
|
|
29
|
+
if (enable) {
|
|
30
|
+
(0, install_agent_1.enableFunctionHooks)(projectRoot);
|
|
31
|
+
console.log(chalk_1.default.green('✓') + ' .claude/settings.json (function hooks enabled)');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
(0, install_agent_1.printManualFunctionHooksActivation)();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
(0, install_agent_1.applyClaudeModsSelection)(projectRoot, selectedMods, previousMods);
|
|
38
|
+
if (selectedMods.length === 0) {
|
|
39
|
+
console.log(chalk_1.default.dim(' No mods selected — any previously-installed mod hooks were removed.'));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(chalk_1.default.green('✓') + ` Claude mods installed: ${selectedMods.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -48,7 +48,7 @@ function initCommand() {
|
|
|
48
48
|
return new commander_1.Command('init')
|
|
49
49
|
.description('Initialize oprim in the current repository')
|
|
50
50
|
.option('--name <name>', 'project name (defaults to directory name)')
|
|
51
|
-
.option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe, qwen, kimi)', (val, prev) => [...prev, val], [])
|
|
51
|
+
.option('--agent <name>', 'AI agent to install skills for (repeatable; supported: claude, cursor, codex, gemini, poolside, vibe, qwen, kimi, dsh)', (val, prev) => [...prev, val], [])
|
|
52
52
|
.action(async (opts) => {
|
|
53
53
|
const projectRoot = process.cwd();
|
|
54
54
|
const projectName = opts.name ?? path.basename(projectRoot);
|
|
@@ -147,6 +147,22 @@ function initCommand() {
|
|
|
147
147
|
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
|
|
148
148
|
}
|
|
149
149
|
console.log('\n' + chalk_1.default.green('✓') + ` Agent skills installed: ${selectedAgents.join(', ')}`);
|
|
150
|
+
if (selectedAgents.includes('claude')) {
|
|
151
|
+
const previousMods = (0, detect_1.readClaudeModsFromConfig)(projectRoot);
|
|
152
|
+
console.log('');
|
|
153
|
+
const selectedMods = await (0, install_agent_1.promptClaudeModsSelection)(previousMods);
|
|
154
|
+
if (selectedMods.length > 0 && !(0, install_agent_1.isFunctionHooksActive)(projectRoot)) {
|
|
155
|
+
const enable = await (0, install_agent_1.promptEnableFunctionHooks)();
|
|
156
|
+
if (enable) {
|
|
157
|
+
(0, install_agent_1.enableFunctionHooks)(projectRoot);
|
|
158
|
+
console.log(chalk_1.default.green('✓') + ' .claude/settings.json (function hooks enabled)');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
(0, install_agent_1.printManualFunctionHooksActivation)();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
(0, install_agent_1.applyClaudeModsSelection)(projectRoot, selectedMods, previousMods);
|
|
165
|
+
}
|
|
150
166
|
}
|
|
151
167
|
console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
|
|
152
168
|
console.log('\n' +
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ClaudeModHookFile {
|
|
2
|
+
filename: string;
|
|
3
|
+
content: string;
|
|
4
|
+
event: 'PreToolUse' | 'PostToolUse' | 'UserPromptSubmit' | 'Stop';
|
|
5
|
+
matcher?: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ClaudeMod {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
hookFiles: ClaudeModHookFile[];
|
|
13
|
+
}
|
|
14
|
+
export declare const CLAUDE_MODS_REGISTRY: ClaudeMod[];
|
|
15
|
+
export declare function getClaudeMod(id: string): ClaudeMod | undefined;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Static registry of installable Claude Code function-hook "mods" — see
|
|
3
|
+
// oprim/bets/pending/BET-051-spec-delta-drift-interceptor-mod/design.md. Both `oprim init`'s
|
|
4
|
+
// mod-selection prompt and the `oprim claude-mods` command read from this single registry, so a
|
|
5
|
+
// future mod (BET-052–066) is added here once rather than duplicated per entry point.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CLAUDE_MODS_REGISTRY = void 0;
|
|
8
|
+
exports.getClaudeMod = getClaudeMod;
|
|
9
|
+
// PostToolUse hook: on a Write/Edit to a bet's specs/<capability>/spec.md delta, shells out to
|
|
10
|
+
// `oprim validate --json` (already runs checkSpecDeltaDrift()) and surfaces any MODIFIED/REMOVED
|
|
11
|
+
// drift for that capability inline, at write time instead of only at validate/CI time. Silently
|
|
12
|
+
// no-ops on any failure (oprim not resolvable, malformed hook input, etc.) — this is a
|
|
13
|
+
// write-time convenience on top of `oprim validate`, never a second source of required failures.
|
|
14
|
+
const SPEC_DELTA_DRIFT_INTERCEPTOR_SCRIPT = `#!/usr/bin/env node
|
|
15
|
+
// PostToolUse hook (Write|Edit) — see claude-mods.ts's spec-delta-drift-interceptor entry.
|
|
16
|
+
|
|
17
|
+
let raw = '';
|
|
18
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
19
|
+
process.stdin.on('end', () => {
|
|
20
|
+
try {
|
|
21
|
+
const input = JSON.parse(raw || '{}');
|
|
22
|
+
const filePath = input.tool_input && input.tool_input.file_path;
|
|
23
|
+
if (!filePath) return;
|
|
24
|
+
|
|
25
|
+
const match = String(filePath).replace(/\\\\/g, '/').match(
|
|
26
|
+
/oprim\\/bets\\/pending\\/[^/]+\\/specs\\/([^/]+)\\/spec\\.md$/
|
|
27
|
+
);
|
|
28
|
+
if (!match) return;
|
|
29
|
+
const capability = match[1];
|
|
30
|
+
|
|
31
|
+
const { execSync } = require('child_process');
|
|
32
|
+
let output;
|
|
33
|
+
try {
|
|
34
|
+
output = execSync('npx --no-install oprim validate --json', {
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// validate exits non-zero when a required check fails — stdout still carries the report
|
|
40
|
+
output = err && err.stdout ? err.stdout.toString() : null;
|
|
41
|
+
}
|
|
42
|
+
if (!output) return;
|
|
43
|
+
|
|
44
|
+
const report = JSON.parse(output);
|
|
45
|
+
const drift = (report.checks || []).filter(
|
|
46
|
+
(c) => c.name && c.name.startsWith('spec-delta:') && c.name.includes(\`in \${capability} \`) && !c.pass
|
|
47
|
+
);
|
|
48
|
+
if (drift.length === 0) return;
|
|
49
|
+
|
|
50
|
+
const reason = drift.map((c) => \`- \${c.name}\${c.note ? \` (\${c.note})\` : ''}\`).join('\\n');
|
|
51
|
+
process.stdout.write(JSON.stringify({
|
|
52
|
+
decision: 'block',
|
|
53
|
+
reason: \`Spec-delta drift detected in \${capability}:\\n\${reason}\`,
|
|
54
|
+
}) + '\\n');
|
|
55
|
+
} catch {
|
|
56
|
+
// Graceful degradation — never error or block on interceptor failure.
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
`;
|
|
60
|
+
exports.CLAUDE_MODS_REGISTRY = [
|
|
61
|
+
{
|
|
62
|
+
id: 'spec-delta-drift-interceptor',
|
|
63
|
+
title: 'Spec-delta drift interceptor',
|
|
64
|
+
description: "Catches a bet's spec-delta MODIFIED/REMOVED requirement drifting from oprim/specs current truth at write time, instead of only at oprim validate/CI time.",
|
|
65
|
+
hookFiles: [
|
|
66
|
+
{
|
|
67
|
+
filename: 'spec-delta-drift-interceptor.js',
|
|
68
|
+
content: SPEC_DELTA_DRIFT_INTERCEPTOR_SCRIPT,
|
|
69
|
+
event: 'PostToolUse',
|
|
70
|
+
matcher: 'Write|Edit',
|
|
71
|
+
command: 'node ".claude/hooks/spec-delta-drift-interceptor.js"',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
function getClaudeMod(id) {
|
|
77
|
+
return exports.CLAUDE_MODS_REGISTRY.find((mod) => mod.id === id);
|
|
78
|
+
}
|
package/dist/lib/config-merge.js
CHANGED
|
@@ -12,6 +12,8 @@ const CONFIG_SCHEMA_FIELDS = [
|
|
|
12
12
|
// merge only adds missing keys, never removes existing ones), but new/updated configs only
|
|
13
13
|
// ever gain `remote_context`.
|
|
14
14
|
{ key: 'remote_context', block: 'remote_context:\n enabled: false\n sources: []\n' },
|
|
15
|
+
// bet-051 — installed claude-mods selection, mirroring how `agents:` is stored/rewritten.
|
|
16
|
+
{ key: 'claude_mods', block: 'claude_mods: []\n' },
|
|
15
17
|
];
|
|
16
18
|
function existingTopLevelKeys(content) {
|
|
17
19
|
const keys = new Set();
|
package/dist/lib/detect.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export declare function readAgentsFromConfig(projectRoot: string): string[] | nu
|
|
|
10
10
|
export declare function readOkfEnabledFromConfig(projectRoot: string): boolean;
|
|
11
11
|
export declare function detectAvailableAgents(projectRoot: string): string[];
|
|
12
12
|
export declare function writeAgentsToConfig(agents: string[], projectRoot: string): void;
|
|
13
|
+
export declare function readClaudeModsFromConfig(projectRoot: string): string[];
|
|
14
|
+
export declare function writeClaudeModsToConfig(mods: string[], projectRoot: string): void;
|
package/dist/lib/detect.js
CHANGED
|
@@ -39,6 +39,8 @@ exports.readAgentsFromConfig = readAgentsFromConfig;
|
|
|
39
39
|
exports.readOkfEnabledFromConfig = readOkfEnabledFromConfig;
|
|
40
40
|
exports.detectAvailableAgents = detectAvailableAgents;
|
|
41
41
|
exports.writeAgentsToConfig = writeAgentsToConfig;
|
|
42
|
+
exports.readClaudeModsFromConfig = readClaudeModsFromConfig;
|
|
43
|
+
exports.writeClaudeModsToConfig = writeClaudeModsToConfig;
|
|
42
44
|
const fs = __importStar(require("fs"));
|
|
43
45
|
const path = __importStar(require("path"));
|
|
44
46
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -90,6 +92,8 @@ function detectAvailableAgents(projectRoot) {
|
|
|
90
92
|
detected.push('qwen');
|
|
91
93
|
if (fs.existsSync(path.join(projectRoot, '.kimi')))
|
|
92
94
|
detected.push('kimi');
|
|
95
|
+
if (fs.existsSync(path.join(projectRoot, '.dsh')))
|
|
96
|
+
detected.push('dsh');
|
|
93
97
|
return detected;
|
|
94
98
|
}
|
|
95
99
|
function writeAgentsToConfig(agents, projectRoot) {
|
|
@@ -101,3 +105,21 @@ function writeAgentsToConfig(agents, projectRoot) {
|
|
|
101
105
|
config['agents'] = agents;
|
|
102
106
|
fs.writeFileSync(configPath, yaml.dump(config, { indent: 2 }), 'utf-8');
|
|
103
107
|
}
|
|
108
|
+
function readClaudeModsFromConfig(projectRoot) {
|
|
109
|
+
const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
110
|
+
if (!fs.existsSync(configPath))
|
|
111
|
+
return [];
|
|
112
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
113
|
+
const config = yaml.load(content);
|
|
114
|
+
const mods = config?.['claude_mods'];
|
|
115
|
+
return Array.isArray(mods) ? mods : [];
|
|
116
|
+
}
|
|
117
|
+
function writeClaudeModsToConfig(mods, projectRoot) {
|
|
118
|
+
const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
119
|
+
if (!fs.existsSync(configPath))
|
|
120
|
+
return;
|
|
121
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
122
|
+
const config = yaml.load(content);
|
|
123
|
+
config['claude_mods'] = mods;
|
|
124
|
+
fs.writeFileSync(configPath, yaml.dump(config, { indent: 2 }), 'utf-8');
|
|
125
|
+
}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe' | 'qwen' | 'kimi';
|
|
1
|
+
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside' | 'vibe' | 'qwen' | 'kimi' | 'dsh';
|
|
2
2
|
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
4
|
export declare function resolveSpecFramework(projectRoot: string): string;
|
|
5
5
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
6
|
+
export declare function promptClaudeModsSelection(preChecked: string[]): Promise<string[]>;
|
|
7
|
+
export declare function isFunctionHooksActive(projectRoot: string): boolean;
|
|
8
|
+
export declare function promptEnableFunctionHooks(): Promise<boolean>;
|
|
9
|
+
export declare function enableFunctionHooks(projectRoot: string): void;
|
|
10
|
+
export declare function printManualFunctionHooksActivation(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Merges newly-selected mods' hookFiles into .claude/settings.json (additive, non-clobbering —
|
|
13
|
+
* same convention as mergeClaudeSettingsHooks) and removes deselected mods' entries, writing/
|
|
14
|
+
* deleting each mod's hook script file(s) to match. Persists the resulting selection to
|
|
15
|
+
* oprim/config.yaml's claude_mods key.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyClaudeModsSelection(projectRoot: string, selectedIds: string[], previousIds: string[]): void;
|
|
6
18
|
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
7
19
|
export declare function promptOkfFrontmatter(): Promise<boolean>;
|
|
8
20
|
export declare function installAgentSkills(agent: Agent, projectRoot: string, framework?: string, pdrSurfacing?: boolean): void;
|
|
@@ -13,6 +25,7 @@ export declare const POOLSIDE_SKILLS: Record<string, string>;
|
|
|
13
25
|
export declare const VIBE_SKILLS: Record<string, string>;
|
|
14
26
|
export declare const QWEN_SKILLS: Record<string, string>;
|
|
15
27
|
export declare const KIMI_SKILLS: Record<string, string>;
|
|
28
|
+
export declare const DSH_SKILLS: Record<string, string>;
|
|
16
29
|
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
17
30
|
export declare const CURSOR_COMMANDS: Record<string, string>;
|
|
18
31
|
export declare function specAuthoringSkill(): string;
|
|
@@ -23,3 +36,4 @@ export declare function poolsideInstructions(): string;
|
|
|
23
36
|
export declare function vibeInstructions(): string;
|
|
24
37
|
export declare function qwenInstructions(): string;
|
|
25
38
|
export declare function kimiInstructions(): string;
|
|
39
|
+
export declare function dshInstructions(): string;
|
|
@@ -36,10 +36,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.KIMI_SKILLS = exports.QWEN_SKILLS = exports.VIBE_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
|
|
39
|
+
exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.DSH_SKILLS = exports.KIMI_SKILLS = exports.QWEN_SKILLS = exports.VIBE_SKILLS = exports.POOLSIDE_SKILLS = exports.CLAUDE_COMMANDS = exports.CLAUDE_SKILLS = exports.OPRIM_CONTEXT_SKILL_STEP = exports.SUPPORTED_AGENTS = void 0;
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
41
|
exports.resolveSpecFramework = resolveSpecFramework;
|
|
42
42
|
exports.promptAgentSelection = promptAgentSelection;
|
|
43
|
+
exports.promptClaudeModsSelection = promptClaudeModsSelection;
|
|
44
|
+
exports.isFunctionHooksActive = isFunctionHooksActive;
|
|
45
|
+
exports.promptEnableFunctionHooks = promptEnableFunctionHooks;
|
|
46
|
+
exports.enableFunctionHooks = enableFunctionHooks;
|
|
47
|
+
exports.printManualFunctionHooksActivation = printManualFunctionHooksActivation;
|
|
48
|
+
exports.applyClaudeModsSelection = applyClaudeModsSelection;
|
|
43
49
|
exports.promptPdrSurfacing = promptPdrSurfacing;
|
|
44
50
|
exports.promptOkfFrontmatter = promptOkfFrontmatter;
|
|
45
51
|
exports.installAgentSkills = installAgentSkills;
|
|
@@ -51,6 +57,7 @@ exports.poolsideInstructions = poolsideInstructions;
|
|
|
51
57
|
exports.vibeInstructions = vibeInstructions;
|
|
52
58
|
exports.qwenInstructions = qwenInstructions;
|
|
53
59
|
exports.kimiInstructions = kimiInstructions;
|
|
60
|
+
exports.dshInstructions = dshInstructions;
|
|
54
61
|
const path = __importStar(require("path"));
|
|
55
62
|
const fs = __importStar(require("fs"));
|
|
56
63
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -59,6 +66,7 @@ const detect_1 = require("./detect");
|
|
|
59
66
|
const config_merge_1 = require("./config-merge");
|
|
60
67
|
const workflow_schema_1 = require("./workflow-schema");
|
|
61
68
|
const workflow_renderer_1 = require("./workflow-renderer");
|
|
69
|
+
const claude_mods_1 = require("./claude-mods");
|
|
62
70
|
exports.SUPPORTED_AGENTS = [
|
|
63
71
|
'claude',
|
|
64
72
|
'cursor',
|
|
@@ -68,6 +76,7 @@ exports.SUPPORTED_AGENTS = [
|
|
|
68
76
|
'vibe',
|
|
69
77
|
'qwen',
|
|
70
78
|
'kimi',
|
|
79
|
+
'dsh',
|
|
71
80
|
];
|
|
72
81
|
// oprim/config.yaml (via integrations.spec_framework) is the source of truth for the
|
|
73
82
|
// selected speccing framework; .claude/hooks/config.json is checked only as a fallback for
|
|
@@ -139,9 +148,144 @@ async function promptAgentSelection(projectRoot) {
|
|
|
139
148
|
{ name: 'Mistral Vibe', value: 'vibe', checked: detected.includes('vibe') },
|
|
140
149
|
{ name: 'Qwen Code', value: 'qwen', checked: detected.includes('qwen') },
|
|
141
150
|
{ name: 'Kimi CLI', value: 'kimi', checked: detected.includes('kimi') },
|
|
151
|
+
{ name: 'DeepSeek Harness', value: 'dsh', checked: detected.includes('dsh') },
|
|
142
152
|
],
|
|
143
153
|
});
|
|
144
154
|
}
|
|
155
|
+
// ─── claude-mods ───────────────────────────────────────────────────────────
|
|
156
|
+
async function promptClaudeModsSelection(preChecked) {
|
|
157
|
+
const { checkbox } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
158
|
+
return checkbox({
|
|
159
|
+
message: 'Which Claude Code mods should be installed?',
|
|
160
|
+
choices: claude_mods_1.CLAUDE_MODS_REGISTRY.map((mod) => ({
|
|
161
|
+
name: `${mod.title} — ${mod.description}`,
|
|
162
|
+
value: mod.id,
|
|
163
|
+
checked: preChecked.includes(mod.id),
|
|
164
|
+
})),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function isFunctionHooksActive(projectRoot) {
|
|
168
|
+
if (process.env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS)
|
|
169
|
+
return true;
|
|
170
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
171
|
+
if (!fs.existsSync(settingsPath))
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
175
|
+
const env = settings.env;
|
|
176
|
+
return Boolean(env?.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function promptEnableFunctionHooks() {
|
|
183
|
+
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
184
|
+
return confirm({ message: 'Enable Claude Code function hooks now? (y/n)', default: true });
|
|
185
|
+
}
|
|
186
|
+
function enableFunctionHooks(projectRoot) {
|
|
187
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
188
|
+
const settings = readSettings(settingsPath);
|
|
189
|
+
if (!settings.env)
|
|
190
|
+
settings.env = {};
|
|
191
|
+
settings.env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS = '1';
|
|
192
|
+
writeSettings(settingsPath, settings);
|
|
193
|
+
}
|
|
194
|
+
function printManualFunctionHooksActivation() {
|
|
195
|
+
console.log(chalk_1.default.yellow(' Function hooks are not active.') +
|
|
196
|
+
' Selected mod(s) will no-op until you either:\n' +
|
|
197
|
+
' - set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in your shell environment, or\n' +
|
|
198
|
+
' - add an "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } block to .claude/settings.json');
|
|
199
|
+
}
|
|
200
|
+
function readSettings(settingsPath) {
|
|
201
|
+
if (!fs.existsSync(settingsPath))
|
|
202
|
+
return {};
|
|
203
|
+
try {
|
|
204
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function writeSettings(settingsPath, settings) {
|
|
211
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
function addModHookToSettings(settings, hookFile) {
|
|
214
|
+
if (!settings.hooks)
|
|
215
|
+
settings.hooks = {};
|
|
216
|
+
const hooks = settings.hooks;
|
|
217
|
+
if (!hooks[hookFile.event])
|
|
218
|
+
hooks[hookFile.event] = [];
|
|
219
|
+
const entries = hooks[hookFile.event];
|
|
220
|
+
const present = entries.some((entry) => {
|
|
221
|
+
const entryHooks = entry.hooks;
|
|
222
|
+
return entryHooks?.some((h) => h.command === hookFile.command);
|
|
223
|
+
});
|
|
224
|
+
if (!present) {
|
|
225
|
+
const entry = { hooks: [{ type: 'command', command: hookFile.command }] };
|
|
226
|
+
if (hookFile.matcher)
|
|
227
|
+
entry.matcher = hookFile.matcher;
|
|
228
|
+
entries.push(entry);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function removeModHookFromSettings(settings, hookFile) {
|
|
232
|
+
const hooks = settings.hooks;
|
|
233
|
+
if (!hooks || !hooks[hookFile.event])
|
|
234
|
+
return;
|
|
235
|
+
const entries = hooks[hookFile.event];
|
|
236
|
+
const filtered = entries.filter((entry) => {
|
|
237
|
+
const entryHooks = entry.hooks;
|
|
238
|
+
return !entryHooks?.some((h) => h.command === hookFile.command);
|
|
239
|
+
});
|
|
240
|
+
if (filtered.length === 0) {
|
|
241
|
+
delete hooks[hookFile.event];
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
hooks[hookFile.event] = filtered;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Merges newly-selected mods' hookFiles into .claude/settings.json (additive, non-clobbering —
|
|
249
|
+
* same convention as mergeClaudeSettingsHooks) and removes deselected mods' entries, writing/
|
|
250
|
+
* deleting each mod's hook script file(s) to match. Persists the resulting selection to
|
|
251
|
+
* oprim/config.yaml's claude_mods key.
|
|
252
|
+
*/
|
|
253
|
+
function applyClaudeModsSelection(projectRoot, selectedIds, previousIds) {
|
|
254
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
255
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
256
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
257
|
+
const settings = readSettings(settingsPath);
|
|
258
|
+
const added = selectedIds.filter((id) => !previousIds.includes(id));
|
|
259
|
+
const removed = previousIds.filter((id) => !selectedIds.includes(id));
|
|
260
|
+
for (const id of added) {
|
|
261
|
+
const mod = (0, claude_mods_1.getClaudeMod)(id);
|
|
262
|
+
if (!mod)
|
|
263
|
+
continue;
|
|
264
|
+
for (const hookFile of mod.hookFiles) {
|
|
265
|
+
const scriptPath = path.join(hooksDir, hookFile.filename);
|
|
266
|
+
(0, scaffold_1.writeFile)(scriptPath, hookFile.content);
|
|
267
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
268
|
+
addModHookToSettings(settings, hookFile);
|
|
269
|
+
console.log(chalk_1.default.green('✓') + ` .claude/hooks/${hookFile.filename} (${mod.title})`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const id of removed) {
|
|
273
|
+
const mod = (0, claude_mods_1.getClaudeMod)(id);
|
|
274
|
+
if (!mod)
|
|
275
|
+
continue;
|
|
276
|
+
for (const hookFile of mod.hookFiles) {
|
|
277
|
+
const scriptPath = path.join(hooksDir, hookFile.filename);
|
|
278
|
+
if (fs.existsSync(scriptPath))
|
|
279
|
+
fs.unlinkSync(scriptPath);
|
|
280
|
+
removeModHookFromSettings(settings, hookFile);
|
|
281
|
+
console.log(chalk_1.default.dim(` removed .claude/hooks/${hookFile.filename} (${mod.title})`));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (added.length > 0 || removed.length > 0) {
|
|
285
|
+
writeSettings(settingsPath, settings);
|
|
286
|
+
}
|
|
287
|
+
(0, detect_1.writeClaudeModsToConfig)(selectedIds, projectRoot);
|
|
288
|
+
}
|
|
145
289
|
async function promptPdrSurfacing() {
|
|
146
290
|
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
147
291
|
return confirm({ message: 'Enable proactive PDR surfacing in skills? (Y/n)', default: true });
|
|
@@ -360,6 +504,37 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec', pdrSurfa
|
|
|
360
504
|
console.log(chalk_1.default.dim(' .qwen/ created — Qwen Code will discover these files automatically.'));
|
|
361
505
|
}
|
|
362
506
|
}
|
|
507
|
+
else if (agent === 'dsh') {
|
|
508
|
+
const dshDir = path.join(projectRoot, '.dsh');
|
|
509
|
+
const dirCreated = !fs.existsSync(dshDir);
|
|
510
|
+
const skillsBase = path.join(dshDir, 'skills');
|
|
511
|
+
for (const id of POOLSIDE_SKILL_WORKFLOW_IDS) {
|
|
512
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id, projectRoot);
|
|
513
|
+
if (!schema.dsh.skill || !schema.skillName)
|
|
514
|
+
continue;
|
|
515
|
+
(0, scaffold_1.writeFile)(path.join(skillsBase, schema.skillName, 'SKILL.md'), (0, workflow_renderer_1.renderSkillBody)(id, projectRoot));
|
|
516
|
+
console.log(chalk_1.default.green('✓') + ` .dsh/skills/${schema.skillName}/SKILL.md`);
|
|
517
|
+
}
|
|
518
|
+
const dshSpecSkillPath = path.join(skillsBase, 'oprim-spec', 'SKILL.md');
|
|
519
|
+
if (framework === 'native') {
|
|
520
|
+
(0, scaffold_1.writeFile)(dshSpecSkillPath, (0, workflow_renderer_1.renderSkillBody)('spec-authoring', projectRoot));
|
|
521
|
+
console.log(chalk_1.default.green('✓') + ' .dsh/skills/oprim-spec/SKILL.md');
|
|
522
|
+
}
|
|
523
|
+
else if (fs.existsSync(dshSpecSkillPath)) {
|
|
524
|
+
fs.unlinkSync(dshSpecSkillPath);
|
|
525
|
+
try {
|
|
526
|
+
fs.rmdirSync(path.dirname(dshSpecSkillPath));
|
|
527
|
+
}
|
|
528
|
+
catch { /* not empty or already gone */ }
|
|
529
|
+
console.log(chalk_1.default.dim(' removed .dsh/skills/oprim-spec/SKILL.md'));
|
|
530
|
+
}
|
|
531
|
+
const agentsFile = path.join(projectRoot, 'AGENTS.md');
|
|
532
|
+
writeAgentInstructionFile(agentsFile, dshInstructions());
|
|
533
|
+
console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
|
|
534
|
+
if (dirCreated) {
|
|
535
|
+
console.log(chalk_1.default.dim(' .dsh/ created — DeepSeek Harness will discover these files automatically.'));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
363
538
|
else if (agent === 'kimi') {
|
|
364
539
|
// Kimi CLI is a split-path install: .kimi/ is the detection signal, but skills are
|
|
365
540
|
// discovered from a project-root .skills/ directory, not .kimi/skills/ — see
|
|
@@ -573,6 +748,10 @@ exports.KIMI_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) =>
|
|
|
573
748
|
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
574
749
|
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
575
750
|
}));
|
|
751
|
+
exports.DSH_SKILLS = Object.fromEntries(POOLSIDE_SKILL_WORKFLOW_IDS.map((id) => {
|
|
752
|
+
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
753
|
+
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
754
|
+
}));
|
|
576
755
|
exports.CURSOR_SKILLS = Object.fromEntries(CURSOR_SKILL_WORKFLOW_IDS.map((id) => {
|
|
577
756
|
const schema = (0, workflow_schema_1.loadWorkflowSchema)(id);
|
|
578
757
|
return [schema.skillName, (0, workflow_renderer_1.renderSkillBody)(id)];
|
|
@@ -773,3 +952,6 @@ function qwenInstructions() {
|
|
|
773
952
|
function kimiInstructions() {
|
|
774
953
|
return (0, workflow_renderer_1.renderAgentInstructions)();
|
|
775
954
|
}
|
|
955
|
+
function dshInstructions() {
|
|
956
|
+
return (0, workflow_renderer_1.renderAgentInstructions)();
|
|
957
|
+
}
|
package/dist/lib/templates.js
CHANGED
|
@@ -68,12 +68,14 @@ function parseSchema(raw, filePath) {
|
|
|
68
68
|
const claude = requireField(obj, 'claude', filePath);
|
|
69
69
|
const cursor = requireField(obj, 'cursor', filePath);
|
|
70
70
|
const poolside = requireField(obj, 'poolside', filePath);
|
|
71
|
-
// vibe/qwen/kimi are optional and mirror poolside by default — see mistral-vibe-agent-support,
|
|
72
|
-
// qwen-code-agent-support,
|
|
73
|
-
// schemas (none declare a `vibe:`/`qwen:`/`kimi:` key) working
|
|
71
|
+
// vibe/qwen/kimi/dsh are optional and mirror poolside by default — see mistral-vibe-agent-support,
|
|
72
|
+
// qwen-code-agent-support, kimi-cli-agent-support, and deepseek-harness-agent-support specs. This
|
|
73
|
+
// keeps existing bundled/forked schemas (none declare a `vibe:`/`qwen:`/`kimi:`/`dsh:` key) working
|
|
74
|
+
// unchanged.
|
|
74
75
|
const vibe = obj.vibe ?? poolside;
|
|
75
76
|
const qwen = obj.qwen ?? poolside;
|
|
76
77
|
const kimi = obj.kimi ?? poolside;
|
|
78
|
+
const dsh = obj.dsh ?? poolside;
|
|
77
79
|
requireField(obj, 'inline', filePath);
|
|
78
80
|
return {
|
|
79
81
|
id: obj.id,
|
|
@@ -87,6 +89,7 @@ function parseSchema(raw, filePath) {
|
|
|
87
89
|
vibe: { skill: Boolean(vibe.skill) },
|
|
88
90
|
qwen: { skill: Boolean(qwen.skill) },
|
|
89
91
|
kimi: { skill: Boolean(kimi.skill) },
|
|
92
|
+
dsh: { skill: Boolean(dsh.skill) },
|
|
90
93
|
inline: Boolean(obj.inline),
|
|
91
94
|
variants: obj.variants ?? null,
|
|
92
95
|
};
|