nofax 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nofax",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "mcpName": "io.github.AKzar1el/nofax",
5
5
  "description": "Human-in-the-loop approvals and notifications for AI coding agents via CLI, MCP, and agent hooks.",
6
6
  "type": "module",
package/src/cli.mjs CHANGED
@@ -1,10 +1,21 @@
1
+ import { readFile } from 'node:fs/promises';
1
2
  import { initConfig, loadConfig } from './config.mjs';
2
3
  import { requestApproval, requestRefinement, sendNotification } from './ntfy.mjs';
3
4
  import { handleClaudePermissionRequest } from './adapters/claude.mjs';
4
5
  import { handleCodexPermissionRequest } from './adapters/codex.mjs';
5
6
  import { handleGeminiHook } from './adapters/gemini.mjs';
6
7
 
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
+ const HELP = `nofax - remote approvals and notifications for coding agents\n\nUsage:\n nofax --version\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`;
9
+
10
+ const PACKAGE_JSON_URL = new URL('../package.json', import.meta.url);
11
+
12
+ async function readPackageVersion() {
13
+ const metadata = JSON.parse(await readFile(PACKAGE_JSON_URL, 'utf8'));
14
+ if (typeof metadata.version !== 'string' || metadata.version.length === 0) {
15
+ throw new Error('NOFAX_PACKAGE_VERSION_INVALID');
16
+ }
17
+ return metadata.version;
18
+ }
8
19
 
9
20
  function parseArgs(args) {
10
21
  const flags = {};
@@ -96,6 +107,11 @@ export async function runCli(args, overrides = {}) {
96
107
  return 0;
97
108
  }
98
109
 
110
+ if (args[0] === '--version' || args[0] === '-v') {
111
+ deps.stdout.write(`${await readPackageVersion()}\n`);
112
+ return 0;
113
+ }
114
+
99
115
  const command = args[0];
100
116
  if (command === 'init') {
101
117
  const { flags } = parseArgs(args.slice(1));
@@ -32,7 +32,7 @@ function result(value) {
32
32
 
33
33
  export function buildMcpServer({ handlers = createMcpToolHandlers() } = {}) {
34
34
  const server = new McpServer(
35
- { name: 'nofax', version: '0.2.1' },
35
+ { name: 'nofax', version: '0.2.3' },
36
36
  { instructions: SERVER_INSTRUCTIONS }
37
37
  );
38
38
 
package/src/requests.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { chmod, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
1
+ import { chmod, link, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { resolveNofaxHome } from './config.mjs';
4
4
 
@@ -47,7 +47,12 @@ function validateRequest(input) {
47
47
  function paths({ home, env, requestId }) {
48
48
  const root = resolveNofaxHome({ home, env });
49
49
  const requestsRoot = join(root, 'requests');
50
- return { requestsRoot, file: join(requestsRoot, `${validateRequestId(requestId)}.json`) };
50
+ const normalizedRequestId = validateRequestId(requestId);
51
+ return {
52
+ requestsRoot,
53
+ file: join(requestsRoot, `${normalizedRequestId}.json`),
54
+ terminal: join(requestsRoot, `${normalizedRequestId}.terminal.json`)
55
+ };
51
56
  }
52
57
 
53
58
  async function atomicWrite(path, value) {
@@ -58,6 +63,35 @@ async function atomicWrite(path, value) {
58
63
  try { await chmod(path, 0o600); } catch {}
59
64
  }
60
65
 
66
+ async function loadTerminalClaim(path) {
67
+ try {
68
+ const terminal = validateRequest(JSON.parse(await readFile(path, 'utf8')));
69
+ if (terminal.status !== 'resolved') throw new Error('NOFAX_REQUEST_TERMINAL_INVALID');
70
+ return terminal;
71
+ } catch (error) {
72
+ if (error?.code === 'ENOENT') return null;
73
+ if (error instanceof SyntaxError) throw new Error('NOFAX_REQUEST_TERMINAL_INVALID_JSON');
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ async function claimTerminal(path, value) {
79
+ const temp = `${path}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
80
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
81
+ try { await chmod(temp, 0o600); } catch {}
82
+
83
+ try {
84
+ await link(temp, path);
85
+ try { await chmod(path, 0o600); } catch {}
86
+ return true;
87
+ } catch (error) {
88
+ if (error?.code === 'EEXIST') return false;
89
+ throw error;
90
+ } finally {
91
+ try { await unlink(temp); } catch {}
92
+ }
93
+ }
94
+
61
95
  export async function savePendingRequest({ home, env, request }) {
62
96
  const normalized = validateRequest(request);
63
97
  if (normalized.status !== 'pending') throw new Error('NOFAX_REQUEST_NOT_PENDING');
@@ -68,7 +102,9 @@ export async function savePendingRequest({ home, env, request }) {
68
102
  }
69
103
 
70
104
  export async function loadRequest({ home, env, requestId }) {
71
- const { file } = paths({ home, env, requestId });
105
+ const { file, terminal } = paths({ home, env, requestId });
106
+ const claimed = await loadTerminalClaim(terminal);
107
+ if (claimed !== null) return claimed;
72
108
  try {
73
109
  return validateRequest(JSON.parse(await readFile(file, 'utf8')));
74
110
  } catch (error) {
@@ -89,8 +125,10 @@ export async function resolveRequest({ home, env, requestId, response, resolvedA
89
125
  decision: response.decision,
90
126
  ...(response.text === undefined ? {} : { text: response.text })
91
127
  });
92
- const { file } = paths({ home, env, requestId });
93
- await atomicWrite(file, resolved);
128
+ const { terminal } = paths({ home, env, requestId });
129
+ if (!await claimTerminal(terminal, resolved)) {
130
+ return loadRequest({ home, env, requestId });
131
+ }
94
132
  return resolved;
95
133
  }
96
134