gm-skill 0.1.1 → 0.1.2

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/lib/git.js ADDED
@@ -0,0 +1,332 @@
1
+ const net = require('net');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+
6
+ const GIT_USER = 'lanmower';
7
+ const GIT_EMAIL = 'almagestfraternite@gmail.com';
8
+
9
+ function emitGitEvent(severity, message, data = {}) {
10
+ const logDir = path.join(os.homedir(), '.claude', 'gm-log', new Date().toISOString().split('T')[0]);
11
+ if (!fs.existsSync(logDir)) {
12
+ try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}
13
+ }
14
+ const logFile = path.join(logDir, 'git.jsonl');
15
+ try {
16
+ fs.appendFileSync(logFile, JSON.stringify({
17
+ ts: new Date().toISOString(),
18
+ subsystem: 'git',
19
+ severity,
20
+ message,
21
+ ...data,
22
+ }) + '\n');
23
+ } catch (e) {}
24
+ }
25
+
26
+ function escapeShellArg(arg) {
27
+ if (os.platform() === 'win32') {
28
+ if (!arg) return '""';
29
+ if (/^[a-zA-Z0-9._\-/:=\\]+$/.test(arg)) return arg;
30
+ return '"' + arg.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`') + '"';
31
+ }
32
+ if (!arg) return "''";
33
+ if (/^[a-zA-Z0-9._\-/:=]+$/.test(arg)) return arg;
34
+ return "'" + arg.replace(/'/g, "'\"'\"'") + "'";
35
+ }
36
+
37
+ function parseGitStatus(porcelain) {
38
+ const isDirty = porcelain.trim().length > 0;
39
+ const lines = porcelain.trim().split('\n').filter(l => l.length > 0);
40
+ const modified = lines.filter(l => /^[ AM][MD]/.test(l)).map(l => l.substring(3));
41
+ const untracked = lines.filter(l => /^\?\?/.test(l)).map(l => l.substring(3));
42
+ const deleted = lines.filter(l => /^[ A]D/.test(l)).map(l => l.substring(3));
43
+ return { isDirty, modified, untracked, deleted, all: modified.concat(untracked, deleted) };
44
+ }
45
+
46
+ async function sendSpoolRequest(lang, code, timeoutMs = 30000) {
47
+ const gmDir = path.join(process.cwd(), '.gm');
48
+ const spoolIn = path.join(gmDir, 'exec-spool', 'in', lang);
49
+ const spoolOut = path.join(gmDir, 'exec-spool', 'out');
50
+
51
+ if (!fs.existsSync(spoolIn)) {
52
+ try { fs.mkdirSync(spoolIn, { recursive: true }); } catch (e) {}
53
+ }
54
+
55
+ let taskId = '';
56
+ try {
57
+ taskId = fs.readdirSync(spoolOut).filter(f => f.endsWith('.json')).length + 1;
58
+ } catch (e) {
59
+ taskId = Math.random().toString(36).substring(7);
60
+ }
61
+
62
+ const ext = lang === 'bash' ? 'sh' : 'js';
63
+ const inPath = path.join(spoolIn, `${taskId}.${ext}`);
64
+ const outPath = path.join(spoolOut, `${taskId}.out`);
65
+ const errPath = path.join(spoolOut, `${taskId}.err`);
66
+ const jsonPath = path.join(spoolOut, `${taskId}.json`);
67
+
68
+ try {
69
+ fs.writeFileSync(inPath, code, 'utf8');
70
+ } catch (e) {
71
+ emitGitEvent('error', 'Could not write spool file', { path: inPath, error: e.message });
72
+ throw new Error(`Could not write spool request: ${e.message}`);
73
+ }
74
+
75
+ const startTime = Date.now();
76
+ const deadline = startTime + timeoutMs;
77
+
78
+ while (Date.now() < deadline) {
79
+ if (fs.existsSync(jsonPath)) {
80
+ try {
81
+ const metadata = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
82
+ const stdout = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
83
+ const stderr = fs.existsSync(errPath) ? fs.readFileSync(errPath, 'utf8') : '';
84
+ return { ok: !metadata.timedOut && metadata.exitCode === 0, stdout, stderr, exitCode: metadata.exitCode, durationMs: metadata.durationMs };
85
+ } catch (e) {
86
+ emitGitEvent('error', 'Could not parse spool response', { path: jsonPath, error: e.message });
87
+ }
88
+ }
89
+ await new Promise(r => setTimeout(r, 100));
90
+ }
91
+
92
+ throw new Error(`Spool request timed out after ${timeoutMs}ms`);
93
+ }
94
+
95
+ async function commit(message, files = [], sessionId = 'unknown') {
96
+ emitGitEvent('info', 'commit() called', { message: message.substring(0, 72), fileCount: files.length, sessionId });
97
+
98
+ if (!message || message.trim().length === 0) {
99
+ emitGitEvent('error', 'commit message required', { sessionId });
100
+ throw new Error('Commit message required');
101
+ }
102
+
103
+ const summary = message.split('\n')[0];
104
+ if (summary.length > 72) {
105
+ emitGitEvent('warn', 'commit summary exceeds 72 chars', { length: summary.length, sessionId });
106
+ }
107
+
108
+ const filesToStage = files && files.length > 0 ? files : ['.'];
109
+ const stageCmd = filesToStage.map(f => `git add ${escapeShellArg(f)}`).join(' && ');
110
+ const commitMsg = message.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
111
+ const commitCmd = `git -c user.name=${escapeShellArg(GIT_USER)} -c user.email=${escapeShellArg(GIT_EMAIL)} commit -m "${commitMsg}"`;
112
+
113
+ const script = `${stageCmd} && ${commitCmd}`;
114
+
115
+ try {
116
+ const result = await sendSpoolRequest('bash', script, 30000);
117
+ if (!result.ok) {
118
+ const err = result.stderr || result.stdout || 'unknown error';
119
+ emitGitEvent('error', 'commit failed', { error: err.substring(0, 200), sessionId });
120
+ throw new Error(`Commit failed: ${err}`);
121
+ }
122
+ emitGitEvent('info', 'commit succeeded', { message: summary, fileCount: files.length, sessionId });
123
+ return { ok: true, message: summary };
124
+ } catch (e) {
125
+ emitGitEvent('error', 'commit error', { error: e.message, sessionId });
126
+ throw e;
127
+ }
128
+ }
129
+
130
+ async function push(branch = 'main', sessionId = 'unknown') {
131
+ emitGitEvent('info', 'push() called', { branch, sessionId });
132
+
133
+ const pushCmd = `git push origin ${escapeShellArg(branch)} 2>&1`;
134
+
135
+ try {
136
+ const result = await sendSpoolRequest('bash', pushCmd, 60000);
137
+ if (!result.ok || result.stderr.includes('fatal') || result.stdout.includes('fatal')) {
138
+ const err = result.stderr || result.stdout || 'unknown error';
139
+
140
+ if (err.includes('remote: error') || err.includes('Permission denied')) {
141
+ emitGitEvent('error', 'push auth failed', { branch, sessionId });
142
+ throw new Error(`Push authentication failed. Check credentials for branch: ${branch}\n${err}`);
143
+ }
144
+ if (err.includes('no changes added') || err.includes('nothing to commit')) {
145
+ emitGitEvent('info', 'push: nothing to push', { branch, sessionId });
146
+ return { ok: true, message: 'nothing to push' };
147
+ }
148
+ emitGitEvent('error', 'push failed', { error: err.substring(0, 200), branch, sessionId });
149
+ throw new Error(`Push failed: ${err}`);
150
+ }
151
+ emitGitEvent('info', 'push succeeded', { branch, sessionId });
152
+ return { ok: true, message: `pushed ${branch}` };
153
+ } catch (e) {
154
+ emitGitEvent('error', 'push error', { error: e.message, branch, sessionId });
155
+ throw e;
156
+ }
157
+ }
158
+
159
+ async function status(sessionId = 'unknown') {
160
+ emitGitEvent('info', 'status() called', { sessionId });
161
+
162
+ const statusCmd = 'git status --porcelain';
163
+
164
+ try {
165
+ const result = await sendSpoolRequest('bash', statusCmd, 10000);
166
+ if (!result.ok) {
167
+ emitGitEvent('error', 'status failed', { error: result.stderr, sessionId });
168
+ throw new Error(`Status check failed: ${result.stderr}`);
169
+ }
170
+
171
+ const parsed = parseGitStatus(result.stdout);
172
+ emitGitEvent('info', 'status retrieved', { isDirty: parsed.isDirty, modifiedCount: parsed.modified.length, sessionId });
173
+ return { ok: true, ...parsed };
174
+ } catch (e) {
175
+ emitGitEvent('error', 'status error', { error: e.message, sessionId });
176
+ throw e;
177
+ }
178
+ }
179
+
180
+ async function diff(sessionId = 'unknown') {
181
+ emitGitEvent('info', 'diff() called', { sessionId });
182
+
183
+ const diffCmd = 'git diff HEAD';
184
+
185
+ try {
186
+ const result = await sendSpoolRequest('bash', diffCmd, 30000);
187
+ if (!result.ok && !result.stdout) {
188
+ emitGitEvent('error', 'diff failed', { error: result.stderr, sessionId });
189
+ throw new Error(`Diff failed: ${result.stderr}`);
190
+ }
191
+
192
+ emitGitEvent('info', 'diff retrieved', { hasChanges: result.stdout.length > 0, sessionId });
193
+ return { ok: true, diff: result.stdout };
194
+ } catch (e) {
195
+ emitGitEvent('error', 'diff error', { error: e.message, sessionId });
196
+ throw e;
197
+ }
198
+ }
199
+
200
+ async function log(sessionId = 'unknown', count = 10) {
201
+ emitGitEvent('info', 'log() called', { count, sessionId });
202
+
203
+ const logCmd = `git log -${count} --oneline`;
204
+
205
+ try {
206
+ const result = await sendSpoolRequest('bash', logCmd, 10000);
207
+ if (!result.ok) {
208
+ emitGitEvent('error', 'log failed', { error: result.stderr, sessionId });
209
+ throw new Error(`Log failed: ${result.stderr}`);
210
+ }
211
+
212
+ const commits = result.stdout.trim().split('\n').filter(l => l.length > 0);
213
+ emitGitEvent('info', 'log retrieved', { commitCount: commits.length, sessionId });
214
+ return { ok: true, commits };
215
+ } catch (e) {
216
+ emitGitEvent('error', 'log error', { error: e.message, sessionId });
217
+ throw e;
218
+ }
219
+ }
220
+
221
+ async function checkout(branch, sessionId = 'unknown') {
222
+ emitGitEvent('info', 'checkout() called', { branch, sessionId });
223
+
224
+ if (!branch || branch.trim().length === 0) {
225
+ emitGitEvent('error', 'branch name required', { sessionId });
226
+ throw new Error('Branch name required');
227
+ }
228
+
229
+ const checkCmd = `git rev-parse --verify ${escapeShellArg(branch)} 2>&1`;
230
+
231
+ try {
232
+ const checkResult = await sendSpoolRequest('bash', checkCmd, 10000);
233
+ if (!checkResult.ok || checkResult.stderr.includes('fatal') || checkResult.stdout.includes('fatal')) {
234
+ emitGitEvent('error', 'checkout: branch not found', { branch, sessionId });
235
+ throw new Error(`Branch not found: ${branch}`);
236
+ }
237
+
238
+ const checkoutCmd = `git checkout ${escapeShellArg(branch)}`;
239
+ const result = await sendSpoolRequest('bash', checkoutCmd, 10000);
240
+
241
+ if (!result.ok) {
242
+ emitGitEvent('error', 'checkout failed', { error: result.stderr, branch, sessionId });
243
+ throw new Error(`Checkout failed: ${result.stderr}`);
244
+ }
245
+
246
+ emitGitEvent('info', 'checkout succeeded', { branch, sessionId });
247
+ return { ok: true, branch };
248
+ } catch (e) {
249
+ emitGitEvent('error', 'checkout error', { error: e.message, branch, sessionId });
250
+ throw e;
251
+ }
252
+ }
253
+
254
+ async function rebase(upstream, sessionId = 'unknown') {
255
+ emitGitEvent('info', 'rebase() called', { upstream, sessionId });
256
+
257
+ if (!upstream || upstream.trim().length === 0) {
258
+ emitGitEvent('error', 'upstream branch required', { sessionId });
259
+ throw new Error('Upstream branch required');
260
+ }
261
+
262
+ const rebaseCmd = `git rebase ${escapeShellArg(upstream)} 2>&1`;
263
+
264
+ try {
265
+ const result = await sendSpoolRequest('bash', rebaseCmd, 60000);
266
+
267
+ if (result.stderr.includes('CONFLICT') || result.stdout.includes('CONFLICT')) {
268
+ emitGitEvent('warn', 'rebase: conflicts detected', { upstream, sessionId });
269
+ const statusCmd = 'git status --porcelain';
270
+ const statusResult = await sendSpoolRequest('bash', statusCmd, 10000);
271
+ const conflicts = statusResult.stdout.split('\n').filter(l => l.startsWith('UU') || l.startsWith('AA') || l.startsWith('DD'));
272
+ return { ok: false, conflicts: true, conflictFiles: conflicts, message: 'Rebase halted due to conflicts. Resolve conflicts and run git rebase --continue' };
273
+ }
274
+
275
+ if (!result.ok) {
276
+ emitGitEvent('error', 'rebase failed', { error: result.stderr, upstream, sessionId });
277
+ throw new Error(`Rebase failed: ${result.stderr}`);
278
+ }
279
+
280
+ emitGitEvent('info', 'rebase succeeded', { upstream, sessionId });
281
+ return { ok: true, upstream };
282
+ } catch (e) {
283
+ emitGitEvent('error', 'rebase error', { error: e.message, upstream, sessionId });
284
+ throw e;
285
+ }
286
+ }
287
+
288
+ async function cherryPick(commit, sessionId = 'unknown') {
289
+ emitGitEvent('info', 'cherryPick() called', { commit, sessionId });
290
+
291
+ if (!commit || commit.trim().length === 0) {
292
+ emitGitEvent('error', 'commit hash required', { sessionId });
293
+ throw new Error('Commit hash required');
294
+ }
295
+
296
+ const pickCmd = `git cherry-pick ${escapeShellArg(commit)} 2>&1`;
297
+
298
+ try {
299
+ const result = await sendSpoolRequest('bash', pickCmd, 60000);
300
+
301
+ if (result.stderr.includes('CONFLICT') || result.stdout.includes('CONFLICT')) {
302
+ emitGitEvent('warn', 'cherry-pick: conflicts detected', { commit, sessionId });
303
+ return { ok: false, conflicts: true, message: 'Cherry-pick halted due to conflicts. Resolve conflicts and run git cherry-pick --continue' };
304
+ }
305
+
306
+ if (!result.ok) {
307
+ emitGitEvent('error', 'cherry-pick failed', { error: result.stderr, commit, sessionId });
308
+ throw new Error(`Cherry-pick failed: ${result.stderr}`);
309
+ }
310
+
311
+ emitGitEvent('info', 'cherry-pick succeeded', { commit, sessionId });
312
+ return { ok: true, commit };
313
+ } catch (e) {
314
+ emitGitEvent('error', 'cherry-pick error', { error: e.message, commit, sessionId });
315
+ throw e;
316
+ }
317
+ }
318
+
319
+ module.exports = {
320
+ commit,
321
+ push,
322
+ status,
323
+ diff,
324
+ log,
325
+ checkout,
326
+ rebase,
327
+ cherryPick,
328
+ escapeShellArg,
329
+ parseGitStatus,
330
+ GIT_USER,
331
+ GIT_EMAIL,
332
+ };
package/lib/index.js ADDED
@@ -0,0 +1,37 @@
1
+ const daemon = require('./daemon-bootstrap.js');
2
+ const manifest = require('./manifest.js');
3
+ const loader = require('./loader.js');
4
+ const prepareModule = require('./prepare.js');
5
+
6
+ function getSkills() {
7
+ return manifest.getAllSkills();
8
+ }
9
+
10
+ function getSkill(name) {
11
+ return manifest.getSkill(name);
12
+ }
13
+
14
+ function loadSkill(skillName, baseDir) {
15
+ return loader.dynamicLoadSkill(skillName, baseDir);
16
+ }
17
+
18
+ function bootstrapDaemon(daemonName, cmd) {
19
+ return daemon.spawnDaemon(daemonName, cmd);
20
+ }
21
+
22
+ module.exports = {
23
+ getSkills: getSkills,
24
+ getSkill: getSkill,
25
+ loadSkill: loadSkill,
26
+ bootstrapDaemon: bootstrapDaemon,
27
+ checkState: daemon.checkState,
28
+ waitForReady: daemon.waitForReady,
29
+ getSocket: daemon.getSocket,
30
+ shutdown: daemon.shutdown,
31
+ emitEvent: daemon.emitEvent,
32
+ isDaemonRunning: daemon.isDaemonRunning,
33
+ checkPortReachable: daemon.checkPortReachable,
34
+ manifest: manifest,
35
+ loader: loader,
36
+ prepare: prepareModule.prepare
37
+ };
package/lib/loader.js ADDED
@@ -0,0 +1,66 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ function loadSkill(skillName, skillPath) {
5
+ if (!fs.existsSync(skillPath)) {
6
+ throw new Error(`Skill path does not exist: ${skillPath}`);
7
+ }
8
+
9
+ const indexPath = path.join(skillPath, 'index.js');
10
+ if (!fs.existsSync(indexPath)) {
11
+ throw new Error(`Skill index.js not found at ${indexPath}`);
12
+ }
13
+
14
+ try {
15
+ const skillModule = require(indexPath);
16
+ return skillModule;
17
+ } catch (e) {
18
+ throw new Error(`Failed to load skill ${skillName} from ${indexPath}: ${e.message}`);
19
+ }
20
+ }
21
+
22
+ function dynamicLoadSkill(skillName, baseDir) {
23
+ const searchDirs = [];
24
+
25
+ if (baseDir) {
26
+ searchDirs.push(path.join(baseDir, 'skills', skillName));
27
+ }
28
+
29
+ searchDirs.push(path.join(__dirname, '..', 'skills', skillName));
30
+ searchDirs.push(path.join(__dirname, '..', '..', 'gm-starter', 'skills', skillName));
31
+ searchDirs.push(path.join(process.cwd(), 'skills', skillName));
32
+
33
+ for (const searchDir of searchDirs) {
34
+ if (fs.existsSync(searchDir)) {
35
+ return loadSkill(skillName, searchDir);
36
+ }
37
+ }
38
+
39
+ throw new Error(`Skill "${skillName}" not found in any search directory: ${searchDirs.join(', ')}`);
40
+ }
41
+
42
+ function getSkillPath(skillName, baseDir) {
43
+ const searchDirs = [];
44
+
45
+ if (baseDir) {
46
+ searchDirs.push(path.join(baseDir, 'skills', skillName));
47
+ }
48
+
49
+ searchDirs.push(path.join(__dirname, '..', 'skills', skillName));
50
+ searchDirs.push(path.join(__dirname, '..', '..', 'gm-starter', 'skills', skillName));
51
+ searchDirs.push(path.join(process.cwd(), 'skills', skillName));
52
+
53
+ for (const searchDir of searchDirs) {
54
+ if (fs.existsSync(searchDir)) {
55
+ return searchDir;
56
+ }
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ module.exports = {
63
+ loadSkill,
64
+ dynamicLoadSkill,
65
+ getSkillPath
66
+ };
package/lib/manifest.js CHANGED
@@ -30,17 +30,20 @@ function parseSkillMarkdown(content) {
30
30
  }
31
31
 
32
32
  function readSkillManifest(skillName) {
33
- const skillMdPath = path.join(
34
- __dirname,
35
- '..',
36
- '..',
37
- 'gm-starter',
38
- 'skills',
39
- skillName,
40
- 'SKILL.md'
41
- );
42
-
43
- if (!fs.existsSync(skillMdPath)) {
33
+ const searchPaths = [
34
+ path.join(__dirname, '..', 'skills', skillName, 'SKILL.md'),
35
+ path.join(__dirname, '..', '..', 'gm-starter', 'skills', skillName, 'SKILL.md')
36
+ ];
37
+
38
+ let skillMdPath = null;
39
+ for (const p of searchPaths) {
40
+ if (fs.existsSync(p)) {
41
+ skillMdPath = p;
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (!skillMdPath) {
44
47
  return {
45
48
  name: skillName,
46
49
  description: '',
@@ -69,7 +72,7 @@ function readSkillManifest(skillName) {
69
72
  }
70
73
 
71
74
  function getManifest() {
72
- const skills = ['gm', 'gm-execute', 'gm-emit', 'gm-complete'];
75
+ const skills = ['gm', 'planning', 'gm-execute', 'gm-emit', 'gm-complete', 'update-docs'];
73
76
  return {
74
77
  name: 'gm-skill',
75
78
  version: '1.0.0',
@@ -83,7 +86,7 @@ function getSkill(name) {
83
86
  }
84
87
 
85
88
  function getAllSkills() {
86
- const skills = ['gm', 'gm-execute', 'gm-emit', 'gm-complete'];
89
+ const skills = ['gm', 'planning', 'gm-execute', 'gm-emit', 'gm-complete', 'update-docs'];
87
90
  return skills.map(name => readSkillManifest(name));
88
91
  }
89
92
 
package/lib/prepare.js ADDED
@@ -0,0 +1,14 @@
1
+ async function prepare(options = {}) {
2
+ const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID || 'default';
3
+ const cwd = options.cwd || process.cwd();
4
+
5
+ return {
6
+ sessionId,
7
+ cwd,
8
+ initialized: true
9
+ };
10
+ }
11
+
12
+ module.exports = {
13
+ prepare
14
+ };
package/lib/spool.js ADDED
@@ -0,0 +1,163 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function getSpoolBaseDir() {
5
+ return path.join(process.cwd(), '.gm', 'exec-spool');
6
+ }
7
+
8
+ function generateTaskId() {
9
+ return `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
10
+ }
11
+
12
+ function validateLang(lang) {
13
+ const valid = ['nodejs', 'python', 'bash', 'typescript', 'go', 'rust', 'c', 'cpp', 'java', 'deno'];
14
+ return valid.includes(lang) ? lang : 'nodejs';
15
+ }
16
+
17
+ function getExtForLang(lang) {
18
+ const langExt = {
19
+ nodejs: 'js', python: 'py', bash: 'sh', typescript: 'ts', go: 'go',
20
+ rust: 'rs', c: 'c', cpp: 'cpp', java: 'java', deno: 'ts'
21
+ };
22
+ return langExt[lang] || 'js';
23
+ }
24
+
25
+ function validateVerb(verb) {
26
+ const valid = ['codesearch', 'recall', 'memorize', 'wait', 'sleep', 'status', 'close', 'browser', 'runner', 'type', 'kill-port', 'forget', 'feedback', 'learn-status', 'learn-debug', 'learn-build', 'discipline', 'pause', 'health'];
27
+ return valid.includes(verb) ? verb : 'status';
28
+ }
29
+
30
+ function writeSpool(body, lang = 'nodejs', options = {}) {
31
+ const validLang = validateLang(lang);
32
+ const taskId = options.taskId || generateTaskId();
33
+ const inDir = path.join(getSpoolBaseDir(), 'in', validLang);
34
+ const inFile = path.join(inDir, `${taskId}.${getExtForLang(validLang)}`);
35
+
36
+ fs.mkdirSync(inDir, { recursive: true });
37
+
38
+ const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID;
39
+ const code = sessionId ? `const SESSION_ID = '${sessionId}';\n${body}` : body;
40
+ fs.writeFileSync(inFile, code, 'utf8');
41
+
42
+ return { id: taskId, path: inFile, lang: validLang, ext: getExtForLang(validLang) };
43
+ }
44
+
45
+ function writeSpoolVerb(body, verb, options = {}) {
46
+ const validVerb = validateVerb(verb);
47
+ const taskId = options.taskId || generateTaskId();
48
+ const inDir = path.join(getSpoolBaseDir(), 'in', validVerb);
49
+ const inFile = path.join(inDir, `${taskId}.txt`);
50
+
51
+ fs.mkdirSync(inDir, { recursive: true });
52
+ fs.writeFileSync(inFile, body, 'utf8');
53
+ return { id: taskId, path: inFile, verb: validVerb };
54
+ }
55
+
56
+ function readSpoolOutput(id) {
57
+ const outDir = path.join(getSpoolBaseDir(), 'out');
58
+ const outFile = path.join(outDir, `${id}.out`);
59
+ const errFile = path.join(outDir, `${id}.err`);
60
+ const jsonFile = path.join(outDir, `${id}.json`);
61
+
62
+ const stdout = fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : '';
63
+ const stderr = fs.existsSync(errFile) ? fs.readFileSync(errFile, 'utf8') : '';
64
+
65
+ let metadata = {};
66
+ if (fs.existsSync(jsonFile)) {
67
+ try {
68
+ metadata = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
69
+ } catch (e) {
70
+ metadata = { error: 'Failed to parse metadata' };
71
+ }
72
+ }
73
+
74
+ return {
75
+ id, stdout, stderr, metadata,
76
+ exitCode: metadata.exitCode,
77
+ durationMs: metadata.durationMs,
78
+ timedOut: metadata.timedOut || false
79
+ };
80
+ }
81
+
82
+ async function waitForCompletion(id, timeoutMs = 30000) {
83
+ const jsonFile = path.join(getSpoolBaseDir(), 'out', `${id}.json`);
84
+ const start = Date.now();
85
+
86
+ while (Date.now() - start < timeoutMs) {
87
+ if (fs.existsSync(jsonFile)) {
88
+ try {
89
+ const metadata = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
90
+ const output = readSpoolOutput(id);
91
+ return { ok: metadata.exitCode === 0 && !metadata.timedOut, ...output };
92
+ } catch (e) {
93
+ await new Promise(r => setTimeout(r, 50));
94
+ }
95
+ } else {
96
+ await new Promise(r => setTimeout(r, 50));
97
+ }
98
+ }
99
+
100
+ const output = readSpoolOutput(id);
101
+ return { ok: false, ...output, timedOut: true, stderr: output.stderr + `\n[timeout ${timeoutMs}ms]` };
102
+ }
103
+
104
+ async function execSpool(body, lang, options = {}) {
105
+ const timeoutMs = options.timeoutMs || 30000;
106
+ const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID;
107
+ const task = lang === 'nodejs' || lang === 'bash' ? writeSpool(body, lang, { sessionId }) : writeSpoolVerb(body, lang, {});
108
+ const result = await waitForCompletion(task.id, timeoutMs);
109
+ if (options.cleanup !== false) {
110
+ try { fs.unlinkSync(task.path); } catch (e) {}
111
+ }
112
+ return result;
113
+ }
114
+
115
+ async function execNodejs(body, options = {}) {
116
+ return execSpool(body, 'nodejs', options);
117
+ }
118
+
119
+ async function execBash(body, options = {}) {
120
+ return execSpool(body, 'bash', options);
121
+ }
122
+
123
+ async function execCodesearch(query, options = {}) {
124
+ return execSpool(query, 'codesearch', options);
125
+ }
126
+
127
+ async function execRecall(query, options = {}) {
128
+ return execSpool(query, 'recall', options);
129
+ }
130
+
131
+ async function execMemorize(fact, options = {}) {
132
+ return execSpool(fact, 'memorize', options);
133
+ }
134
+
135
+ function getAllOutputs() {
136
+ const outDir = path.join(getSpoolBaseDir(), 'out');
137
+ if (!fs.existsSync(outDir)) return [];
138
+
139
+ const files = fs.readdirSync(outDir);
140
+ const taskIds = new Set();
141
+ files.forEach(file => {
142
+ const match = file.match(/^(.+?)\.(out|err|json)$/);
143
+ if (match) taskIds.add(match[1]);
144
+ });
145
+ return Array.from(taskIds).map(id => readSpoolOutput(id));
146
+ }
147
+
148
+ function cleanupTempFiles(taskId) {
149
+ const outDir = path.join(getSpoolBaseDir(), 'out');
150
+ ['.out', '.err', '.json'].forEach(ext => {
151
+ try {
152
+ const file = path.join(outDir, `${taskId}${ext}`);
153
+ if (fs.existsSync(file)) fs.unlinkSync(file);
154
+ } catch (e) {}
155
+ });
156
+ }
157
+
158
+ module.exports = {
159
+ writeSpool, writeSpoolVerb, readSpoolOutput, waitForCompletion,
160
+ execNodejs, execBash, execCodesearch, execRecall, execMemorize,
161
+ getAllOutputs, cleanupTempFiles, getSpoolBaseDir, generateTaskId,
162
+ validateLang, getExtForLang, validateVerb
163
+ };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Unified skill library for gm platform implementations",
5
- "main": "index.js",
5
+ "main": "lib/index.js",
6
6
  "exports": {
7
- ".": "./index.js",
7
+ ".": "./lib/index.js",
8
8
  "./daemon-bootstrap": "./lib/daemon-bootstrap.js",
9
- "./manifest": "./lib/manifest.js"
9
+ "./manifest": "./lib/manifest.js",
10
+ "./prepare": "./lib/prepare.js",
11
+ "./loader": "./lib/loader.js"
10
12
  },
11
13
  "scripts": {
12
14
  "test": "node test.js"