offhands 0.1.5 → 0.1.7

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/approval-mcp.mjs CHANGED
@@ -1,15 +1,20 @@
1
- // offhand approval MCP server (plain JS, zero deps — spawned BY claude).
2
- // Newline-delimited JSON-RPC 2.0 over stdio. Exposes one tool,
3
- // `approval_prompt`, named via --permission-prompt-tool
4
- // mcp__offhand__approval_prompt. Each call is forwarded to the daemon's
5
- // local HTTP endpoint (OFFHAND_APPROVAL_URL) which long-polls the phone's
6
- // verdict, then this returns allow/deny to claude.
1
+ // offhand MCP toolkit (plain JS, zero deps — spawned BY the agent CLI).
2
+ // Newline-delimited JSON-RPC 2.0 over stdio. Started life as a single tool
3
+ // (`approval_prompt`, still the one named via --permission-prompt-tool
4
+ // mcp__offhand__approval_prompt) Execution Plan 5 adds `offhand_status_update`
5
+ // alongside it, since a CLI wired in via --mcp-config sees every tool this
6
+ // server exposes, not just the one used as the permission hook. Each call
7
+ // is forwarded to a daemon-local HTTP endpoint (localhost-only by
8
+ // construction); the daemon does the real work, this file is just the wire
9
+ // adapter between JSON-RPC-over-stdio and plain HTTP.
7
10
 
8
11
  import { createInterface } from 'node:readline';
9
12
 
10
13
  const APPROVAL_URL = process.env.OFFHAND_APPROVAL_URL ?? 'http://127.0.0.1:4317/approval';
14
+ const STATUS_URL = process.env.OFFHAND_STATUS_URL ?? 'http://127.0.0.1:4317/status';
15
+ const SESSION_ID = process.env.OFFHAND_SESSION_ID ?? '';
11
16
 
12
- const TOOL = {
17
+ const APPROVAL_TOOL = {
13
18
  name: 'approval_prompt',
14
19
  description: 'Forwards a permission request to the offhand phone client and waits for the verdict.',
15
20
  inputSchema: {
@@ -23,6 +28,21 @@ const TOOL = {
23
28
  },
24
29
  };
25
30
 
31
+ const STATUS_TOOL = {
32
+ name: 'offhand_status_update',
33
+ description:
34
+ "Sends a short plain-English progress note to the person's phone, outside the normal response — use it at meaningful checkpoints (starting a long step, hitting a snag, finishing a phase) so they see progress without waiting for the whole run to finish.",
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ text: { type: 'string', description: 'A short, human-readable status note.' },
39
+ },
40
+ required: ['text'],
41
+ },
42
+ };
43
+
44
+ const TOOLS = [APPROVAL_TOOL, STATUS_TOOL];
45
+
26
46
  function send(msg) {
27
47
  process.stdout.write(JSON.stringify(msg) + '\n');
28
48
  }
@@ -45,6 +65,14 @@ async function callDaemon(args) {
45
65
  return res.json();
46
66
  }
47
67
 
68
+ async function sendStatus(text) {
69
+ await fetch(STATUS_URL, {
70
+ method: 'POST',
71
+ headers: { 'content-type': 'application/json' },
72
+ body: JSON.stringify({ sessionId: SESSION_ID, text }),
73
+ });
74
+ }
75
+
48
76
  const rl = createInterface({ input: process.stdin });
49
77
  rl.on('line', (line) => {
50
78
  if (!line.trim()) return;
@@ -66,14 +94,26 @@ async function handle(msg) {
66
94
  reply(id, {
67
95
  protocolVersion: params?.protocolVersion ?? '2024-11-05',
68
96
  capabilities: { tools: {} },
69
- serverInfo: { name: 'offhand-approvals', version: '0.0.1' },
97
+ serverInfo: { name: 'offhand', version: '0.1.0' },
70
98
  });
71
99
  return;
72
100
  case 'tools/list':
73
- reply(id, { tools: [TOOL] });
101
+ reply(id, { tools: TOOLS });
74
102
  return;
75
103
  case 'tools/call': {
76
- if (params?.name !== TOOL.name) {
104
+ if (params?.name === STATUS_TOOL.name) {
105
+ const text = String(params.arguments?.text ?? '').trim();
106
+ try {
107
+ if (text) await sendStatus(text);
108
+ reply(id, { content: [{ type: 'text', text: text ? 'sent' : 'nothing to send (empty text)' }] });
109
+ } catch (e) {
110
+ // A dropped status note is never fatal to the run — it's a nice-
111
+ // to-have channel, not a control one like approvals.
112
+ reply(id, { content: [{ type: 'text', text: `status channel error: ${e?.message ?? e}` }] });
113
+ }
114
+ return;
115
+ }
116
+ if (params?.name !== APPROVAL_TOOL.name) {
77
117
  replyError(id, -32602, `unknown tool: ${params?.name}`);
78
118
  return;
79
119
  }