project-cypher 0.16.0 → 0.16.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 +12 -5
  2. package/package.json +1 -1
package/bin/cypher.js CHANGED
@@ -231,7 +231,7 @@ 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
  },
@@ -241,15 +241,22 @@ const cmds = {
241
241
  ensureRoot();
242
242
  const runs = new Map(); let retry = 1000; let tx = () => false;
243
243
  // 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) => String(i.command || i.file_path || i.path || i.url || i.pattern || JSON.stringify(i)).slice(0, 400);
246
- const settle = (id, allow, message) => { const p = pending.get(id); if (!p) return; pending.delete(id); clearTimeout(p.timer); p.res.end(JSON.stringify({ allow, message })); if (!allow && message) tx({ type: 'approval-closed', id, reqId: p.msg.reqId, projectId: p.msg.projectId, reason: message }); };
244
+ const pendingInput = new Map(), pending = new Map(), AKEY = require('crypto').randomBytes(24).toString('hex'); let AURL = '';
245
+ const summ = (t, i) => {
246
+ if (i.command) return String(i.command).slice(0, 600);
247
+ const head = i.file_path || i.path || i.url || i.pattern || '';
248
+ 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)}`);
249
+ return [head, ...rest].filter(Boolean).join('\n').slice(0, 600) || (i.description || '');
250
+ };
251
+ 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
252
  const asrv = require('http').createServer((req, res) => {
248
253
  if (req.method !== 'POST' || req.headers['x-cypher-key'] !== AKEY) { res.statusCode = 403; return res.end('{}'); }
249
254
  let b = ''; req.on('data', d => (b += d)); req.on('end', () => {
250
255
  let a; try { a = JSON.parse(b); } catch { res.statusCode = 400; return res.end('{}'); }
251
256
  const id = require('crypto').randomBytes(6).toString('hex');
252
257
  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
+ 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
+ pendingInput.set(id, a.input || {});
253
260
  pending.set(id, { res, msg, timer: setTimeout(() => settle(id, false, 'No answer from the phone within 10 minutes.'), 600000) });
254
261
  console.log(`[${a.projectId}] approval asked: ${msg.tool} ${msg.summary.slice(0, 60)}`);
255
262
  if (!tx(msg)) console.log('(phone not reachable yet; will resend when connected)');
@@ -269,7 +276,7 @@ const cmds = {
269
276
  let m; try { m = JSON.parse(raw); } catch { return; }
270
277
  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
278
  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.');
279
+ 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
280
  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
281
  if (m.type === 'rmproject') { if (m.projectId !== 'root') writeRegistry(readRegistry().filter(x => x.id !== m.projectId)); return; }
275
282
  if (m.type === 'reset') { const k = load(); if (k.sessions) { delete k.sessions[m.projectId]; save(k); } return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-cypher",
3
- "version": "0.16.0",
3
+ "version": "0.16.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"