project-cypher 0.16.0 → 0.17.1
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/bin/cypher.js +28 -10
- package/package.json +1 -1
package/bin/cypher.js
CHANGED
|
@@ -231,25 +231,39 @@ const cmds = {
|
|
|
231
231
|
process.stdin.on('data', d => { buf += d; let i; while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); if (!l.trim()) continue; let m; try { m = JSON.parse(l); } catch { continue; }
|
|
232
232
|
if (m.method === 'initialize') out({ jsonrpc: '2.0', id: m.id, result: { protocolVersion: m.params?.protocolVersion || '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'cypher', version: '1' } } });
|
|
233
233
|
else if (m.method === 'tools/list') out({ jsonrpc: '2.0', id: m.id, result: { tools: [{ name: 'approve', description: 'Ask the user on their phone to approve a tool use', inputSchema: { type: 'object', properties: { tool_name: { type: 'string' }, input: { type: 'object' }, tool_use_id: { type: 'string' } }, required: ['tool_name', 'input'] } }] } });
|
|
234
|
-
else if (m.method === 'tools/call') { const a = m.params?.arguments || {}; ask(a).then(r => out({ jsonrpc: '2.0', id: m.id, result: { content: [{ type: 'text', text: JSON.stringify(r.allow ? { behavior: 'allow', updatedInput: a.input || {} } : { behavior: 'deny', message: r.message || 'The user denied this on their phone.' }) }] } })); }
|
|
234
|
+
else if (m.method === 'tools/call') { const a = m.params?.arguments || {}; ask(a).then(r => out({ jsonrpc: '2.0', id: m.id, result: { content: [{ type: 'text', text: JSON.stringify(r.allow ? { behavior: 'allow', updatedInput: r.updatedInput || a.input || {} } : { behavior: 'deny', message: r.message || 'The user denied this on their phone.' }) }] } })); }
|
|
235
235
|
else if (m.id !== undefined) out({ jsonrpc: '2.0', id: m.id, result: {} });
|
|
236
236
|
} });
|
|
237
237
|
},
|
|
238
238
|
connect() {
|
|
239
|
-
const
|
|
239
|
+
const autoOn = id => !!rawCfg().autoApprove?.[id];
|
|
240
|
+
const announce = () => [{ id: 'root', name: 'Root', root: true, auto: autoOn('root') }, ...readRegistry().map(({ id, name, summary }) => ({ id, name, summary, auto: autoOn(id) }))];
|
|
240
241
|
const WebSocket = require('ws'); let c = load(); if (!c.token) return console.log('not linked yet. Run: cypher link');
|
|
241
242
|
ensureRoot();
|
|
242
243
|
const runs = new Map(); let retry = 1000; let tx = () => false;
|
|
243
244
|
// Approvals: claude -> mcp-approve -> this local server -> phone (WS) -> back. Unanswered requests are denied after 10 minutes.
|
|
244
|
-
const pending = new Map(), AKEY = require('crypto').randomBytes(24).toString('hex'); let AURL = '';
|
|
245
|
-
const summ = (t, i) =>
|
|
246
|
-
|
|
245
|
+
const pendingInput = new Map(), pending = new Map(), AKEY = require('crypto').randomBytes(24).toString('hex'); let AURL = '';
|
|
246
|
+
const summ = (t, i) => {
|
|
247
|
+
if (i.command) return String(i.command).slice(0, 600);
|
|
248
|
+
const head = i.file_path || i.path || i.url || i.pattern || '';
|
|
249
|
+
const rest = Object.entries(i).filter(([k, v]) => !['file_path', 'path', 'url', 'pattern', 'description'].includes(k) && v != null && typeof v !== 'object').map(([k, v]) => `${k}: ${String(v).slice(0, 160)}`);
|
|
250
|
+
return [head, ...rest].filter(Boolean).join('\n').slice(0, 600) || (i.description || '');
|
|
251
|
+
};
|
|
252
|
+
const settle = (id, allow, message, answers) => { const p = pending.get(id); if (!p) return; pending.delete(id); clearTimeout(p.timer); const inp = pendingInput.get(id) || {}; pendingInput.delete(id); p.res.end(JSON.stringify({ allow, message, updatedInput: answers && typeof answers === 'object' ? { ...inp, answers } : undefined })); if (!allow && message) tx({ type: 'approval-closed', id, reqId: p.msg.reqId, projectId: p.msg.projectId, reason: message }); };
|
|
247
253
|
const asrv = require('http').createServer((req, res) => {
|
|
248
254
|
if (req.method !== 'POST' || req.headers['x-cypher-key'] !== AKEY) { res.statusCode = 403; return res.end('{}'); }
|
|
249
255
|
let b = ''; req.on('data', d => (b += d)); req.on('end', () => {
|
|
250
256
|
let a; try { a = JSON.parse(b); } catch { res.statusCode = 400; return res.end('{}'); }
|
|
251
257
|
const id = require('crypto').randomBytes(6).toString('hex');
|
|
252
258
|
const msg = { type: 'approval', id, reqId: a.reqId, projectId: a.projectId, tool: String(a.tool || '').slice(0, 80), summary: summ(a.tool, a.input || {}) };
|
|
259
|
+
if (a.tool === 'AskUserQuestion' && Array.isArray(a.input?.questions)) msg.questions = a.input.questions.slice(0, 4).map(q => ({ question: String(q.question || '').slice(0, 500), header: String(q.header || '').slice(0, 60), multiSelect: !!q.multiSelect, options: (q.options || []).slice(0, 6).map(o => ({ label: String(o.label || '').slice(0, 120), description: String(o.description || '').slice(0, 240) })) }));
|
|
260
|
+
pendingInput.set(id, a.input || {});
|
|
261
|
+
if (autoOn(a.projectId)) { // auto-approve mode (e.g. overnight jobs): never ask
|
|
262
|
+
pendingInput.delete(id);
|
|
263
|
+
console.log(`[${a.projectId}] AUTO-APPROVED: ${msg.tool} ${msg.summary.slice(0, 80)}`);
|
|
264
|
+
if (msg.questions) return res.end(JSON.stringify({ allow: false, message: 'The user is away (auto-approve mode is on), so nobody can answer questions. Make the most sensible choice yourself and continue.' }));
|
|
265
|
+
return res.end(JSON.stringify({ allow: true }));
|
|
266
|
+
}
|
|
253
267
|
pending.set(id, { res, msg, timer: setTimeout(() => settle(id, false, 'No answer from the phone within 10 minutes.'), 600000) });
|
|
254
268
|
console.log(`[${a.projectId}] approval asked: ${msg.tool} ${msg.summary.slice(0, 60)}`);
|
|
255
269
|
if (!tx(msg)) console.log('(phone not reachable yet; will resend when connected)');
|
|
@@ -263,15 +277,19 @@ const cmds = {
|
|
|
263
277
|
// pick up `cypher add` / `remove` while running: re-announce projects when the config changes
|
|
264
278
|
let sig = JSON.stringify(announce());
|
|
265
279
|
const watch = setInterval(() => { const a = announce(), n = JSON.stringify(a); if (n !== sig) { sig = n; tx({ type: 'hello', name: devName(), projects: a }); console.log(`projects updated: ${a.length - 1}`); } }, 2000);
|
|
266
|
-
|
|
267
|
-
ws.on('
|
|
280
|
+
// One reconnect per socket, whatever ended it (close, 502 from the proxy, dead connection).
|
|
281
|
+
let lastPong = Date.now(); ws.on('pong', () => (lastPong = Date.now())); ws.on('ping', () => (lastPong = Date.now())); ws.on('message', () => (lastPong = Date.now()));
|
|
282
|
+
const hb = setInterval(() => { if (ws.readyState !== 1) return; if (Date.now() - lastPong > 70000) { console.log('no answer from the server for 70s, reconnecting'); ws.terminate(); return again(); } try { ws.ping(); } catch {} }, 25000);
|
|
283
|
+
const again = why => { if (ws._re) return; ws._re = true; clearInterval(watch); clearInterval(hb); if (why) console.log(why); setTimeout(open, retry); retry = Math.min(retry * 2, 15000); };
|
|
284
|
+
ws.on('unexpected-response', (req, res) => { if (res.statusCode === 401) { dropToken('This computer is not linked (removed from the app). Run cypher init to link again.'); process.exit(0); } else { console.log(`server answered ${res.statusCode}, retrying...`); again(); req.destroy(); } });
|
|
268
285
|
ws.on('message', raw => {
|
|
269
286
|
let m; try { m = JSON.parse(raw); } catch { return; }
|
|
270
287
|
if (m.type === 'revoked') { runs.forEach(r => r.kill('SIGINT')); dropToken('Disconnected from the app. Run cypher init to link again.'); process.exit(0); }
|
|
271
288
|
const proj = m.projectId === 'root' ? { id: 'root', path: ROOT_DIR, root: true } : readRegistry().find(p => p.id === m.projectId); const ctx = { reqId: m.reqId, projectId: m.projectId };
|
|
272
|
-
if (m.type === 'approve') return settle(m.id, !!m.allow, m.allow ? '' : 'The user denied this on their phone.');
|
|
289
|
+
if (m.type === 'approve') return settle(m.id, !!m.allow, m.allow ? '' : (m.answers ? '' : 'The user denied this on their phone.'), m.allow && m.answers && typeof m.answers === 'object' ? Object.fromEntries(Object.entries(m.answers).slice(0, 8).map(([k, v]) => [String(k).slice(0, 500), String(v).slice(0, 1000)])) : undefined);
|
|
273
290
|
if (m.type === 'stop') { pending.forEach((p, id) => p.msg.projectId === m.projectId && settle(id, false, 'Stopped from the phone.')); const r = runs.get(m.projectId); if (r) { r.stopped = true; r.kill('SIGINT'); } return; }
|
|
274
291
|
if (m.type === 'rmproject') { if (m.projectId !== 'root') writeRegistry(readRegistry().filter(x => x.id !== m.projectId)); return; }
|
|
292
|
+
if (m.type === 'autoapprove') { const k = rawCfg(); k.autoApprove = { ...(k.autoApprove || {}) }; if (m.on) k.autoApprove[m.projectId] = true; else delete k.autoApprove[m.projectId]; save(k); console.log(`[${m.projectId}] auto-approve ${m.on ? 'ON' : 'off'}`); return tx({ type: 'hello', name: devName(), projects: announce() }); }
|
|
275
293
|
if (m.type === 'reset') { const k = load(); if (k.sessions) { delete k.sessions[m.projectId]; save(k); } return; }
|
|
276
294
|
if (m.type !== 'prompt') return;
|
|
277
295
|
if (!proj) return tx({ ...ctx, type: 'error', message: 'unknown project' });
|
|
@@ -315,8 +333,8 @@ const cmds = {
|
|
|
315
333
|
ch.on('close', code => { runs.delete(proj.id); pending.forEach((p, id) => p.msg.reqId === m.reqId && settle(id, false, 'Run ended.')); if (ch.finished) return;
|
|
316
334
|
if (ch.stopped) tx({ ...ctx, type: 'done', stopped: true }); else tx({ ...ctx, type: 'error', message: (err.trim().split('\n').pop() || 'claude exited ' + code).slice(0, 200) }); });
|
|
317
335
|
});
|
|
318
|
-
ws.on('close', () =>
|
|
319
|
-
ws.on('error', e => console.log('connection error: ' + e.message));
|
|
336
|
+
ws.on('close', () => again(ws._re ? '' : 'disconnected, retrying...'));
|
|
337
|
+
ws.on('error', e => { console.log('connection error: ' + e.message); if (ws.readyState !== 1) again(); });
|
|
320
338
|
};
|
|
321
339
|
open();
|
|
322
340
|
},
|