refacil-sdd-ai 5.2.2 → 5.3.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/NOTICE.md +46 -0
- package/README.md +209 -42
- package/agents/auditor.md +46 -0
- package/agents/debugger.md +41 -1
- package/agents/implementer.md +76 -10
- package/agents/investigator.md +36 -0
- package/agents/proposer.md +46 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +428 -83
- package/bin/postinstall.js +20 -0
- package/lib/bus/broker.js +121 -3
- package/lib/bus/spawn.js +189 -121
- package/lib/check-review.js +102 -0
- package/lib/codegraph-telemetry.js +135 -0
- package/lib/codegraph.js +273 -0
- package/lib/commands/autopilot.js +120 -0
- package/lib/commands/bus.js +29 -36
- package/lib/commands/compact.js +185 -46
- package/lib/commands/read-spec.js +352 -0
- package/lib/commands/sdd.js +429 -44
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +32 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +202 -19
- package/lib/kapso.js +241 -0
- package/lib/methodology-migration-pending.js +13 -0
- package/lib/open-browser.js +32 -0
- package/lib/opencode-migrate.js +148 -0
- package/lib/opencode-plugin/index.js +84 -104
- package/lib/opencode-plugin/rules.js +236 -0
- package/lib/project-root.js +154 -0
- package/lib/repo-ide-sync.js +5 -0
- package/lib/spec-reader/lang.js +72 -0
- package/lib/spec-reader/md-parser.js +299 -0
- package/lib/spec-reader/session.js +139 -0
- package/lib/spec-reader/ui/app.js +685 -0
- package/lib/spec-reader/ui/index.html +59 -0
- package/lib/spec-reader/ui/mixed-lang.js +200 -0
- package/lib/spec-reader/ui/model-cache.js +117 -0
- package/lib/spec-reader/ui/style.css +294 -0
- package/lib/spec-reader/ui/supertonic-helper.js +565 -0
- package/lib/spec-sync.js +258 -0
- package/lib/test-scope.js +713 -0
- package/lib/testing-policy-sync.js +14 -2
- package/package.json +6 -3
- package/skills/apply/SKILL.md +39 -64
- package/skills/archive/SKILL.md +74 -48
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +476 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +31 -13
- package/skills/inbox/SKILL.md +9 -0
- package/skills/join/SKILL.md +1 -1
- package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
- package/skills/prereqs/SKILL.md +1 -1
- package/skills/propose/SKILL.md +74 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +63 -25
- package/skills/review/checklist.md +2 -2
- package/skills/say/SKILL.md +40 -4
- package/skills/setup/SKILL.md +59 -5
- package/skills/setup/troubleshooting.md +11 -3
- package/skills/stats/SKILL.md +157 -0
- package/skills/test/SKILL.md +35 -10
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +78 -41
- package/templates/compact-guidance.md +10 -0
- package/templates/methodology-guide.md +5 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodeGraph telemetry — analogous to lib/compact/telemetry.js.
|
|
5
|
+
*
|
|
6
|
+
* Logs events to ~/.refacil-sdd-ai/codegraph.log (JSONL).
|
|
7
|
+
* Uses the same try/catch-silent pattern and ensureDir() approach
|
|
8
|
+
* as compact telemetry. Never throws to the caller.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const HOME_DIR = path.join(os.homedir(), '.refacil-sdd-ai');
|
|
16
|
+
const LOG_PATH = path.join(HOME_DIR, 'codegraph.log');
|
|
17
|
+
|
|
18
|
+
function ensureDir() {
|
|
19
|
+
try {
|
|
20
|
+
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Log a CodeGraph usage event.
|
|
26
|
+
* @param {string} skillId — e.g. 'investigator' | 'proposer' | 'debugger'
|
|
27
|
+
* @param {boolean} hasGraph — whether .codegraph/ was present in the repo
|
|
28
|
+
* @param {number} toolCallsCount — number of CodeGraph tool calls made this session
|
|
29
|
+
* @param {number} estimatedTokensSaved — estimated tokens saved vs reading files directly
|
|
30
|
+
*/
|
|
31
|
+
function logEvent(skillId, hasGraph, toolCallsCount, estimatedTokensSaved) {
|
|
32
|
+
try {
|
|
33
|
+
ensureDir();
|
|
34
|
+
const line =
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
ts: new Date().toISOString(),
|
|
37
|
+
skillId: skillId || 'unknown',
|
|
38
|
+
hasGraph: Boolean(hasGraph),
|
|
39
|
+
toolCallsCount: toolCallsCount || 0,
|
|
40
|
+
estimatedTokensSaved: estimatedTokensSaved || 0,
|
|
41
|
+
}) + '\n';
|
|
42
|
+
fs.appendFileSync(LOG_PATH, line);
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// Telemetry must never break the caller
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read all log entries as an array of parsed objects.
|
|
50
|
+
* Returns [] if the log file does not exist or cannot be read.
|
|
51
|
+
* @returns {object[]}
|
|
52
|
+
*/
|
|
53
|
+
function readLog() {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(LOG_PATH, 'utf8');
|
|
56
|
+
return content
|
|
57
|
+
.split('\n')
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((line) => {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(line);
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
} catch (_) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compute stats grouped by skillId, comparing with-graph vs without-graph.
|
|
74
|
+
* @returns {{
|
|
75
|
+
* bySkill: Record<string, { withGraph: { events: number, toolCalls: number, tokensSaved: number }, withoutGraph: { events: number } }>,
|
|
76
|
+
* totalEvents: number,
|
|
77
|
+
* totalTokensSaved: number,
|
|
78
|
+
* totalToolCalls: number
|
|
79
|
+
* }}
|
|
80
|
+
*/
|
|
81
|
+
function stats() {
|
|
82
|
+
const entries = readLog();
|
|
83
|
+
const bySkill = {};
|
|
84
|
+
let totalEvents = 0;
|
|
85
|
+
let totalTokensSaved = 0;
|
|
86
|
+
let totalToolCalls = 0;
|
|
87
|
+
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
const skillId = e.skillId || 'unknown';
|
|
90
|
+
if (!bySkill[skillId]) {
|
|
91
|
+
bySkill[skillId] = {
|
|
92
|
+
withGraph: { events: 0, toolCalls: 0, tokensSaved: 0 },
|
|
93
|
+
withoutGraph: { events: 0 },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (e.hasGraph) {
|
|
98
|
+
bySkill[skillId].withGraph.events++;
|
|
99
|
+
bySkill[skillId].withGraph.toolCalls += e.toolCallsCount || 0;
|
|
100
|
+
bySkill[skillId].withGraph.tokensSaved += e.estimatedTokensSaved || 0;
|
|
101
|
+
totalToolCalls += e.toolCallsCount || 0;
|
|
102
|
+
totalTokensSaved += e.estimatedTokensSaved || 0;
|
|
103
|
+
} else {
|
|
104
|
+
bySkill[skillId].withoutGraph.events++;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
totalEvents++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
bySkill,
|
|
112
|
+
totalEvents,
|
|
113
|
+
totalTokensSaved,
|
|
114
|
+
totalToolCalls,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Delete the log file.
|
|
120
|
+
* Silent on errors (file may not exist).
|
|
121
|
+
*/
|
|
122
|
+
function clearLog() {
|
|
123
|
+
try {
|
|
124
|
+
fs.unlinkSync(LOG_PATH);
|
|
125
|
+
} catch (_) {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
HOME_DIR,
|
|
130
|
+
LOG_PATH,
|
|
131
|
+
logEvent,
|
|
132
|
+
readLog,
|
|
133
|
+
stats,
|
|
134
|
+
clearLog,
|
|
135
|
+
};
|
package/lib/codegraph.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CodeGraph abstraction layer.
|
|
5
|
+
*
|
|
6
|
+
* Wraps @colbymchenry/codegraph as an optional dependency.
|
|
7
|
+
* Never throws to the caller — all errors are swallowed silently.
|
|
8
|
+
*
|
|
9
|
+
* IDE support matrix (MCP registration):
|
|
10
|
+
* - Claude Code : ~/.claude/settings.json → mcpServers.codegraph
|
|
11
|
+
* - Cursor : ~/.cursor/mcp.json → mcpServers.codegraph
|
|
12
|
+
* - OpenCode : <globalOpenCodeDir>/opencode.jsonc → mcp.codegraph
|
|
13
|
+
* - Codex : ~/.codex/config.toml → mcp_servers.codegraph
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check whether @colbymchenry/codegraph is installed in the current Node.js resolution chain.
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function isInstalled() {
|
|
25
|
+
try {
|
|
26
|
+
const { spawnSync } = require('child_process');
|
|
27
|
+
// shell: true is required on Windows where npm binaries are installed as .cmd files
|
|
28
|
+
const result = spawnSync('codegraph', ['--version'], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
shell: true,
|
|
33
|
+
});
|
|
34
|
+
return result.status === 0;
|
|
35
|
+
} catch (_) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check whether the given repo path has already been indexed by CodeGraph.
|
|
42
|
+
* A repo is considered indexed when a `.codegraph/` directory exists at its root.
|
|
43
|
+
* @param {string} repoPath - absolute path to the project root
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isInitialized(repoPath) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.existsSync(path.join(repoPath, '.codegraph'));
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Timestamp file written inside .codegraph/ each time we trigger init. */
|
|
55
|
+
const LAST_INIT_FILE = '.codegraph/.refacil-last-init';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Spawn a background CodeGraph indexing process for the given repo.
|
|
59
|
+
* Uses the global `codegraph` CLI binary (shell: true for Windows .cmd support).
|
|
60
|
+
* Also writes a timestamp so isStale() can compare against git history.
|
|
61
|
+
* Fire-and-forget — returns immediately. Never throws.
|
|
62
|
+
* @param {string} repoPath - absolute path to the project root
|
|
63
|
+
*/
|
|
64
|
+
function init(repoPath) {
|
|
65
|
+
try {
|
|
66
|
+
const { spawn } = require('child_process');
|
|
67
|
+
// `codegraph init` creates .codegraph/ (structure only, 0 nodes).
|
|
68
|
+
// `codegraph index` builds the actual graph (must run after init).
|
|
69
|
+
// For first-time: chain both commands so the graph is built in one shot.
|
|
70
|
+
// For re-index: only `codegraph index` (structure already exists).
|
|
71
|
+
const cgDir = path.join(repoPath, '.codegraph');
|
|
72
|
+
const isFirstTime = !fs.existsSync(cgDir);
|
|
73
|
+
// Use shell chaining (&&) so the second command only runs if the first succeeds.
|
|
74
|
+
const cmd = isFirstTime ? 'codegraph init && codegraph index' : 'codegraph index';
|
|
75
|
+
const child = spawn(cmd, [], {
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: 'ignore',
|
|
78
|
+
shell: true, // required for && chaining and Windows .cmd binaries
|
|
79
|
+
windowsHide: true, // suppress console window pop-ups on Windows
|
|
80
|
+
cwd: repoPath,
|
|
81
|
+
});
|
|
82
|
+
child.unref();
|
|
83
|
+
// Update the timestamp on re-index (.codegraph/ exists). For first-time,
|
|
84
|
+
// the directory does not exist yet so we skip — isStale() falls back to mtime.
|
|
85
|
+
if (!isFirstTime) {
|
|
86
|
+
fs.writeFileSync(path.join(cgDir, '.refacil-last-init'), new Date().toISOString());
|
|
87
|
+
}
|
|
88
|
+
} catch (_) {
|
|
89
|
+
// Swallow: CodeGraph not installed, spawn failed, or fs write failed
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Return true when there are git commits newer than the last time we triggered
|
|
95
|
+
* `codegraph init` for this repo. Always returns false when git is unavailable,
|
|
96
|
+
* the repo has no commits, or any error occurs — never blocks, never throws.
|
|
97
|
+
* @param {string} repoPath - absolute path to the project root
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isStale(repoPath) {
|
|
101
|
+
try {
|
|
102
|
+
// Determine the reference date: prefer our timestamp file, fall back to .codegraph/ mtime
|
|
103
|
+
let refDate;
|
|
104
|
+
const lastInitPath = path.join(repoPath, LAST_INIT_FILE);
|
|
105
|
+
if (fs.existsSync(lastInitPath)) {
|
|
106
|
+
const parsed = new Date(fs.readFileSync(lastInitPath, 'utf8').trim());
|
|
107
|
+
if (!isNaN(parsed.getTime())) refDate = parsed;
|
|
108
|
+
}
|
|
109
|
+
if (!refDate) {
|
|
110
|
+
const cgDir = path.join(repoPath, '.codegraph');
|
|
111
|
+
if (!fs.existsSync(cgDir)) return false;
|
|
112
|
+
refDate = fs.statSync(cgDir).mtime;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for git commits after refDate — gracefully skip if no git
|
|
116
|
+
const { spawnSync } = require('child_process');
|
|
117
|
+
const result = spawnSync(
|
|
118
|
+
'git', ['-C', repoPath, 'log', '--oneline', `--after=${refDate.toISOString()}`, '-1'],
|
|
119
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 },
|
|
120
|
+
);
|
|
121
|
+
if (result.status !== 0 || result.error) return false; // no git or git error
|
|
122
|
+
return result.stdout.trim().length > 0;
|
|
123
|
+
} catch (_) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Return the MCP tool hint string for the agent system prompt.
|
|
130
|
+
* All four IDEs support MCP, so this returns a non-null value for all of them
|
|
131
|
+
* when CodeGraph is installed.
|
|
132
|
+
* Returns null when CodeGraph is not installed or any error occurs.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} ide - 'claude' | 'cursor' | 'opencode' | 'codex'
|
|
135
|
+
* @returns {string|null}
|
|
136
|
+
*/
|
|
137
|
+
function mcpEntry(ide) {
|
|
138
|
+
try {
|
|
139
|
+
if (!ide) return null;
|
|
140
|
+
if (!isInstalled()) return null;
|
|
141
|
+
return (
|
|
142
|
+
'CodeGraph MCP tools available: ' +
|
|
143
|
+
'codegraph_search, codegraph_callers, codegraph_callees, codegraph_context, codegraph_impact. ' +
|
|
144
|
+
'Use these BEFORE Grep or Read for symbol and call-graph queries when .codegraph/ is present.'
|
|
145
|
+
);
|
|
146
|
+
} catch (_) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── MCP server registration ───────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
const CODEGRAPH_MCP_KEY = 'codegraph';
|
|
154
|
+
|
|
155
|
+
function _upsertMcpEntry(filePath, entry) {
|
|
156
|
+
let config = {};
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
159
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) config = parsed;
|
|
160
|
+
} catch (_) {}
|
|
161
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
162
|
+
if (config.mcpServers[CODEGRAPH_MCP_KEY]) return; // already registered — idempotent
|
|
163
|
+
config.mcpServers[CODEGRAPH_MCP_KEY] = entry;
|
|
164
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
165
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _registerClaudeMcp(homeDir) {
|
|
169
|
+
const dir = path.join(homeDir, '.claude');
|
|
170
|
+
if (!fs.existsSync(dir)) return;
|
|
171
|
+
_upsertMcpEntry(path.join(dir, 'settings.json'), {
|
|
172
|
+
command: 'codegraph',
|
|
173
|
+
args: ['serve', '--mcp', '--path', '.'],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _registerCursorMcp(homeDir) {
|
|
178
|
+
const dir = path.join(homeDir, '.cursor');
|
|
179
|
+
if (!fs.existsSync(dir)) return;
|
|
180
|
+
_upsertMcpEntry(path.join(dir, 'mcp.json'), {
|
|
181
|
+
type: 'stdio',
|
|
182
|
+
command: 'codegraph',
|
|
183
|
+
args: ['serve', '--mcp', '--path', '${workspaceFolder}'],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _registerOpenCodeMcp(homeDir) {
|
|
188
|
+
const { globalOpenCodeDir } = require('./global-paths');
|
|
189
|
+
const openCodeDir = globalOpenCodeDir(homeDir);
|
|
190
|
+
fs.mkdirSync(openCodeDir, { recursive: true });
|
|
191
|
+
const configPath = path.join(openCodeDir, 'opencode.jsonc');
|
|
192
|
+
let config = { $schema: 'https://opencode.ai/config.json' };
|
|
193
|
+
try {
|
|
194
|
+
const raw = fs.readFileSync(configPath, 'utf8').replace(/\/\/[^\n]*/g, '');
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
if (parsed && typeof parsed === 'object') config = parsed;
|
|
197
|
+
} catch (_) {}
|
|
198
|
+
if (!config.mcp) config.mcp = {};
|
|
199
|
+
if (config.mcp[CODEGRAPH_MCP_KEY]) return;
|
|
200
|
+
config.mcp[CODEGRAPH_MCP_KEY] = { type: 'local', command: ['codegraph', 'serve', '--mcp'], enabled: true };
|
|
201
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function _registerCodexMcp(homeDir) {
|
|
205
|
+
const codexDir = path.join(homeDir, '.codex');
|
|
206
|
+
if (!fs.existsSync(codexDir)) return;
|
|
207
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
208
|
+
const smolToml = require('smol-toml');
|
|
209
|
+
let config = {};
|
|
210
|
+
try {
|
|
211
|
+
config = smolToml.parse(fs.readFileSync(configPath, 'utf8'));
|
|
212
|
+
} catch (_) {}
|
|
213
|
+
if (!config.mcp_servers) config.mcp_servers = {};
|
|
214
|
+
if (config.mcp_servers[CODEGRAPH_MCP_KEY]) return;
|
|
215
|
+
config.mcp_servers[CODEGRAPH_MCP_KEY] = { command: 'codegraph', args: ['serve', '--mcp'] };
|
|
216
|
+
fs.writeFileSync(configPath, smolToml.stringify(config), 'utf8');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Register the CodeGraph MCP server entry in each selected IDE's config file.
|
|
221
|
+
* Idempotent — no-ops if the entry already exists or the IDE dir is absent. Never throws.
|
|
222
|
+
*
|
|
223
|
+
* Accepts both dot-prefixed (.claude) and plain (claude) IDE names — both formats are normalized.
|
|
224
|
+
*
|
|
225
|
+
* IDE → file:
|
|
226
|
+
* claude → ~/.claude/settings.json (mcpServers.codegraph)
|
|
227
|
+
* cursor → ~/.cursor/mcp.json (mcpServers.codegraph)
|
|
228
|
+
* opencode → <globalOpenCodeDir>/opencode.jsonc (mcp.codegraph)
|
|
229
|
+
* codex → ~/.codex/config.toml (mcp_servers.codegraph)
|
|
230
|
+
*
|
|
231
|
+
* @param {string[]} selectedIDEs - e.g. ['.claude','.cursor'] or ['claude','cursor']
|
|
232
|
+
* @param {string} [homeDir] - injectable for testing
|
|
233
|
+
*/
|
|
234
|
+
function registerMcp(selectedIDEs, homeDir) {
|
|
235
|
+
const resolvedHome = homeDir || os.homedir();
|
|
236
|
+
const ides = Array.isArray(selectedIDEs) ? selectedIDEs : [];
|
|
237
|
+
for (const ide of ides) {
|
|
238
|
+
try {
|
|
239
|
+
const name = ide.startsWith('.') ? ide.slice(1) : ide;
|
|
240
|
+
if (name === 'claude') _registerClaudeMcp(resolvedHome);
|
|
241
|
+
else if (name === 'cursor') _registerCursorMcp(resolvedHome);
|
|
242
|
+
else if (name === 'opencode') _registerOpenCodeMcp(resolvedHome);
|
|
243
|
+
else if (name === 'codex') _registerCodexMcp(resolvedHome);
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Write the .refacil-last-init timestamp into an existing .codegraph/ directory.
|
|
250
|
+
* No-op when .codegraph/ does not exist. Used by checkUpdate to stamp indexes
|
|
251
|
+
* that were built externally (not through our setup/init commands).
|
|
252
|
+
* @param {string} repoPath
|
|
253
|
+
*/
|
|
254
|
+
function touchTimestamp(repoPath) {
|
|
255
|
+
try {
|
|
256
|
+
const cgDir = path.join(repoPath, '.codegraph');
|
|
257
|
+
if (!fs.existsSync(cgDir)) return;
|
|
258
|
+
const tsFile = path.join(cgDir, '.refacil-last-init');
|
|
259
|
+
if (!fs.existsSync(tsFile)) {
|
|
260
|
+
fs.writeFileSync(tsFile, new Date().toISOString());
|
|
261
|
+
}
|
|
262
|
+
} catch (_) {}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
isInstalled,
|
|
267
|
+
isInitialized,
|
|
268
|
+
isStale,
|
|
269
|
+
init,
|
|
270
|
+
touchTimestamp,
|
|
271
|
+
mcpEntry,
|
|
272
|
+
registerMcp,
|
|
273
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Recognized IDE identifiers. Only 'cursor' gets a dedicated message path;
|
|
4
|
+
// all other values (including 'unknown') fall to the generic A/B options block.
|
|
5
|
+
const SUPPORTED_IDES = new Set(['cursor', 'claude-code', 'codex', 'opencode', 'unknown']);
|
|
6
|
+
|
|
7
|
+
function getFlag(argv, flag, fallback) {
|
|
8
|
+
const idx = argv.indexOf(flag);
|
|
9
|
+
return idx !== -1 && argv[idx + 1] !== undefined ? argv[idx + 1] : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeIde(raw) {
|
|
13
|
+
if (!raw) return 'unknown';
|
|
14
|
+
const lower = raw.toLowerCase();
|
|
15
|
+
return SUPPORTED_IDES.has(lower) ? lower : 'unknown';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Shared message builder. Receives a localized strings object so both language
|
|
20
|
+
* variants (ES / EN) share the same structural logic — only the copy differs.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} changeName
|
|
23
|
+
* @param {string} ide
|
|
24
|
+
* @param {{ header: string, ideLabel: string, phases: string, cursorLines: string[], genericLines: string[] }} strings
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function buildReadyMessage(changeName, ide, strings) {
|
|
28
|
+
const lines = [
|
|
29
|
+
strings.header,
|
|
30
|
+
`Change: ${changeName}`,
|
|
31
|
+
`${strings.ideLabel}: ${ide}`,
|
|
32
|
+
``,
|
|
33
|
+
strings.phases,
|
|
34
|
+
``,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
if (ide === 'cursor') {
|
|
38
|
+
lines.push(...strings.cursorLines);
|
|
39
|
+
} else {
|
|
40
|
+
lines.push(...strings.genericLines);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildReadyMessageEs(changeName, ide) {
|
|
47
|
+
return buildReadyMessage(changeName, ide, {
|
|
48
|
+
header: `Autopilot listo para arrancar`,
|
|
49
|
+
ideLabel: `IDE detectado`,
|
|
50
|
+
phases: `Fases: apply → test → verify → review → archive → up-code`,
|
|
51
|
+
cursorLines: [
|
|
52
|
+
`Para continuar en esta sesión, responde con una afirmación explícita:`,
|
|
53
|
+
` go · ok · start · sí · dale · arrancar`,
|
|
54
|
+
``,
|
|
55
|
+
`Hasta que no respondas así, no inicio el pipeline.`,
|
|
56
|
+
],
|
|
57
|
+
genericLines: [
|
|
58
|
+
`Opciones:`,
|
|
59
|
+
` A) Ejecutar aquí (pueden aparecer prompts de permisos durante el pipeline)`,
|
|
60
|
+
` → responde "go" / "ok" / "start" para iniciar en esta sesión.`,
|
|
61
|
+
``,
|
|
62
|
+
` B) Ejecutar headless (sin interrupciones, salida visible en nueva terminal)`,
|
|
63
|
+
` → cierra esta sesión y abre el IDE con el flag de auto-permisos adecuado,`,
|
|
64
|
+
` luego escribe /refacil:autopilot.`,
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildReadyMessageEn(changeName, ide) {
|
|
70
|
+
return buildReadyMessage(changeName, ide, {
|
|
71
|
+
header: `Autopilot ready to start`,
|
|
72
|
+
ideLabel: `Detected IDE`,
|
|
73
|
+
phases: `Phases: apply → test → verify → review → archive → up-code`,
|
|
74
|
+
cursorLines: [
|
|
75
|
+
`To continue in this session, reply with an explicit affirmative:`,
|
|
76
|
+
` go · ok · start · yes · dale`,
|
|
77
|
+
``,
|
|
78
|
+
`The pipeline will not start until you reply.`,
|
|
79
|
+
],
|
|
80
|
+
genericLines: [
|
|
81
|
+
`Options:`,
|
|
82
|
+
` A) Run here (Bash permission prompts may appear during the pipeline)`,
|
|
83
|
+
` → reply "go" / "ok" / "start" to begin in this session.`,
|
|
84
|
+
``,
|
|
85
|
+
` B) Run headless (no interruptions, output visible in a new terminal)`,
|
|
86
|
+
` → close this session and open the IDE with the appropriate auto-permissions flag,`,
|
|
87
|
+
` then type /refacil:autopilot.`,
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleAutopilot(sub, argv) {
|
|
93
|
+
if (sub === 'ready-message') {
|
|
94
|
+
const changeName = getFlag(argv, '--change', '');
|
|
95
|
+
const idRaw = getFlag(argv, '--ide', '');
|
|
96
|
+
const lang = getFlag(argv, '--lang', 'es');
|
|
97
|
+
const ide = normalizeIde(idRaw);
|
|
98
|
+
|
|
99
|
+
if (!changeName) {
|
|
100
|
+
process.stderr.write(' Error: --change <changeName> is required\n');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let message;
|
|
105
|
+
if (lang === 'en') {
|
|
106
|
+
message = buildReadyMessageEn(changeName, ide);
|
|
107
|
+
} else {
|
|
108
|
+
message = buildReadyMessageEs(changeName, ide);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.stdout.write(message + '\n');
|
|
112
|
+
} else {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
` Unknown autopilot subcommand: ${sub || '(none)'}. Usage: autopilot <ready-message>\n`,
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { handleAutopilot };
|
package/lib/commands/bus.js
CHANGED
|
@@ -8,6 +8,7 @@ const busClient = require('../bus/client');
|
|
|
8
8
|
const busWatch = require('../bus/watch');
|
|
9
9
|
const busPresenter = require('../bus/presenter');
|
|
10
10
|
const { askHasMatchingReply } = require('../bus/askFulfillment');
|
|
11
|
+
const { openInBrowser } = require('../open-browser');
|
|
11
12
|
|
|
12
13
|
function parseBusArgs(argv) {
|
|
13
14
|
const args = {};
|
|
@@ -155,13 +156,32 @@ async function busLeave(args, packageRoot) {
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
console.error('
|
|
159
|
+
function resolveText(args, usageHint) {
|
|
160
|
+
const hasText = args.text !== undefined;
|
|
161
|
+
const hasFromEnv = args['from-env'] !== undefined;
|
|
162
|
+
if (hasText && hasFromEnv) {
|
|
163
|
+
console.error(' Error: --text and --from-env are mutually exclusive. Use one or the other.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
if (hasFromEnv) {
|
|
167
|
+
const varName = args['from-env'];
|
|
168
|
+
const value = process.env[varName];
|
|
169
|
+
if (!value) {
|
|
170
|
+
console.error(` Error: environment variable "${varName}" is not set or is empty.`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
if (!hasText) {
|
|
176
|
+
console.error(` ${usageHint}`);
|
|
163
177
|
process.exit(1);
|
|
164
178
|
}
|
|
179
|
+
return args.text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function busSay(args, packageRoot) {
|
|
183
|
+
const session = args.session || defaultSessionName();
|
|
184
|
+
const text = resolveText(args, 'Uso: refacil-sdd-ai bus say --text "..." | --from-env VAR [--session <s>]');
|
|
165
185
|
const { ws } = await connectOrDie(packageRoot);
|
|
166
186
|
const reply = await busClient.sendAndWait(
|
|
167
187
|
ws,
|
|
@@ -183,12 +203,12 @@ async function busSay(args, packageRoot) {
|
|
|
183
203
|
async function busAsk(args, packageRoot) {
|
|
184
204
|
const session = args.session || defaultSessionName();
|
|
185
205
|
const to = args.to;
|
|
186
|
-
const text = args.text;
|
|
187
206
|
const waitSec = args.wait ? parseInt(args.wait, 10) : 0;
|
|
188
|
-
if (!to
|
|
189
|
-
console.error(' Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." [--wait N] [--session <s>]');
|
|
207
|
+
if (!to) {
|
|
208
|
+
console.error(' Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." | --from-env VAR [--wait N] [--session <s>]');
|
|
190
209
|
process.exit(1);
|
|
191
210
|
}
|
|
211
|
+
const text = resolveText(args, 'Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." | --from-env VAR [--wait N] [--session <s>]');
|
|
192
212
|
const { ws } = await connectOrDie(packageRoot);
|
|
193
213
|
const ack = await busClient.sendAndWait(
|
|
194
214
|
ws,
|
|
@@ -232,13 +252,9 @@ async function busAsk(args, packageRoot) {
|
|
|
232
252
|
|
|
233
253
|
async function busReply(args, packageRoot) {
|
|
234
254
|
const session = args.session || defaultSessionName();
|
|
235
|
-
const text = args.text;
|
|
236
255
|
const correlationId = args.correlation || null;
|
|
237
256
|
const to = args.to ? args.to.replace(/^@/, '') : null;
|
|
238
|
-
|
|
239
|
-
console.error(' Uso: refacil-sdd-ai bus reply --text "..." [--to <name>] [--correlation <id>]');
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
257
|
+
const text = resolveText(args, 'Uso: refacil-sdd-ai bus reply --text "..." | --from-env VAR [--to <name>] [--correlation <id>]');
|
|
242
258
|
const { ws } = await connectOrDie(packageRoot);
|
|
243
259
|
const reply = await busClient.sendAndWait(
|
|
244
260
|
ws,
|
|
@@ -432,29 +448,6 @@ async function busRooms(packageRoot) {
|
|
|
432
448
|
}
|
|
433
449
|
}
|
|
434
450
|
|
|
435
|
-
function openInBrowser(url) {
|
|
436
|
-
const { spawn } = require('child_process');
|
|
437
|
-
const platform = process.platform;
|
|
438
|
-
let cmd;
|
|
439
|
-
let cmdArgs;
|
|
440
|
-
if (platform === 'win32') {
|
|
441
|
-
cmd = 'cmd';
|
|
442
|
-
cmdArgs = ['/c', 'start', '""', url];
|
|
443
|
-
} else if (platform === 'darwin') {
|
|
444
|
-
cmd = 'open';
|
|
445
|
-
cmdArgs = [url];
|
|
446
|
-
} else {
|
|
447
|
-
cmd = 'xdg-open';
|
|
448
|
-
cmdArgs = [url];
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
452
|
-
return true;
|
|
453
|
-
} catch (_) {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
451
|
async function busView(packageRoot) {
|
|
459
452
|
try {
|
|
460
453
|
const { info } = await busSpawn.ensureBroker(packageRoot);
|