@securityreviewai/securityreview-kit 0.1.38 → 0.1.39
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/README.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +4 -0
- package/src/commands/init.js +50 -0
- package/src/generators/mcp/claude.js +35 -0
- package/src/generators/mcp/claude.test.js +53 -0
- package/src/generators/rules/claude.js +66 -11
- package/src/generators/rules/claude.test.js +39 -0
- package/src/generators/rules/guardrails-init-profile.md +8 -0
- package/src/utils/constants.js +2 -0
- package/src/utils/profiler-agent.js +26 -3
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npx @securityreviewai/securityreview-kit init --switch-project
|
|
|
28
28
|
| Target | Flag | MCP Config | Workspace Rule |
|
|
29
29
|
|---|---|---|---|
|
|
30
30
|
| Cursor | `cursor` | `.cursor/mcp.json` | `.cursor/rules/srai-security-review.mdc`, `.cursor/rules/ctm_sync_rule.mdc`, `.cursor/commands/ctm_sync.md`, `.cursor/agents/ctm_sync.md`, `.cursor/commands/create-ide-workflow.md`, `.cursor/commands/srai-profile.md`, `.cursor/skills/threat-modelling/SKILL.md` |
|
|
31
|
-
| Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md` |
|
|
31
|
+
| Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md`, `.claude/skills/threat-modelling/SKILL.md`, `.claude/skills/guardrails-profiler/SKILL.md`, `.claude/skills/guardrails-selection/SKILL.md`, `.claude/agents/ctm_sync.md`, `.claude/commands/guardrails-init-profile.md` |
|
|
32
32
|
| VS Code Copilot | `vscode` | `.vscode/mcp.json` | `.github/copilot-instructions.md`, `.github/skills/threat-modelling/SKILL.md`, `.github/skills/guardrails-profiler/SKILL.md`, `.github/skills/guardrails-selection/SKILL.md`, `.github/agents/ctm_sync.agent.md`, `.github/hooks/srai-session-policy.json` |
|
|
33
33
|
| Windsurf | `windsurf` | `.windsurf/mcp_config.json` | `.windsurf/rules/srai-security-review.md` |
|
|
34
34
|
| Codex | `codex` | `.codex/config.toml` | `.codex/AGENTS.md`, `.codex/skills/threat-modelling/SKILL.md`, `.codex/skills/guardrails-profiler/SKILL.md`, `.codex/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
|
|
@@ -52,6 +52,7 @@ Options:
|
|
|
52
52
|
--skip-mcp Skip MCP server config installation
|
|
53
53
|
--skip-rules Skip workspace rule installation
|
|
54
54
|
--profile-repo Run the guardrails profiler after init
|
|
55
|
+
--profiler-claude-login Run Claude Code login before profiling
|
|
55
56
|
--profiler-copilot-login
|
|
56
57
|
Run GitHub Copilot CLI login before VS Code Copilot profiling
|
|
57
58
|
--profiler-codex-login Run Codex login before Codex profiling
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -37,6 +37,10 @@ export function run() {
|
|
|
37
37
|
'--profiler-cursor-login',
|
|
38
38
|
'Before Cursor profiling, run `agent login` (or `cursor-agent login`) in this terminal (then profiling runs in the same init)',
|
|
39
39
|
)
|
|
40
|
+
.option(
|
|
41
|
+
'--profiler-claude-login',
|
|
42
|
+
'Before Claude Code profiling, run `claude auth login` in this terminal',
|
|
43
|
+
)
|
|
40
44
|
.option(
|
|
41
45
|
'--profiler-copilot-login',
|
|
42
46
|
'Before VS Code Copilot profiling, run `copilot login` in this terminal',
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ensureIdeClisForTargets } from '../utils/ide-cli-install.js';
|
|
|
6
6
|
import { writeGuardrailsSkillBundles } from '../utils/guardrails-profiler-bundle.js';
|
|
7
7
|
import {
|
|
8
8
|
pickProfilerAgentTarget,
|
|
9
|
+
runClaudeLogin,
|
|
9
10
|
runCodexLogin,
|
|
10
11
|
runCopilotLogin,
|
|
11
12
|
runCursorAgentLogin,
|
|
@@ -486,6 +487,37 @@ export async function initCommand(options) {
|
|
|
486
487
|
}
|
|
487
488
|
console.log('');
|
|
488
489
|
}
|
|
490
|
+
} else if (agentTarget === 'claude') {
|
|
491
|
+
console.log(
|
|
492
|
+
chalk.dim(
|
|
493
|
+
' Claude Code: profiling uses `.claude/settings.json`, the configured MCP server, and the Haiku model for this run.',
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
let runLogin = Boolean(options.profilerClaudeLogin);
|
|
498
|
+
if (!runLogin && interactive) {
|
|
499
|
+
runLogin = await confirm({
|
|
500
|
+
message:
|
|
501
|
+
'Run Claude Code login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
|
|
502
|
+
default: true,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (runLogin) {
|
|
506
|
+
console.log('');
|
|
507
|
+
console.log(chalk.bold.white(' Claude Code login'));
|
|
508
|
+
console.log(chalk.dim(' Complete the browser prompt, then return here.\n'));
|
|
509
|
+
const loginResult = runClaudeLogin(cwd);
|
|
510
|
+
if (loginResult.ok) {
|
|
511
|
+
console.log(chalk.green(' \u2713 Claude Code login step finished.'));
|
|
512
|
+
} else {
|
|
513
|
+
console.log(
|
|
514
|
+
chalk.yellow(
|
|
515
|
+
` \u26a0 Claude login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
console.log('');
|
|
520
|
+
}
|
|
489
521
|
} else if (agentTarget === 'codex') {
|
|
490
522
|
console.log(
|
|
491
523
|
chalk.dim(
|
|
@@ -592,6 +624,24 @@ export async function initCommand(options) {
|
|
|
592
624
|
' • MCP missing: re-run init with VS Code selected and MCP installation enabled so `.vscode/mcp.json` is written.',
|
|
593
625
|
),
|
|
594
626
|
);
|
|
627
|
+
} else if (agentTarget === 'claude') {
|
|
628
|
+
console.log('');
|
|
629
|
+
console.log(chalk.dim(' Typical fixes:'));
|
|
630
|
+
console.log(
|
|
631
|
+
chalk.dim(
|
|
632
|
+
' • Not signed in: re-run `securityreview-kit init` and choose Yes for Claude Code login, or pass `--profiler-claude-login` with `--profile-repo`.',
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
console.log(
|
|
636
|
+
chalk.dim(
|
|
637
|
+
' • CLI missing: install Claude Code and verify `claude --version`.',
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
console.log(
|
|
641
|
+
chalk.dim(
|
|
642
|
+
' • MCP missing: re-run init with Claude Code selected and MCP installation enabled so `.claude/settings.json` is written.',
|
|
643
|
+
),
|
|
644
|
+
);
|
|
595
645
|
} else if (agentTarget === 'codex') {
|
|
596
646
|
console.log('');
|
|
597
647
|
console.log(chalk.dim(' Typical fixes:'));
|
|
@@ -2,6 +2,32 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
3
|
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
4
|
|
|
5
|
+
function getClaudeSessionStartHooks() {
|
|
6
|
+
const prompt = [
|
|
7
|
+
'MANDATORY SECURITY GATE (Claude Code Session Policy)',
|
|
8
|
+
'',
|
|
9
|
+
'For any request with security impact, follow this order:',
|
|
10
|
+
'1. Fetch Vibe Guardrails first using .claude/skills/guardrails-selection/SKILL.md.',
|
|
11
|
+
'2. Run PWNISMS threat modelling using .claude/skills/threat-modelling/SKILL.md.',
|
|
12
|
+
'3. Implement secure code using the hydrated guardrails and threat findings.',
|
|
13
|
+
'4. Invoke the ctm_sync agent using .claude/agents/ctm_sync.md after implementation or threat-model updates.',
|
|
14
|
+
'',
|
|
15
|
+
'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, ctm_sync last.',
|
|
16
|
+
].join('\n');
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
matcher: '.*',
|
|
21
|
+
hooks: [
|
|
22
|
+
{
|
|
23
|
+
type: 'prompt',
|
|
24
|
+
prompt,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
/**
|
|
6
32
|
* Generate Claude Code MCP config at .claude/settings.json
|
|
7
33
|
*/
|
|
@@ -22,6 +48,15 @@ export function generate(cwd, envVars) {
|
|
|
22
48
|
},
|
|
23
49
|
};
|
|
24
50
|
|
|
51
|
+
const existingSessionStart = Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
|
|
52
|
+
const marker = 'MANDATORY SECURITY GATE (Claude Code Session Policy)';
|
|
53
|
+
const ours = getClaudeSessionStartHooks();
|
|
54
|
+
const hasOurs = existingSessionStart.some((entry) =>
|
|
55
|
+
Array.isArray(entry?.hooks) &&
|
|
56
|
+
entry.hooks.some((hook) => hook?.type === 'prompt' && typeof hook?.prompt === 'string' && hook.prompt.includes(marker)),
|
|
57
|
+
);
|
|
58
|
+
existing.SessionStart = hasOurs ? existingSessionStart : [...existingSessionStart, ...ours];
|
|
59
|
+
|
|
25
60
|
writeJson(filePath, existing);
|
|
26
61
|
return filePath;
|
|
27
62
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { generate } from './claude.js';
|
|
7
|
+
|
|
8
|
+
test('Claude MCP generator writes mcp server and session hooks', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-'));
|
|
10
|
+
|
|
11
|
+
generate(cwd, {
|
|
12
|
+
apiUrl: 'https://example.test',
|
|
13
|
+
apiToken: 'secret-token',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const config = JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8'));
|
|
17
|
+
assert.equal(config.mcpServers['security-review-mcp'].command, 'npx');
|
|
18
|
+
assert.equal(Array.isArray(config.SessionStart), true);
|
|
19
|
+
assert.match(config.SessionStart[0].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
23
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-merge-'));
|
|
24
|
+
const configPath = join(cwd, '.claude', 'settings.json');
|
|
25
|
+
mkdirSync(join(cwd, '.claude'), { recursive: true });
|
|
26
|
+
|
|
27
|
+
writeFileSync(
|
|
28
|
+
configPath,
|
|
29
|
+
JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
SessionStart: [
|
|
32
|
+
{
|
|
33
|
+
matcher: '.*',
|
|
34
|
+
hooks: [{ type: 'prompt', prompt: 'Existing hook' }],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
2,
|
|
40
|
+
),
|
|
41
|
+
'utf8',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
generate(cwd, {
|
|
45
|
+
apiUrl: 'https://example.test',
|
|
46
|
+
apiToken: 'secret-token',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
50
|
+
assert.equal(config.SessionStart.length, 2);
|
|
51
|
+
assert.match(config.SessionStart[0].hooks[0].prompt, /Existing hook/);
|
|
52
|
+
assert.match(config.SessionStart[1].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
53
|
+
});
|
|
@@ -1,28 +1,83 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CTM_SYNC_AGENT_REL_PATH,
|
|
5
|
+
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
6
|
+
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
7
|
+
THREAT_MODELLING_SKILL_REL_DIR,
|
|
8
|
+
} from '../../utils/constants.js';
|
|
3
9
|
import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
4
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getCtmSyncWorkflowContent,
|
|
12
|
+
getGuardrailsInitProfileContent,
|
|
13
|
+
getRuleContent,
|
|
14
|
+
getThreatModellingSkillContent,
|
|
15
|
+
} from './content.js';
|
|
16
|
+
|
|
17
|
+
function writeGeneratedText(filePath, content) {
|
|
18
|
+
const action = existsSync(filePath) ? 'updated' : 'created';
|
|
19
|
+
writeText(filePath, content);
|
|
20
|
+
return { filePath, action };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAgentBody(content) {
|
|
24
|
+
return content.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getClaudeCtmSyncAgentContent(options = {}) {
|
|
28
|
+
const body = getAgentBody(getCtmSyncWorkflowContent(options));
|
|
29
|
+
return [
|
|
30
|
+
'---',
|
|
31
|
+
'name: ctm_sync',
|
|
32
|
+
'description: Use this agent after security-relevant implementation or threat modelling work to synchronize the latest threat model, mitigations, and guardrails to SRAI.',
|
|
33
|
+
'model: inherit',
|
|
34
|
+
'color: blue',
|
|
35
|
+
'---',
|
|
36
|
+
'',
|
|
37
|
+
body,
|
|
38
|
+
'',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
/**
|
|
7
43
|
* Generate Claude Code workspace rule — appends to CLAUDE.md
|
|
8
44
|
*/
|
|
9
45
|
export function generate(cwd, options = {}) {
|
|
10
|
-
const
|
|
11
|
-
const content = getRuleContent({
|
|
46
|
+
const optionsWithSkillDirs = {
|
|
12
47
|
...options,
|
|
13
48
|
guardrailsSelectionSkillDir: GUARDRAILS_SELECTION_SKILL_REL_DIR.claude,
|
|
14
|
-
|
|
49
|
+
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.claude,
|
|
50
|
+
ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.claude,
|
|
51
|
+
};
|
|
52
|
+
const filePath = join(cwd, 'CLAUDE.md');
|
|
53
|
+
const content = getRuleContent(optionsWithSkillDirs);
|
|
15
54
|
const action = upsertSentinelBlock(filePath, content);
|
|
16
55
|
|
|
56
|
+
const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.claude, 'SKILL.md');
|
|
57
|
+
const threatSkill = writeGeneratedText(
|
|
58
|
+
threatSkillPath,
|
|
59
|
+
getThreatModellingSkillContent(optionsWithSkillDirs),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const ctmSyncAgentPath = join(cwd, CTM_SYNC_AGENT_REL_PATH.claude);
|
|
63
|
+
const ctmSyncAgent = writeGeneratedText(
|
|
64
|
+
ctmSyncAgentPath,
|
|
65
|
+
getClaudeCtmSyncAgentContent(optionsWithSkillDirs),
|
|
66
|
+
);
|
|
67
|
+
|
|
17
68
|
const guardrailsInitPath = join(cwd, '.claude', 'commands', 'guardrails-init-profile.md');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
69
|
+
const guardrailsInit = writeGeneratedText(
|
|
70
|
+
guardrailsInitPath,
|
|
71
|
+
getGuardrailsInitProfileContent({
|
|
72
|
+
...optionsWithSkillDirs,
|
|
73
|
+
guardrailsSkillDir: GUARDRAILS_PROFILER_SKILL_REL_DIR.claude,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
23
76
|
|
|
24
77
|
return [
|
|
25
78
|
{ filePath, action, kind: 'rule' },
|
|
26
|
-
{
|
|
79
|
+
{ ...threatSkill, kind: 'skill' },
|
|
80
|
+
{ ...ctmSyncAgent, kind: 'agent' },
|
|
81
|
+
{ ...guardrailsInit, kind: 'command' },
|
|
27
82
|
];
|
|
28
83
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { generate } from './claude.js';
|
|
7
|
+
|
|
8
|
+
test('Claude generator writes CLAUDE.md, threat skill, ctm_sync agent, and profiling command', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-'));
|
|
10
|
+
|
|
11
|
+
const results = generate(cwd, { projectName: 'SmokeProject' });
|
|
12
|
+
|
|
13
|
+
const expectedPaths = [
|
|
14
|
+
'CLAUDE.md',
|
|
15
|
+
'.claude/skills/threat-modelling/SKILL.md',
|
|
16
|
+
'.claude/agents/ctm_sync.md',
|
|
17
|
+
'.claude/commands/guardrails-init-profile.md',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const relPath of expectedPaths) {
|
|
21
|
+
assert.equal(existsSync(join(cwd, relPath)), true, `${relPath} should exist`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'command']);
|
|
25
|
+
|
|
26
|
+
const instructions = readFileSync(join(cwd, 'CLAUDE.md'), 'utf8');
|
|
27
|
+
assert.match(instructions, /\.claude\/skills\/guardrails-selection\/SKILL\.md/);
|
|
28
|
+
assert.match(instructions, /\.claude\/skills\/threat-modelling\/SKILL\.md/);
|
|
29
|
+
assert.match(instructions, /\.claude\/agents\/ctm_sync\.md/);
|
|
30
|
+
|
|
31
|
+
const threatSkill = readFileSync(join(cwd, '.claude/skills/threat-modelling/SKILL.md'), 'utf8');
|
|
32
|
+
assert.match(threatSkill, /\.claude\/agents\/ctm_sync\.md/);
|
|
33
|
+
assert.doesNotMatch(threatSkill, /get_project_profile_description/);
|
|
34
|
+
|
|
35
|
+
const agent = readFileSync(join(cwd, '.claude/agents/ctm_sync.md'), 'utf8');
|
|
36
|
+
assert.match(agent, /^---\nname: ctm_sync/m);
|
|
37
|
+
assert.match(agent, /model: inherit/);
|
|
38
|
+
assert.match(agent, /Configured SRAI project name: `SmokeProject`/);
|
|
39
|
+
});
|
|
@@ -31,6 +31,14 @@ During `securityreview-kit init`, choose **Yes** when asked to run Cursor login
|
|
|
31
31
|
|
|
32
32
|
You can still sign in manually with `agent login` (or `cursor-agent login`). To handle trust/login interactively in the terminal, omit `--trust` and `--approve-mcps`.
|
|
33
33
|
|
|
34
|
+
## Claude Code CLI (scripted)
|
|
35
|
+
|
|
36
|
+
From the repo root, non-interactive runs should execute with the project settings file and the Haiku model for this profiling pass:
|
|
37
|
+
|
|
38
|
+
`claude -p "<your profiling instructions>" --settings .claude/settings.json --model haiku`
|
|
39
|
+
|
|
40
|
+
During `securityreview-kit init`, choose **Yes** when asked to run Claude Code login, or pass **`--profiler-claude-login`** with **`--profile-repo`** so `claude auth login` and profiling stay in one run.
|
|
41
|
+
|
|
34
42
|
## GitHub Copilot CLI (scripted)
|
|
35
43
|
|
|
36
44
|
From the repo root, non-interactive runs should load the SRAI MCP server and allow the tools needed to scan, write profile files, and call MCP:
|
package/src/utils/constants.js
CHANGED
|
@@ -78,6 +78,7 @@ export const GUARDRAILS_SELECTION_SKILL_REL_DIR = {
|
|
|
78
78
|
/** Relative workspace dirs for the PWNISMS threat-modelling skill (per IDE / CLI). */
|
|
79
79
|
export const THREAT_MODELLING_SKILL_REL_DIR = {
|
|
80
80
|
cursor: '.cursor/skills/threat-modelling',
|
|
81
|
+
claude: '.claude/skills/threat-modelling',
|
|
81
82
|
vscode: '.github/skills/threat-modelling',
|
|
82
83
|
codex: '.codex/skills/threat-modelling',
|
|
83
84
|
};
|
|
@@ -85,6 +86,7 @@ export const THREAT_MODELLING_SKILL_REL_DIR = {
|
|
|
85
86
|
/** Relative workspace paths for the CTM sync agent/workflow (per IDE / CLI). */
|
|
86
87
|
export const CTM_SYNC_AGENT_REL_PATH = {
|
|
87
88
|
cursor: '.cursor/agents/ctm_sync.md',
|
|
89
|
+
claude: '.claude/agents/ctm_sync.md',
|
|
88
90
|
vscode: '.github/agents/ctm_sync.agent.md',
|
|
89
91
|
codex: '.codex/agents/ctm_sync.toml',
|
|
90
92
|
};
|
|
@@ -109,6 +109,26 @@ export function runCopilotLogin(cwd) {
|
|
|
109
109
|
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Run Claude Code login in the current terminal.
|
|
114
|
+
*/
|
|
115
|
+
export function runClaudeLogin(cwd) {
|
|
116
|
+
const env = augmentPathEnv(process.env);
|
|
117
|
+
if (!commandOk('claude', ['--version'], env)) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
status: null,
|
|
121
|
+
message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const r = spawnSync('claude', ['auth', 'login', '--claudeai'], { cwd, stdio: 'inherit', env });
|
|
125
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
126
|
+
if (r.status === null && spawnErr) {
|
|
127
|
+
return { ok: false, status: null, message: spawnErr };
|
|
128
|
+
}
|
|
129
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
/**
|
|
113
133
|
* Run Codex login in the current terminal. Device auth keeps the flow in-terminal.
|
|
114
134
|
*/
|
|
@@ -230,9 +250,12 @@ export function runProfilerAgent(
|
|
|
230
250
|
if (!commandOk('claude', ['--version'], env)) {
|
|
231
251
|
return { ok: false, message: 'claude not on PATH' };
|
|
232
252
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
254
|
+
const args = ['-p', '--settings', settingsPath, '--model', 'haiku'];
|
|
255
|
+
if (streamProgress) {
|
|
256
|
+
args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
|
|
257
|
+
}
|
|
258
|
+
args.push(prompt);
|
|
236
259
|
const r = spawnSync('claude', args, opts);
|
|
237
260
|
return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
|
|
238
261
|
}
|