gm-hermes 2.0.1074 → 2.0.1076

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/hermes-skill.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1074",
3
+ "version": "2.0.1076",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "homepage": "https://github.com/AnEntrypoint/gm",
package/index.html CHANGED
@@ -74,7 +74,7 @@ body { display: flex; flex-direction: column; min-height: 100vh; }
74
74
  <section>
75
75
  <div class="gm-section-label"><span class="slash">//</span>status</div>
76
76
  <div class="panel">
77
- <div class="panel-head"><span>release · v2.0.1074</span><span>probably emerging</span></div>
77
+ <div class="panel-head"><span>release · v2.0.1076</span><span>probably emerging</span></div>
78
78
  <div class="panel-body">
79
79
  <div class="row">
80
80
  <span class="code"><span style="color:var(--panel-accent)">●</span></span>
@@ -0,0 +1,130 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const {
5
+ createSession,
6
+ sendCommand,
7
+ getScreenshot,
8
+ closeSession,
9
+ isBrowserAvailable,
10
+ } = require('./browser');
11
+
12
+ const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
13
+
14
+ function emitHandlerEvent(severity, message, details) {
15
+ try {
16
+ const date = new Date().toISOString().split('T')[0];
17
+ const logDir = path.join(LOG_DIR, date);
18
+ if (!fs.existsSync(logDir)) {
19
+ fs.mkdirSync(logDir, { recursive: true });
20
+ }
21
+ const logFile = path.join(logDir, 'browser-handler.jsonl');
22
+ const entry = {
23
+ ts: new Date().toISOString(),
24
+ severity,
25
+ message,
26
+ ...details,
27
+ };
28
+ fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
29
+ } catch (e) {
30
+ console.error(`[browser-handler] Failed to emit event: ${e.message}`);
31
+ }
32
+ }
33
+
34
+ async function handleBrowserVerb(body, sessionId) {
35
+ const lines = body.trim().split('\n');
36
+ const action = lines[0]?.trim();
37
+ const args = lines.slice(1).join('\n').trim();
38
+
39
+ try {
40
+ emitHandlerEvent('info', 'Browser verb dispatched', {
41
+ sessionId,
42
+ action,
43
+ argsLength: args.length,
44
+ });
45
+
46
+ const available = await isBrowserAvailable();
47
+ if (!available) {
48
+ throw new Error(
49
+ 'Browser API unavailable at 127.0.0.1:5000. Ensure rs-exec is running with browser support enabled.'
50
+ );
51
+ }
52
+
53
+ switch (action) {
54
+ case 'start': {
55
+ const result = await createSession(sessionId);
56
+ console.log(JSON.stringify(result));
57
+ return result;
58
+ }
59
+
60
+ case 'stop': {
61
+ const result = await closeSession(sessionId);
62
+ console.log(JSON.stringify(result));
63
+ return result;
64
+ }
65
+
66
+ case 'screenshot': {
67
+ const result = await getScreenshot(sessionId);
68
+ console.log(JSON.stringify({
69
+ ok: result.ok,
70
+ mimeType: result.mimeType,
71
+ screenshotLength: result.screenshot?.length || 0,
72
+ screenshot: result.screenshot,
73
+ }));
74
+ return result;
75
+ }
76
+
77
+ case 'click':
78
+ case 'type':
79
+ case 'navigate':
80
+ case 'execute': {
81
+ let commandArgs = {};
82
+ if (args) {
83
+ try {
84
+ commandArgs = JSON.parse(args);
85
+ } catch (e) {
86
+ commandArgs = { value: args };
87
+ }
88
+ }
89
+
90
+ const result = await sendCommand(sessionId, action, commandArgs);
91
+ console.log(JSON.stringify(result));
92
+ return result;
93
+ }
94
+
95
+ default: {
96
+ let commandArgs = {};
97
+ if (args) {
98
+ try {
99
+ commandArgs = JSON.parse(args);
100
+ } catch (e) {
101
+ commandArgs = { value: args };
102
+ }
103
+ }
104
+ const result = await sendCommand(sessionId, action, commandArgs);
105
+ console.log(JSON.stringify(result));
106
+ return result;
107
+ }
108
+ }
109
+ } catch (e) {
110
+ emitHandlerEvent('error', 'Browser verb failed', {
111
+ sessionId,
112
+ action,
113
+ error: e.message,
114
+ });
115
+
116
+ const errorResponse = {
117
+ ok: false,
118
+ error: e.message,
119
+ action,
120
+ sessionId,
121
+ };
122
+
123
+ console.log(JSON.stringify(errorResponse));
124
+ throw e;
125
+ }
126
+ }
127
+
128
+ module.exports = {
129
+ handleBrowserVerb,
130
+ };
package/lib/browser.js ADDED
@@ -0,0 +1,131 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const spool = require('./spool.js');
5
+
6
+ const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
7
+ const SESSION_STATE_DIR = path.join(os.homedir(), '.gm', 'browser-sessions');
8
+
9
+ function emitBrowserEvent(severity, message, details) {
10
+ try {
11
+ const date = new Date().toISOString().split('T')[0];
12
+ const logDir = path.join(LOG_DIR, date);
13
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
14
+ fs.appendFileSync(path.join(logDir, 'browser.jsonl'), JSON.stringify({ ts: new Date().toISOString(), severity, message, ...details }) + '\n');
15
+ } catch (e) {
16
+ console.error(`[browser] Failed to emit event: ${e.message}`);
17
+ }
18
+ }
19
+
20
+ function loadSessionState(sessionId) {
21
+ try {
22
+ fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
23
+ const stateFile = path.join(SESSION_STATE_DIR, `${sessionId}.json`);
24
+ if (fs.existsSync(stateFile)) return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
25
+ } catch (e) {
26
+ emitBrowserEvent('warn', 'Failed to load session state', { sessionId, error: e.message });
27
+ }
28
+ return { sessionId, createdAt: new Date().toISOString(), commands: [] };
29
+ }
30
+
31
+ function saveSessionState(sessionId, state) {
32
+ try {
33
+ fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
34
+ fs.writeFileSync(path.join(SESSION_STATE_DIR, `${sessionId}.json`), JSON.stringify(state, null, 2));
35
+ } catch (e) {
36
+ emitBrowserEvent('warn', 'Failed to save session state', { sessionId, error: e.message });
37
+ }
38
+ }
39
+
40
+ function parseJsonFromStdout(stdout) {
41
+ const trimmed = (stdout || '').trim();
42
+ if (!trimmed) return null;
43
+ const lines = trimmed.split(/\r?\n/).filter(Boolean);
44
+ for (let i = lines.length - 1; i >= 0; i--) {
45
+ try {
46
+ return JSON.parse(lines[i]);
47
+ } catch (e) {}
48
+ }
49
+ try {
50
+ return JSON.parse(trimmed);
51
+ } catch (e) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ async function runBrowserVerb(action, args, sessionId, timeoutMs = 30000) {
57
+ const payload = args && Object.keys(args).length > 0 ? `${action}\n${JSON.stringify(args)}` : action;
58
+ const result = await spool.execSpool(payload, 'browser', { timeoutMs, sessionId });
59
+ if (!result.ok) throw new Error(result.stderr || result.stdout || `browser verb failed: ${action}`);
60
+ const parsed = parseJsonFromStdout(result.stdout);
61
+ return parsed || { ok: true };
62
+ }
63
+
64
+ async function isBrowserAvailable(sessionId = process.env.CLAUDE_SESSION_ID || 'unknown') {
65
+ try {
66
+ const result = await spool.execSpool('health', 'health', { timeoutMs: 1000, sessionId });
67
+ return !!(result && result.ok);
68
+ } catch (e) {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ async function createSession(sessionId) {
74
+ if (!sessionId) throw new Error('sessionId is required');
75
+ const result = await runBrowserVerb('start', {}, sessionId, 30000);
76
+ const state = loadSessionState(sessionId);
77
+ state.browserSessionId = result.browserSessionId || result.sessionId || sessionId;
78
+ state.status = 'active';
79
+ state.createdAt = new Date().toISOString();
80
+ saveSessionState(sessionId, state);
81
+ return { ok: true, sessionId, browserSessionId: state.browserSessionId };
82
+ }
83
+
84
+ async function sendCommand(sessionId, commandType, args) {
85
+ if (!sessionId) throw new Error('sessionId is required');
86
+ const result = await runBrowserVerb(commandType, args || {}, sessionId, 30000);
87
+ const state = loadSessionState(sessionId);
88
+ state.commands = state.commands || [];
89
+ state.commands.push({ type: commandType, args: args || {}, timestamp: new Date().toISOString() });
90
+ if (state.commands.length > 1000) state.commands = state.commands.slice(-500);
91
+ saveSessionState(sessionId, state);
92
+ return { ok: true, result: result.result || result.data || result };
93
+ }
94
+
95
+ async function executeScript(sessionId, code, options = {}) {
96
+ if (!sessionId) throw new Error('sessionId is required');
97
+ if (!code || typeof code !== 'string') throw new Error('code must be a non-empty string');
98
+ const payload = { code, ...options };
99
+ return sendCommand(sessionId, 'execute', payload);
100
+ }
101
+
102
+ async function getScreenshot(sessionId) {
103
+ if (!sessionId) throw new Error('sessionId is required');
104
+ const result = await runBrowserVerb('screenshot', {}, sessionId, 30000);
105
+ let screenshotData = result.screenshot || result.data || '';
106
+ if (screenshotData && !screenshotData.startsWith('data:image')) screenshotData = `data:image/png;base64,${screenshotData}`;
107
+ return { ok: true, screenshot: screenshotData, mimeType: 'image/png' };
108
+ }
109
+
110
+ async function closeSession(sessionId) {
111
+ if (!sessionId) throw new Error('sessionId is required');
112
+ try { await runBrowserVerb('stop', {}, sessionId, 10000); } catch (e) {}
113
+ const stateFile = path.join(SESSION_STATE_DIR, `${sessionId}.json`);
114
+ if (fs.existsSync(stateFile)) fs.unlinkSync(stateFile);
115
+ return { ok: true, sessionId };
116
+ }
117
+
118
+ async function closeAllSessions(excludeSessionId = null) {
119
+ if (!fs.existsSync(SESSION_STATE_DIR)) return { ok: true, closed: 0 };
120
+ const files = fs.readdirSync(SESSION_STATE_DIR);
121
+ let closed = 0;
122
+ for (const file of files) {
123
+ if (!file.endsWith('.json')) continue;
124
+ const sessionId = file.replace('.json', '');
125
+ if (excludeSessionId && sessionId === excludeSessionId) continue;
126
+ try { await closeSession(sessionId); closed++; } catch (e) {}
127
+ }
128
+ return { ok: true, closed };
129
+ }
130
+
131
+ module.exports = { createSession, sendCommand, executeScript, getScreenshot, closeSession, closeAllSessions, isBrowserAvailable, loadSessionState, saveSessionState };
@@ -0,0 +1,109 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const spool = require('./spool.js');
5
+
6
+ const CODEINSIGHT_HOST = '127.0.0.1';
7
+ const CODEINSIGHT_PORT = 4802;
8
+ const REQUEST_TIMEOUT_MS = 30000;
9
+
10
+ function emitEvent(severity, message, details = {}) {
11
+ try {
12
+ const date = new Date().toISOString().split('T')[0];
13
+ const logDir = path.join(os.homedir(), '.claude', 'gm-log', date);
14
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
15
+ const entry = { ts: new Date().toISOString(), severity, message, ...details };
16
+ fs.appendFileSync(path.join(logDir, 'codeinsight.jsonl'), JSON.stringify(entry) + '\n');
17
+ } catch (e) {
18
+ console.error(`[codeinsight] emit failed: ${e.message}`);
19
+ }
20
+ }
21
+
22
+ async function checkSocketReachable(host = CODEINSIGHT_HOST, port = CODEINSIGHT_PORT, timeoutMs = 1000) {
23
+ try {
24
+ const result = await spool.execSpool('health', 'health', { timeoutMs });
25
+ return !!(result && result.ok);
26
+ } catch (e) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async function sendRequest(request, sessionId = 'unknown') {
32
+ const startTime = Date.now();
33
+ const reachable = await checkSocketReachable();
34
+
35
+ if (!reachable) {
36
+ emitEvent('warn', 'Codeinsight socket unreachable', {
37
+ host: CODEINSIGHT_HOST,
38
+ port: CODEINSIGHT_PORT,
39
+ sessionId,
40
+ durationMs: Date.now() - startTime,
41
+ });
42
+ return { ok: false, error: `Codeinsight daemon unavailable at ${CODEINSIGHT_HOST}:${CODEINSIGHT_PORT}`, durationMs: Date.now() - startTime };
43
+ }
44
+
45
+ try {
46
+ if (request.action === 'search') {
47
+ const q = request.discipline && request.discipline !== 'default' ? `@${request.discipline} ${request.query}` : request.query;
48
+ const result = await spool.execCodesearch(q, { timeoutMs: REQUEST_TIMEOUT_MS, sessionId });
49
+ if (!result.ok) return { ok: false, error: result.stderr || result.stdout || 'codesearch failed', durationMs: Date.now() - startTime };
50
+ return { ok: true, raw: result.stdout || '', durationMs: Date.now() - startTime };
51
+ }
52
+ return { ok: false, error: `Unsupported action via spool: ${request.action}`, durationMs: Date.now() - startTime };
53
+ } catch (err) {
54
+ return { ok: false, error: err.message, durationMs: Date.now() - startTime };
55
+ }
56
+ }
57
+
58
+ async function searchCode(query, discipline = 'default', sessionId = 'unknown') {
59
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
60
+ return { ok: false, error: 'Query must be a non-empty string' };
61
+ }
62
+ const result = await sendRequest({ action: 'search', query: query.trim(), discipline, sessionId }, sessionId);
63
+ if (result.ok) {
64
+ emitEvent('info', 'Search completed', { query: query.trim(), discipline, resultCount: (result.results || []).length, sessionId, durationMs: result.durationMs });
65
+ }
66
+ return result;
67
+ }
68
+
69
+ async function indexProject(projectPath, discipline = 'default', sessionId = 'unknown') {
70
+ if (!projectPath || typeof projectPath !== 'string') {
71
+ return { ok: false, error: 'Project path must be a non-empty string' };
72
+ }
73
+ if (!fs.existsSync(projectPath)) {
74
+ emitEvent('warn', 'Project path does not exist', { projectPath, discipline, sessionId });
75
+ return { ok: false, error: `Project path does not exist: ${projectPath}` };
76
+ }
77
+ const result = await sendRequest({ action: 'index', projectPath: path.resolve(projectPath), discipline, sessionId }, sessionId);
78
+ if (result.ok) {
79
+ emitEvent('info', 'Index completed', { projectPath: path.resolve(projectPath), discipline, filesIndexed: result.filesIndexed, sessionId, durationMs: result.durationMs });
80
+ }
81
+ return result;
82
+ }
83
+
84
+ async function getDiagnostics(projectPath = null, discipline = 'default', sessionId = 'unknown') {
85
+ if (projectPath && !fs.existsSync(projectPath)) {
86
+ return { ok: false, error: `Project path does not exist: ${projectPath}` };
87
+ }
88
+ const result = await sendRequest({ action: 'diagnostics', projectPath: projectPath ? path.resolve(projectPath) : null, discipline, sessionId }, sessionId);
89
+ if (result.ok) {
90
+ emitEvent('info', 'Diagnostics retrieved', { projectPath: projectPath ? path.resolve(projectPath) : 'system-wide', discipline, diagnosticCount: (result.diagnostics || []).length, sessionId, durationMs: result.durationMs });
91
+ }
92
+ return result;
93
+ }
94
+
95
+ async function getIndexStatus(discipline = 'default', sessionId = 'unknown') {
96
+ const result = await sendRequest({ action: 'status', discipline, sessionId }, sessionId);
97
+ if (result.ok) {
98
+ emitEvent('info', 'Index status retrieved', { discipline, indexed: result.indexed, sessionId, durationMs: result.durationMs });
99
+ }
100
+ return result;
101
+ }
102
+
103
+ module.exports = {
104
+ searchCode,
105
+ indexProject,
106
+ getDiagnostics,
107
+ getIndexStatus,
108
+ checkSocketReachable,
109
+ };