gm-copilot-cli 2.0.1066 → 2.0.1067
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/copilot-profile.md +1 -1
- package/index.html +1 -1
- package/lib/browser-spool-handler.js +130 -0
- package/lib/browser.js +131 -0
- package/lib/codeinsight.js +109 -0
- package/lib/daemon-bootstrap.js +435 -0
- package/lib/git.js +331 -0
- package/lib/learning.js +169 -0
- package/lib/skill-bootstrap.js +410 -0
- package/lib/spool-dispatch.js +75 -0
- package/lib/spool.js +201 -0
- package/manifest.yml +1 -1
- package/package.json +2 -2
- package/tools.json +1 -1
- package/hooks/hooks.json +0 -70
- package/hooks/hooks.spec.json +0 -65
package/lib/git.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const GIT_USER = 'lanmower';
|
|
6
|
+
const GIT_EMAIL = 'almagestfraternite@gmail.com';
|
|
7
|
+
|
|
8
|
+
function emitGitEvent(severity, message, data = {}) {
|
|
9
|
+
const logDir = path.join(os.homedir(), '.claude', 'gm-log', new Date().toISOString().split('T')[0]);
|
|
10
|
+
if (!fs.existsSync(logDir)) {
|
|
11
|
+
try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
|
|
12
|
+
}
|
|
13
|
+
const logFile = path.join(logDir, 'git.jsonl');
|
|
14
|
+
try {
|
|
15
|
+
fs.appendFileSync(logFile, JSON.stringify({
|
|
16
|
+
ts: new Date().toISOString(),
|
|
17
|
+
subsystem: 'git',
|
|
18
|
+
severity,
|
|
19
|
+
message,
|
|
20
|
+
...data,
|
|
21
|
+
}) + '\n');
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeShellArg(arg) {
|
|
26
|
+
if (os.platform() === 'win32') {
|
|
27
|
+
if (!arg) return '""';
|
|
28
|
+
if (/^[a-zA-Z0-9._\-/:=\\]+$/.test(arg)) return arg;
|
|
29
|
+
return '"' + arg.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`') + '"';
|
|
30
|
+
}
|
|
31
|
+
if (!arg) return "''";
|
|
32
|
+
if (/^[a-zA-Z0-9._\-/:=]+$/.test(arg)) return arg;
|
|
33
|
+
return "'" + arg.replace(/'/g, "'\"'\"'") + "'";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseGitStatus(porcelain) {
|
|
37
|
+
const isDirty = porcelain.trim().length > 0;
|
|
38
|
+
const lines = porcelain.trim().split('\n').filter(l => l.length > 0);
|
|
39
|
+
const modified = lines.filter(l => /^[ AM][MD]/.test(l)).map(l => l.substring(3));
|
|
40
|
+
const untracked = lines.filter(l => /^\?\?/.test(l)).map(l => l.substring(3));
|
|
41
|
+
const deleted = lines.filter(l => /^[ A]D/.test(l)).map(l => l.substring(3));
|
|
42
|
+
return { isDirty, modified, untracked, deleted, all: modified.concat(untracked, deleted) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sendSpoolRequest(lang, code, timeoutMs = 30000) {
|
|
46
|
+
const gmDir = path.join(process.cwd(), '.gm');
|
|
47
|
+
const spoolIn = path.join(gmDir, 'exec-spool', 'in', lang);
|
|
48
|
+
const spoolOut = path.join(gmDir, 'exec-spool', 'out');
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(spoolIn)) {
|
|
51
|
+
try { fs.mkdirSync(spoolIn, { recursive: true }); } catch (e) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let taskId = '';
|
|
55
|
+
try {
|
|
56
|
+
taskId = fs.readdirSync(spoolOut).filter(f => f.endsWith('.json')).length + 1;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
taskId = Math.random().toString(36).substring(7);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ext = lang === 'bash' ? 'sh' : 'js';
|
|
62
|
+
const inPath = path.join(spoolIn, `${taskId}.${ext}`);
|
|
63
|
+
const outPath = path.join(spoolOut, `${taskId}.out`);
|
|
64
|
+
const errPath = path.join(spoolOut, `${taskId}.err`);
|
|
65
|
+
const jsonPath = path.join(spoolOut, `${taskId}.json`);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(inPath, code, 'utf8');
|
|
69
|
+
} catch (e) {
|
|
70
|
+
emitGitEvent('error', 'Could not write spool file', { path: inPath, error: e.message });
|
|
71
|
+
throw new Error(`Could not write spool request: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const startTime = Date.now();
|
|
75
|
+
const deadline = startTime + timeoutMs;
|
|
76
|
+
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
if (fs.existsSync(jsonPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const metadata = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
81
|
+
const stdout = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
|
|
82
|
+
const stderr = fs.existsSync(errPath) ? fs.readFileSync(errPath, 'utf8') : '';
|
|
83
|
+
return { ok: !metadata.timedOut && metadata.exitCode === 0, stdout, stderr, exitCode: metadata.exitCode, durationMs: metadata.durationMs };
|
|
84
|
+
} catch (e) {
|
|
85
|
+
emitGitEvent('error', 'Could not parse spool response', { path: jsonPath, error: e.message });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await new Promise(r => setTimeout(r, 100));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`Spool request timed out after ${timeoutMs}ms`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function commit(message, files = [], sessionId = 'unknown') {
|
|
95
|
+
emitGitEvent('info', 'commit() called', { message: message.substring(0, 72), fileCount: files.length, sessionId });
|
|
96
|
+
|
|
97
|
+
if (!message || message.trim().length === 0) {
|
|
98
|
+
emitGitEvent('error', 'commit message required', { sessionId });
|
|
99
|
+
throw new Error('Commit message required');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const summary = message.split('\n')[0];
|
|
103
|
+
if (summary.length > 72) {
|
|
104
|
+
emitGitEvent('warn', 'commit summary exceeds 72 chars', { length: summary.length, sessionId });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const filesToStage = files && files.length > 0 ? files : ['.'];
|
|
108
|
+
const stageCmd = filesToStage.map(f => `git add ${escapeShellArg(f)}`).join(' && ');
|
|
109
|
+
const commitMsg = message.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
|
110
|
+
const commitCmd = `git -c user.name=${escapeShellArg(GIT_USER)} -c user.email=${escapeShellArg(GIT_EMAIL)} commit -m "${commitMsg}"`;
|
|
111
|
+
|
|
112
|
+
const script = `${stageCmd} && ${commitCmd}`;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = await sendSpoolRequest('bash', script, 30000);
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
const err = result.stderr || result.stdout || 'unknown error';
|
|
118
|
+
emitGitEvent('error', 'commit failed', { error: err.substring(0, 200), sessionId });
|
|
119
|
+
throw new Error(`Commit failed: ${err}`);
|
|
120
|
+
}
|
|
121
|
+
emitGitEvent('info', 'commit succeeded', { message: summary, fileCount: files.length, sessionId });
|
|
122
|
+
return { ok: true, message: summary };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
emitGitEvent('error', 'commit error', { error: e.message, sessionId });
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function push(branch = 'main', sessionId = 'unknown') {
|
|
130
|
+
emitGitEvent('info', 'push() called', { branch, sessionId });
|
|
131
|
+
|
|
132
|
+
const pushCmd = `git push origin ${escapeShellArg(branch)} 2>&1`;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const result = await sendSpoolRequest('bash', pushCmd, 60000);
|
|
136
|
+
if (!result.ok || result.stderr.includes('fatal') || result.stdout.includes('fatal')) {
|
|
137
|
+
const err = result.stderr || result.stdout || 'unknown error';
|
|
138
|
+
|
|
139
|
+
if (err.includes('remote: error') || err.includes('Permission denied')) {
|
|
140
|
+
emitGitEvent('error', 'push auth failed', { branch, sessionId });
|
|
141
|
+
throw new Error(`Push authentication failed. Check credentials for branch: ${branch}\n${err}`);
|
|
142
|
+
}
|
|
143
|
+
if (err.includes('no changes added') || err.includes('nothing to commit')) {
|
|
144
|
+
emitGitEvent('info', 'push: nothing to push', { branch, sessionId });
|
|
145
|
+
return { ok: true, message: 'nothing to push' };
|
|
146
|
+
}
|
|
147
|
+
emitGitEvent('error', 'push failed', { error: err.substring(0, 200), branch, sessionId });
|
|
148
|
+
throw new Error(`Push failed: ${err}`);
|
|
149
|
+
}
|
|
150
|
+
emitGitEvent('info', 'push succeeded', { branch, sessionId });
|
|
151
|
+
return { ok: true, message: `pushed ${branch}` };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
emitGitEvent('error', 'push error', { error: e.message, branch, sessionId });
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function status(sessionId = 'unknown') {
|
|
159
|
+
emitGitEvent('info', 'status() called', { sessionId });
|
|
160
|
+
|
|
161
|
+
const statusCmd = 'git status --porcelain';
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const result = await sendSpoolRequest('bash', statusCmd, 10000);
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
emitGitEvent('error', 'status failed', { error: result.stderr, sessionId });
|
|
167
|
+
throw new Error(`Status check failed: ${result.stderr}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const parsed = parseGitStatus(result.stdout);
|
|
171
|
+
emitGitEvent('info', 'status retrieved', { isDirty: parsed.isDirty, modifiedCount: parsed.modified.length, sessionId });
|
|
172
|
+
return { ok: true, ...parsed };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
emitGitEvent('error', 'status error', { error: e.message, sessionId });
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function diff(sessionId = 'unknown') {
|
|
180
|
+
emitGitEvent('info', 'diff() called', { sessionId });
|
|
181
|
+
|
|
182
|
+
const diffCmd = 'git diff HEAD';
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const result = await sendSpoolRequest('bash', diffCmd, 30000);
|
|
186
|
+
if (!result.ok && !result.stdout) {
|
|
187
|
+
emitGitEvent('error', 'diff failed', { error: result.stderr, sessionId });
|
|
188
|
+
throw new Error(`Diff failed: ${result.stderr}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
emitGitEvent('info', 'diff retrieved', { hasChanges: result.stdout.length > 0, sessionId });
|
|
192
|
+
return { ok: true, diff: result.stdout };
|
|
193
|
+
} catch (e) {
|
|
194
|
+
emitGitEvent('error', 'diff error', { error: e.message, sessionId });
|
|
195
|
+
throw e;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function log(sessionId = 'unknown', count = 10) {
|
|
200
|
+
emitGitEvent('info', 'log() called', { count, sessionId });
|
|
201
|
+
|
|
202
|
+
const logCmd = `git log -${count} --oneline`;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const result = await sendSpoolRequest('bash', logCmd, 10000);
|
|
206
|
+
if (!result.ok) {
|
|
207
|
+
emitGitEvent('error', 'log failed', { error: result.stderr, sessionId });
|
|
208
|
+
throw new Error(`Log failed: ${result.stderr}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const commits = result.stdout.trim().split('\n').filter(l => l.length > 0);
|
|
212
|
+
emitGitEvent('info', 'log retrieved', { commitCount: commits.length, sessionId });
|
|
213
|
+
return { ok: true, commits };
|
|
214
|
+
} catch (e) {
|
|
215
|
+
emitGitEvent('error', 'log error', { error: e.message, sessionId });
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function checkout(branch, sessionId = 'unknown') {
|
|
221
|
+
emitGitEvent('info', 'checkout() called', { branch, sessionId });
|
|
222
|
+
|
|
223
|
+
if (!branch || branch.trim().length === 0) {
|
|
224
|
+
emitGitEvent('error', 'branch name required', { sessionId });
|
|
225
|
+
throw new Error('Branch name required');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const checkCmd = `git rev-parse --verify ${escapeShellArg(branch)} 2>&1`;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const checkResult = await sendSpoolRequest('bash', checkCmd, 10000);
|
|
232
|
+
if (!checkResult.ok || checkResult.stderr.includes('fatal') || checkResult.stdout.includes('fatal')) {
|
|
233
|
+
emitGitEvent('error', 'checkout: branch not found', { branch, sessionId });
|
|
234
|
+
throw new Error(`Branch not found: ${branch}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const checkoutCmd = `git checkout ${escapeShellArg(branch)}`;
|
|
238
|
+
const result = await sendSpoolRequest('bash', checkoutCmd, 10000);
|
|
239
|
+
|
|
240
|
+
if (!result.ok) {
|
|
241
|
+
emitGitEvent('error', 'checkout failed', { error: result.stderr, branch, sessionId });
|
|
242
|
+
throw new Error(`Checkout failed: ${result.stderr}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
emitGitEvent('info', 'checkout succeeded', { branch, sessionId });
|
|
246
|
+
return { ok: true, branch };
|
|
247
|
+
} catch (e) {
|
|
248
|
+
emitGitEvent('error', 'checkout error', { error: e.message, branch, sessionId });
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function rebase(upstream, sessionId = 'unknown') {
|
|
254
|
+
emitGitEvent('info', 'rebase() called', { upstream, sessionId });
|
|
255
|
+
|
|
256
|
+
if (!upstream || upstream.trim().length === 0) {
|
|
257
|
+
emitGitEvent('error', 'upstream branch required', { sessionId });
|
|
258
|
+
throw new Error('Upstream branch required');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const rebaseCmd = `git rebase ${escapeShellArg(upstream)} 2>&1`;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const result = await sendSpoolRequest('bash', rebaseCmd, 60000);
|
|
265
|
+
|
|
266
|
+
if (result.stderr.includes('CONFLICT') || result.stdout.includes('CONFLICT')) {
|
|
267
|
+
emitGitEvent('warn', 'rebase: conflicts detected', { upstream, sessionId });
|
|
268
|
+
const statusCmd = 'git status --porcelain';
|
|
269
|
+
const statusResult = await sendSpoolRequest('bash', statusCmd, 10000);
|
|
270
|
+
const conflicts = statusResult.stdout.split('\n').filter(l => l.startsWith('UU') || l.startsWith('AA') || l.startsWith('DD'));
|
|
271
|
+
return { ok: false, conflicts: true, conflictFiles: conflicts, message: 'Rebase halted due to conflicts. Resolve conflicts and run git rebase --continue' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!result.ok) {
|
|
275
|
+
emitGitEvent('error', 'rebase failed', { error: result.stderr, upstream, sessionId });
|
|
276
|
+
throw new Error(`Rebase failed: ${result.stderr}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
emitGitEvent('info', 'rebase succeeded', { upstream, sessionId });
|
|
280
|
+
return { ok: true, upstream };
|
|
281
|
+
} catch (e) {
|
|
282
|
+
emitGitEvent('error', 'rebase error', { error: e.message, upstream, sessionId });
|
|
283
|
+
throw e;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function cherryPick(commit, sessionId = 'unknown') {
|
|
288
|
+
emitGitEvent('info', 'cherryPick() called', { commit, sessionId });
|
|
289
|
+
|
|
290
|
+
if (!commit || commit.trim().length === 0) {
|
|
291
|
+
emitGitEvent('error', 'commit hash required', { sessionId });
|
|
292
|
+
throw new Error('Commit hash required');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const pickCmd = `git cherry-pick ${escapeShellArg(commit)} 2>&1`;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const result = await sendSpoolRequest('bash', pickCmd, 60000);
|
|
299
|
+
|
|
300
|
+
if (result.stderr.includes('CONFLICT') || result.stdout.includes('CONFLICT')) {
|
|
301
|
+
emitGitEvent('warn', 'cherry-pick: conflicts detected', { commit, sessionId });
|
|
302
|
+
return { ok: false, conflicts: true, message: 'Cherry-pick halted due to conflicts. Resolve conflicts and run git cherry-pick --continue' };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!result.ok) {
|
|
306
|
+
emitGitEvent('error', 'cherry-pick failed', { error: result.stderr, commit, sessionId });
|
|
307
|
+
throw new Error(`Cherry-pick failed: ${result.stderr}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
emitGitEvent('info', 'cherry-pick succeeded', { commit, sessionId });
|
|
311
|
+
return { ok: true, commit };
|
|
312
|
+
} catch (e) {
|
|
313
|
+
emitGitEvent('error', 'cherry-pick error', { error: e.message, commit, sessionId });
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
commit,
|
|
320
|
+
push,
|
|
321
|
+
status,
|
|
322
|
+
diff,
|
|
323
|
+
log,
|
|
324
|
+
checkout,
|
|
325
|
+
rebase,
|
|
326
|
+
cherryPick,
|
|
327
|
+
escapeShellArg,
|
|
328
|
+
parseGitStatus,
|
|
329
|
+
GIT_USER,
|
|
330
|
+
GIT_EMAIL,
|
|
331
|
+
};
|
package/lib/learning.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const spool = require('./spool.js');
|
|
5
|
+
|
|
6
|
+
const RS_LEARN_HOST = '127.0.0.1';
|
|
7
|
+
const RS_LEARN_PORT = 4801;
|
|
8
|
+
const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
|
|
9
|
+
|
|
10
|
+
let daemonBootstrap = null;
|
|
11
|
+
function getDaemonBootstrap() {
|
|
12
|
+
if (!daemonBootstrap) {
|
|
13
|
+
try {
|
|
14
|
+
daemonBootstrap = require('./daemon-bootstrap.js');
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error('[learning] Failed to load daemon-bootstrap:', e.message);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return daemonBootstrap;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let requestIdCounter = 1000;
|
|
24
|
+
|
|
25
|
+
function getRequestId() {
|
|
26
|
+
return ++requestIdCounter;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function emitLearningEvent(action, severity, message, details = {}) {
|
|
30
|
+
try {
|
|
31
|
+
const date = new Date().toISOString().split('T')[0];
|
|
32
|
+
const logDir = path.join(LOG_DIR, date);
|
|
33
|
+
if (!fs.existsSync(logDir)) {
|
|
34
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
const logFile = path.join(logDir, 'learning.jsonl');
|
|
37
|
+
const entry = {
|
|
38
|
+
ts: new Date().toISOString(),
|
|
39
|
+
action,
|
|
40
|
+
severity,
|
|
41
|
+
message,
|
|
42
|
+
...details,
|
|
43
|
+
};
|
|
44
|
+
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(`[learning] Failed to emit event: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureDaemonRunning(sessionId = null) {
|
|
51
|
+
const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'unknown';
|
|
52
|
+
const bootstrap = getDaemonBootstrap();
|
|
53
|
+
|
|
54
|
+
if (!bootstrap) {
|
|
55
|
+
emitLearningEvent('daemon', 'warn', 'daemon-bootstrap not available, skipping spawn', { sessionId: sid });
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
const result = await bootstrap.ensureRsLearningDaemonRunning();
|
|
62
|
+
emitLearningEvent('daemon', 'info', 'daemon spawn result', {
|
|
63
|
+
sessionId: sid,
|
|
64
|
+
ok: result.ok,
|
|
65
|
+
already_running: result.already_running,
|
|
66
|
+
durationMs: Date.now() - startTime,
|
|
67
|
+
});
|
|
68
|
+
return result.ok === true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
emitLearningEvent('daemon', 'warn', 'daemon spawn failed', {
|
|
71
|
+
sessionId: sid,
|
|
72
|
+
error: error.message,
|
|
73
|
+
});
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function checkLearningAvailable(timeoutMs = 500) {
|
|
79
|
+
const sessionId = process.env.CLAUDE_SESSION_ID || 'unknown';
|
|
80
|
+
try {
|
|
81
|
+
const result = await spool.execSpool('health', 'health', { timeoutMs, sessionId });
|
|
82
|
+
const ok = !!(result && result.ok);
|
|
83
|
+
emitLearningEvent('check', ok ? 'info' : 'warn', ok ? 'rs-learn reachable (spool)' : 'rs-learn unavailable (spool)', { sessionId, timeoutMs });
|
|
84
|
+
return ok;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
emitLearningEvent('check', 'warn', 'rs-learn availability check failed (spool)', { sessionId, timeoutMs, error: e.message });
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function queryLearning(query, discipline = 'default', sessionId = null) {
|
|
92
|
+
const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'unknown';
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
emitLearningEvent('query', 'info', `Learning query: ${query}`, { sessionId: sid, query, discipline });
|
|
97
|
+
|
|
98
|
+
const available = await checkLearningAvailable();
|
|
99
|
+
if (!available) {
|
|
100
|
+
emitLearningEvent('query', 'warn', 'Learning unavailable', { sessionId: sid, query });
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const payload = discipline && discipline !== 'default' ? `@${discipline} ${query}` : query;
|
|
105
|
+
const result = await spool.execRecall(payload, { timeoutMs: 5000, sessionId: sid });
|
|
106
|
+
if (!result.ok) throw new Error(result.stderr || result.stdout || 'recall failed');
|
|
107
|
+
|
|
108
|
+
emitLearningEvent('query', 'info', 'Learning query completed', {
|
|
109
|
+
sessionId: sid,
|
|
110
|
+
query,
|
|
111
|
+
discipline,
|
|
112
|
+
hitCount: (result.stdout || '').length > 0 ? 1 : 0,
|
|
113
|
+
durationMs: Date.now() - startTime,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return result.stdout || '';
|
|
117
|
+
} catch (error) {
|
|
118
|
+
emitLearningEvent('query', 'error', `Learning query failed: ${query}`, {
|
|
119
|
+
sessionId: sid,
|
|
120
|
+
query,
|
|
121
|
+
error: error.message,
|
|
122
|
+
durationMs: Date.now() - startTime,
|
|
123
|
+
});
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function persistLearning(fact, discipline = 'default', sessionId = null) {
|
|
129
|
+
const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'unknown';
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
emitLearningEvent('persist', 'info', 'Learning persist initiated', { sessionId: sid, discipline, factLength: fact.length });
|
|
134
|
+
|
|
135
|
+
const available = await checkLearningAvailable();
|
|
136
|
+
if (!available) {
|
|
137
|
+
emitLearningEvent('persist', 'error', 'Learning unavailable for persist', { sessionId: sid, discipline });
|
|
138
|
+
throw new Error('rs-learn daemon not available');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const payload = discipline && discipline !== 'default' ? `@${discipline}\n${fact}` : fact;
|
|
142
|
+
const result = await spool.execMemorize(payload, { timeoutMs: 5000, sessionId: sid });
|
|
143
|
+
if (!result.ok) throw new Error(result.stderr || result.stdout || 'memorize failed');
|
|
144
|
+
|
|
145
|
+
emitLearningEvent('persist', 'info', 'Learning persist completed', {
|
|
146
|
+
sessionId: sid,
|
|
147
|
+
discipline,
|
|
148
|
+
durationMs: Date.now() - startTime,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
emitLearningEvent('persist', 'error', `Learning persist failed: ${error.message}`, {
|
|
154
|
+
sessionId: sid,
|
|
155
|
+
discipline,
|
|
156
|
+
durationMs: Date.now() - startTime,
|
|
157
|
+
});
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
checkLearningAvailable,
|
|
164
|
+
queryLearning,
|
|
165
|
+
persistLearning,
|
|
166
|
+
ensureDaemonRunning,
|
|
167
|
+
RS_LEARN_HOST,
|
|
168
|
+
RS_LEARN_PORT,
|
|
169
|
+
};
|