project-cypher 0.16.1 → 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 +16 -5
- package/package.json +1 -1
package/bin/cypher.js
CHANGED
|
@@ -236,7 +236,8 @@ const cmds = {
|
|
|
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;
|
|
@@ -257,6 +258,12 @@ const cmds = {
|
|
|
257
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 || {}) };
|
|
258
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) })) }));
|
|
259
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
|
+
}
|
|
260
267
|
pending.set(id, { res, msg, timer: setTimeout(() => settle(id, false, 'No answer from the phone within 10 minutes.'), 600000) });
|
|
261
268
|
console.log(`[${a.projectId}] approval asked: ${msg.tool} ${msg.summary.slice(0, 60)}`);
|
|
262
269
|
if (!tx(msg)) console.log('(phone not reachable yet; will resend when connected)');
|
|
@@ -270,8 +277,11 @@ const cmds = {
|
|
|
270
277
|
// pick up `cypher add` / `remove` while running: re-announce projects when the config changes
|
|
271
278
|
let sig = JSON.stringify(announce());
|
|
272
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);
|
|
273
|
-
|
|
274
|
-
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(); } });
|
|
275
285
|
ws.on('message', raw => {
|
|
276
286
|
let m; try { m = JSON.parse(raw); } catch { return; }
|
|
277
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); }
|
|
@@ -279,6 +289,7 @@ const cmds = {
|
|
|
279
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);
|
|
280
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; }
|
|
281
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() }); }
|
|
282
293
|
if (m.type === 'reset') { const k = load(); if (k.sessions) { delete k.sessions[m.projectId]; save(k); } return; }
|
|
283
294
|
if (m.type !== 'prompt') return;
|
|
284
295
|
if (!proj) return tx({ ...ctx, type: 'error', message: 'unknown project' });
|
|
@@ -322,8 +333,8 @@ const cmds = {
|
|
|
322
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;
|
|
323
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) }); });
|
|
324
335
|
});
|
|
325
|
-
ws.on('close', () =>
|
|
326
|
-
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(); });
|
|
327
338
|
};
|
|
328
339
|
open();
|
|
329
340
|
},
|