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.
Files changed (2) hide show
  1. package/bin/cypher.js +16 -5
  2. 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 announce = () => [{ id: 'root', name: 'Root', root: true }, ...readRegistry().map(({ id, name, summary }) => ({ id, name, summary }))];
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
- ws.on('close', () => clearInterval(watch));
274
- 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 { req.destroy(); } });
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', () => { console.log('disconnected, retrying...'); if (!ws.opened) console.log('(if this keeps failing with 401, the computer was removed: pair again)'); setTimeout(open, retry); retry = Math.min(retry * 2, 15000); });
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-cypher",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "Connect your machine's Claude Code projects to the Project Cypher phone app.",
5
5
  "bin": {
6
6
  "cypher": "bin/cypher.js"