nofax 0.2.0 → 0.2.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.
@@ -1,57 +1,57 @@
1
- import { buildAgentSummary } from '../protocol.mjs';
2
- import { requestApproval, sendNotification } from '../ntfy.mjs';
3
-
4
- export async function handleGeminiNotification(input, {
5
- config,
6
- sendNotificationImpl = sendNotification
7
- } = {}) {
8
- if (!input || input.hook_event_name !== 'Notification') throw new Error('NOFAX_GEMINI_EVENT');
9
- const type = typeof input.notification_type === 'string' && input.notification_type
10
- ? input.notification_type
11
- : 'Notification';
12
- await sendNotificationImpl({
13
- config,
14
- title: `Gemini CLI: ${type}`,
15
- message: buildAgentSummary({
16
- source: 'Gemini CLI',
17
- message: typeof input.message === 'string' ? input.message : 'Gemini CLI needs attention.',
18
- toolInput: input.details
19
- })
20
- });
21
- return {};
22
- }
23
-
24
- export async function handleGeminiHook(input, {
25
- config,
26
- requestApprovalImpl = requestApproval,
27
- sendNotificationImpl = sendNotification,
28
- onError = () => {}
29
- } = {}) {
30
- if (!input || typeof input.hook_event_name !== 'string') throw new Error('NOFAX_GEMINI_EVENT');
31
- if (input.hook_event_name === 'Notification') {
32
- return handleGeminiNotification(input, { config, sendNotificationImpl });
33
- }
34
- if (input.hook_event_name !== 'BeforeTool') throw new Error('NOFAX_GEMINI_EVENT');
35
- if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_GEMINI_TOOL');
36
-
37
- try {
38
- const result = await requestApprovalImpl({
39
- config,
40
- title: `Gemini CLI needs approval: ${input.tool_name}`,
41
- message: buildAgentSummary({
42
- source: 'Gemini CLI',
43
- toolName: input.tool_name,
44
- cwd: input.cwd,
45
- toolInput: input.tool_input
46
- })
47
- });
48
- if (result.decision === 'allow') return { decision: 'allow' };
49
- if (result.decision === 'deny') {
50
- return { decision: 'deny', reason: 'Denied remotely via Nofax.' };
51
- }
52
- return {};
53
- } catch (error) {
54
- onError(error);
55
- return {};
56
- }
57
- }
1
+ import { buildAgentSummary } from '../protocol.mjs';
2
+ import { requestApproval, sendNotification } from '../ntfy.mjs';
3
+
4
+ export async function handleGeminiNotification(input, {
5
+ config,
6
+ sendNotificationImpl = sendNotification
7
+ } = {}) {
8
+ if (!input || input.hook_event_name !== 'Notification') throw new Error('NOFAX_GEMINI_EVENT');
9
+ const type = typeof input.notification_type === 'string' && input.notification_type
10
+ ? input.notification_type
11
+ : 'Notification';
12
+ await sendNotificationImpl({
13
+ config,
14
+ title: `Gemini CLI: ${type}`,
15
+ message: buildAgentSummary({
16
+ source: 'Gemini CLI',
17
+ message: typeof input.message === 'string' ? input.message : 'Gemini CLI needs attention.',
18
+ toolInput: input.details
19
+ })
20
+ });
21
+ return {};
22
+ }
23
+
24
+ export async function handleGeminiHook(input, {
25
+ config,
26
+ requestApprovalImpl = requestApproval,
27
+ sendNotificationImpl = sendNotification,
28
+ onError = () => {}
29
+ } = {}) {
30
+ if (!input || typeof input.hook_event_name !== 'string') throw new Error('NOFAX_GEMINI_EVENT');
31
+ if (input.hook_event_name === 'Notification') {
32
+ return handleGeminiNotification(input, { config, sendNotificationImpl });
33
+ }
34
+ if (input.hook_event_name !== 'BeforeTool') throw new Error('NOFAX_GEMINI_EVENT');
35
+ if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_GEMINI_TOOL');
36
+
37
+ try {
38
+ const result = await requestApprovalImpl({
39
+ config,
40
+ title: `Gemini CLI needs approval: ${input.tool_name}`,
41
+ message: buildAgentSummary({
42
+ source: 'Gemini CLI',
43
+ toolName: input.tool_name,
44
+ cwd: input.cwd,
45
+ toolInput: input.tool_input
46
+ })
47
+ });
48
+ if (result.decision === 'allow') return { decision: 'allow' };
49
+ if (result.decision === 'deny') {
50
+ return { decision: 'deny', reason: 'Denied remotely via Nofax.' };
51
+ }
52
+ return {};
53
+ } catch (error) {
54
+ onError(error);
55
+ return {};
56
+ }
57
+ }
package/src/cli.mjs CHANGED
@@ -1,191 +1,191 @@
1
- import { initConfig, loadConfig } from './config.mjs';
2
- import { requestApproval, requestRefinement, sendNotification } from './ntfy.mjs';
3
- import { handleClaudePermissionRequest } from './adapters/claude.mjs';
4
- import { handleCodexPermissionRequest } from './adapters/codex.mjs';
5
- import { handleGeminiHook } from './adapters/gemini.mjs';
6
-
7
- const HELP = `nofax - remote approvals and notifications for coding agents\n\nUsage:\n nofax init [--server URL] [--topic TOPIC] [--timeout SECONDS] [--force]\n nofax test\n nofax notify [--title TITLE] MESSAGE...\n nofax approve [--title TITLE] MESSAGE...\n nofax refine [--title TITLE] MESSAGE...\n nofax mcp\n nofax config\n nofax hook claude\n nofax hook codex\n nofax hook gemini\n\nEnvironment:\n NOFAX_HOME Override ~/.nofax\n`;
8
-
9
- function parseArgs(args) {
10
- const flags = {};
11
- const positionals = [];
12
- for (let index = 0; index < args.length; index += 1) {
13
- const value = args[index];
14
- if (value === '--force') {
15
- flags.force = true;
16
- continue;
17
- }
18
- if (['--title', '--server', '--topic', '--timeout'].includes(value)) {
19
- const next = args[index + 1];
20
- if (next === undefined) throw new Error(`NOFAX_FLAG_VALUE:${value}`);
21
- flags[value.slice(2)] = next;
22
- index += 1;
23
- continue;
24
- }
25
- if (value.startsWith('--')) throw new Error(`NOFAX_UNKNOWN_FLAG:${value}`);
26
- positionals.push(value);
27
- }
28
- return { flags, positionals };
29
- }
30
-
31
- async function readStdin(stdinText) {
32
- if (stdinText !== undefined) return stdinText;
33
- let text = '';
34
- for await (const chunk of process.stdin) text += chunk;
35
- return text;
36
- }
37
-
38
- function writeJson(stream, value) {
39
- stream.write(`${JSON.stringify(value)}\n`);
40
- }
41
-
42
- async function runHook(adapter, deps) {
43
- const inputText = await readStdin(deps.stdinText);
44
- let input;
45
- try {
46
- input = JSON.parse(inputText);
47
- } catch {
48
- throw new Error('NOFAX_HOOK_INVALID_JSON');
49
- }
50
- const config = await deps.loadConfigImpl({ env: deps.env });
51
- const onError = (error) => deps.stderr.write(`nofax: ${error.message}\n`);
52
- let output;
53
- if (adapter === 'claude') {
54
- output = await handleClaudePermissionRequest(input, { config, requestApprovalImpl: deps.requestApprovalImpl, onError });
55
- } else if (adapter === 'codex') {
56
- output = await handleCodexPermissionRequest(input, { config, requestApprovalImpl: deps.requestApprovalImpl, onError });
57
- } else if (adapter === 'gemini') {
58
- output = await handleGeminiHook(input, {
59
- config,
60
- requestApprovalImpl: deps.requestApprovalImpl,
61
- sendNotificationImpl: deps.sendNotificationImpl,
62
- onError
63
- });
64
- } else {
65
- throw new Error(`NOFAX_HOOK_UNSUPPORTED:${adapter}`);
66
- }
67
-
68
- if (output === null) {
69
- deps.stderr.write('nofax: no remote decision; continuing with native approval.\n');
70
- return 0;
71
- }
72
- writeJson(deps.stdout, output);
73
- return 0;
74
- }
75
-
76
- export async function runCli(args, overrides = {}) {
77
- const deps = {
78
- env: overrides.env ?? process.env,
79
- stdinText: overrides.stdinText,
80
- stdout: overrides.stdout ?? process.stdout,
81
- stderr: overrides.stderr ?? process.stderr,
82
- initConfigImpl: overrides.initConfigImpl ?? initConfig,
83
- loadConfigImpl: overrides.loadConfigImpl ?? loadConfig,
84
- requestApprovalImpl: overrides.requestApprovalImpl ?? requestApproval,
85
- requestRefinementImpl: overrides.requestRefinementImpl ?? requestRefinement,
86
- sendNotificationImpl: overrides.sendNotificationImpl ?? sendNotification,
87
- runMcpServerImpl: overrides.runMcpServerImpl ?? (async () => {
88
- const { runMcpServer } = await import('./mcp-server.mjs');
89
- await runMcpServer();
90
- })
91
- };
92
-
93
- try {
94
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
95
- deps.stdout.write(HELP);
96
- return 0;
97
- }
98
-
99
- const command = args[0];
100
- if (command === 'init') {
101
- const { flags } = parseArgs(args.slice(1));
102
- const timeoutSeconds = flags.timeout === undefined ? undefined : Number(flags.timeout);
103
- const config = await deps.initConfigImpl({
104
- env: deps.env,
105
- ...(flags.server === undefined ? {} : { server: flags.server }),
106
- ...(flags.topic === undefined ? {} : { topic: flags.topic }),
107
- ...(timeoutSeconds === undefined ? {} : { timeoutSeconds }),
108
- force: flags.force === true
109
- });
110
- deps.stdout.write('Nofax ready.\n');
111
- deps.stdout.write(`Subscribe your ntfy app to: ${config.server}/${config.topic}\n`);
112
- deps.stdout.write('Then run: nofax test\n');
113
- return 0;
114
- }
115
-
116
- if (command === 'config') {
117
- const config = await deps.loadConfigImpl({ env: deps.env });
118
- deps.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
119
- return 0;
120
- }
121
-
122
- if (command === 'test') {
123
- const config = await deps.loadConfigImpl({ env: deps.env });
124
- await deps.sendNotificationImpl({
125
- config,
126
- title: 'Nofax is connected',
127
- message: 'If you can read this on your phone, Nofax is ready.'
128
- });
129
- deps.stdout.write('Test notification sent.\n');
130
- return 0;
131
- }
132
-
133
- if (command === 'notify') {
134
- const { flags, positionals } = parseArgs(args.slice(1));
135
- if (positionals.length === 0) throw new Error('NOFAX_MESSAGE_REQUIRED');
136
- const config = await deps.loadConfigImpl({ env: deps.env });
137
- await deps.sendNotificationImpl({
138
- config,
139
- title: flags.title ?? 'Nofax',
140
- message: positionals.join(' ')
141
- });
142
- deps.stdout.write('Notification sent.\n');
143
- return 0;
144
- }
145
-
146
- if (command === 'approve') {
147
- const { flags, positionals } = parseArgs(args.slice(1));
148
- if (positionals.length === 0) throw new Error('NOFAX_MESSAGE_REQUIRED');
149
- const config = await deps.loadConfigImpl({ env: deps.env });
150
- const result = await deps.requestApprovalImpl({
151
- config,
152
- title: flags.title ?? 'Nofax approval',
153
- message: positionals.join(' ')
154
- });
155
- writeJson(deps.stdout, { decision: result.decision });
156
- return result.decision === 'timeout' ? 3 : 0;
157
- }
158
-
159
- if (command === 'refine') {
160
- const { flags, positionals } = parseArgs(args.slice(1));
161
- if (positionals.length === 0) throw new Error('NOFAX_MESSAGE_REQUIRED');
162
- const config = await deps.loadConfigImpl({ env: deps.env });
163
- const result = await deps.requestRefinementImpl({
164
- config,
165
- title: flags.title ?? 'Nofax refinement',
166
- message: positionals.join(' ')
167
- });
168
- writeJson(deps.stdout, {
169
- decision: result.decision,
170
- ...(result.text === undefined ? {} : { text: result.text })
171
- });
172
- return result.decision === 'timeout' ? 3 : 0;
173
- }
174
-
175
- if (command === 'mcp') {
176
- await deps.runMcpServerImpl();
177
- return 0;
178
- }
179
-
180
- if (command === 'hook') {
181
- const adapter = args[1];
182
- if (!adapter) throw new Error('NOFAX_HOOK_REQUIRED');
183
- return runHook(adapter, deps);
184
- }
185
-
186
- throw new Error(`NOFAX_UNKNOWN_COMMAND:${command}`);
187
- } catch (error) {
188
- deps.stderr.write(`nofax: ${error.message}\n`);
189
- return 1;
190
- }
191
- }
1
+ import { initConfig, loadConfig } from './config.mjs';
2
+ import { requestApproval, requestRefinement, sendNotification } from './ntfy.mjs';
3
+ import { handleClaudePermissionRequest } from './adapters/claude.mjs';
4
+ import { handleCodexPermissionRequest } from './adapters/codex.mjs';
5
+ import { handleGeminiHook } from './adapters/gemini.mjs';
6
+
7
+ const HELP = `nofax - remote approvals and notifications for coding agents\n\nUsage:\n nofax init [--server URL] [--topic TOPIC] [--timeout SECONDS] [--force]\n nofax test\n nofax notify [--title TITLE] MESSAGE...\n nofax approve [--title TITLE] MESSAGE...\n nofax refine [--title TITLE] MESSAGE...\n nofax mcp\n nofax config\n nofax hook claude\n nofax hook codex\n nofax hook gemini\n\nEnvironment:\n NOFAX_HOME Override ~/.nofax\n`;
8
+
9
+ function parseArgs(args) {
10
+ const flags = {};
11
+ const positionals = [];
12
+ for (let index = 0; index < args.length; index += 1) {
13
+ const value = args[index];
14
+ if (value === '--force') {
15
+ flags.force = true;
16
+ continue;
17
+ }
18
+ if (['--title', '--server', '--topic', '--timeout'].includes(value)) {
19
+ const next = args[index + 1];
20
+ if (next === undefined) throw new Error(`NOFAX_FLAG_VALUE:${value}`);
21
+ flags[value.slice(2)] = next;
22
+ index += 1;
23
+ continue;
24
+ }
25
+ if (value.startsWith('--')) throw new Error(`NOFAX_UNKNOWN_FLAG:${value}`);
26
+ positionals.push(value);
27
+ }
28
+ return { flags, positionals };
29
+ }
30
+
31
+ async function readStdin(stdinText) {
32
+ if (stdinText !== undefined) return stdinText;
33
+ let text = '';
34
+ for await (const chunk of process.stdin) text += chunk;
35
+ return text;
36
+ }
37
+
38
+ function writeJson(stream, value) {
39
+ stream.write(`${JSON.stringify(value)}\n`);
40
+ }
41
+
42
+ async function runHook(adapter, deps) {
43
+ const inputText = await readStdin(deps.stdinText);
44
+ let input;
45
+ try {
46
+ input = JSON.parse(inputText);
47
+ } catch {
48
+ throw new Error('NOFAX_HOOK_INVALID_JSON');
49
+ }
50
+ const config = await deps.loadConfigImpl({ env: deps.env });
51
+ const onError = (error) => deps.stderr.write(`nofax: ${error.message}\n`);
52
+ let output;
53
+ if (adapter === 'claude') {
54
+ output = await handleClaudePermissionRequest(input, { config, requestApprovalImpl: deps.requestApprovalImpl, onError });
55
+ } else if (adapter === 'codex') {
56
+ output = await handleCodexPermissionRequest(input, { config, requestApprovalImpl: deps.requestApprovalImpl, onError });
57
+ } else if (adapter === 'gemini') {
58
+ output = await handleGeminiHook(input, {
59
+ config,
60
+ requestApprovalImpl: deps.requestApprovalImpl,
61
+ sendNotificationImpl: deps.sendNotificationImpl,
62
+ onError
63
+ });
64
+ } else {
65
+ throw new Error(`NOFAX_HOOK_UNSUPPORTED:${adapter}`);
66
+ }
67
+
68
+ if (output === null) {
69
+ deps.stderr.write('nofax: no remote decision; continuing with native approval.\n');
70
+ return 0;
71
+ }
72
+ writeJson(deps.stdout, output);
73
+ return 0;
74
+ }
75
+
76
+ export async function runCli(args, overrides = {}) {
77
+ const deps = {
78
+ env: overrides.env ?? process.env,
79
+ stdinText: overrides.stdinText,
80
+ stdout: overrides.stdout ?? process.stdout,
81
+ stderr: overrides.stderr ?? process.stderr,
82
+ initConfigImpl: overrides.initConfigImpl ?? initConfig,
83
+ loadConfigImpl: overrides.loadConfigImpl ?? loadConfig,
84
+ requestApprovalImpl: overrides.requestApprovalImpl ?? requestApproval,
85
+ requestRefinementImpl: overrides.requestRefinementImpl ?? requestRefinement,
86
+ sendNotificationImpl: overrides.sendNotificationImpl ?? sendNotification,
87
+ runMcpServerImpl: overrides.runMcpServerImpl ?? (async () => {
88
+ const { runMcpServer } = await import('./mcp-server.mjs');
89
+ await runMcpServer();
90
+ })
91
+ };
92
+
93
+ try {
94
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
95
+ deps.stdout.write(HELP);
96
+ return 0;
97
+ }
98
+
99
+ const command = args[0];
100
+ if (command === 'init') {
101
+ const { flags } = parseArgs(args.slice(1));
102
+ const timeoutSeconds = flags.timeout === undefined ? undefined : Number(flags.timeout);
103
+ const config = await deps.initConfigImpl({
104
+ env: deps.env,
105
+ ...(flags.server === undefined ? {} : { server: flags.server }),
106
+ ...(flags.topic === undefined ? {} : { topic: flags.topic }),
107
+ ...(timeoutSeconds === undefined ? {} : { timeoutSeconds }),
108
+ force: flags.force === true
109
+ });
110
+ deps.stdout.write('Nofax ready.\n');
111
+ deps.stdout.write(`Subscribe your ntfy app to: ${config.server}/${config.topic}\n`);
112
+ deps.stdout.write('Then run: nofax test\n');
113
+ return 0;
114
+ }
115
+
116
+ if (command === 'config') {
117
+ const config = await deps.loadConfigImpl({ env: deps.env });
118
+ deps.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
119
+ return 0;
120
+ }
121
+
122
+ if (command === 'test') {
123
+ const config = await deps.loadConfigImpl({ env: deps.env });
124
+ await deps.sendNotificationImpl({
125
+ config,
126
+ title: 'Nofax is connected',
127
+ message: 'If you can read this on your phone, Nofax is ready.'
128
+ });
129
+ deps.stdout.write('Test notification sent.\n');
130
+ return 0;
131
+ }
132
+
133
+ if (command === 'notify') {
134
+ const { flags, positionals } = parseArgs(args.slice(1));
135
+ if (positionals.length === 0) throw new Error('NOFAX_MESSAGE_REQUIRED');
136
+ const config = await deps.loadConfigImpl({ env: deps.env });
137
+ await deps.sendNotificationImpl({
138
+ config,
139
+ title: flags.title ?? 'Nofax',
140
+ message: positionals.join(' ')
141
+ });
142
+ deps.stdout.write('Notification sent.\n');
143
+ return 0;
144
+ }
145
+
146
+ if (command === 'approve') {
147
+ const { flags, positionals } = parseArgs(args.slice(1));
148
+ if (positionals.length === 0) throw new Error('NOFAX_MESSAGE_REQUIRED');
149
+ const config = await deps.loadConfigImpl({ env: deps.env });
150
+ const result = await deps.requestApprovalImpl({
151
+ config,
152
+ title: flags.title ?? 'Nofax approval',
153
+ message: positionals.join(' ')
154
+ });
155
+ writeJson(deps.stdout, { decision: result.decision });
156
+ return result.decision === 'timeout' ? 3 : 0;
157
+ }
158
+
159
+ if (command === 'refine') {
160
+ const { flags, positionals } = parseArgs(args.slice(1));
161
+ if (positionals.length === 0) throw new Error('NOFAX_MESSAGE_REQUIRED');
162
+ const config = await deps.loadConfigImpl({ env: deps.env });
163
+ const result = await deps.requestRefinementImpl({
164
+ config,
165
+ title: flags.title ?? 'Nofax refinement',
166
+ message: positionals.join(' ')
167
+ });
168
+ writeJson(deps.stdout, {
169
+ decision: result.decision,
170
+ ...(result.text === undefined ? {} : { text: result.text })
171
+ });
172
+ return result.decision === 'timeout' ? 3 : 0;
173
+ }
174
+
175
+ if (command === 'mcp') {
176
+ await deps.runMcpServerImpl();
177
+ return 0;
178
+ }
179
+
180
+ if (command === 'hook') {
181
+ const adapter = args[1];
182
+ if (!adapter) throw new Error('NOFAX_HOOK_REQUIRED');
183
+ return runHook(adapter, deps);
184
+ }
185
+
186
+ throw new Error(`NOFAX_UNKNOWN_COMMAND:${command}`);
187
+ } catch (error) {
188
+ deps.stderr.write(`nofax: ${error.message}\n`);
189
+ return 1;
190
+ }
191
+ }