nofax 0.2.4 → 0.2.6

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.4",
3
+ "version": "0.2.6",
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",
@@ -74,7 +74,7 @@ const LIST_PENDING_OUTPUT_SCHEMA = z.object({
74
74
 
75
75
  export function buildMcpServer({ handlers = createMcpToolHandlers() } = {}) {
76
76
  const server = new McpServer(
77
- { name: 'nofax', version: '0.2.4' },
77
+ { name: 'nofax', version: '0.2.6' },
78
78
  { instructions: SERVER_INSTRUCTIONS }
79
79
  );
80
80
 
@@ -156,7 +156,7 @@ export function buildMcpServer({ handlers = createMcpToolHandlers() } = {}) {
156
156
  waitSeconds: z.number().int().min(1).max(240).optional().describe('Maximum seconds to long-poll during this call; defaults to 240. A timeout still returns pending, never approval.')
157
157
  }),
158
158
  outputSchema: WAIT_OUTPUT_SCHEMA,
159
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
159
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
160
160
  },
161
161
  async (args) => result(await handlers.waitForResponse(args))
162
162
  );
package/src/mcp-tools.mjs CHANGED
@@ -140,6 +140,17 @@ export function createMcpToolHandlers(overrides = {}) {
140
140
  return pendingResult(remote.requestId);
141
141
  }
142
142
 
143
+ async function createDurableRemote(kind, input) {
144
+ let persisted;
145
+ const remote = await deps.createRemoteRequestImpl({
146
+ ...input,
147
+ beforePublish: async (prepared) => {
148
+ persisted = await persistRemote({ kind, remote: prepared });
149
+ }
150
+ });
151
+ return persisted ?? persistRemote({ kind, remote });
152
+ }
153
+
143
154
  return {
144
155
  async notify({ title = 'Nofax', message }) {
145
156
  const current = await config();
@@ -153,7 +164,7 @@ export function createMcpToolHandlers(overrides = {}) {
153
164
 
154
165
  async requestApproval({ title = 'Nofax approval', message, allowRefine = false }) {
155
166
  const current = await config();
156
- const remote = await deps.createRemoteRequestImpl({
167
+ return createDurableRemote('approval', {
157
168
  config: current,
158
169
  title: validateText(title, 'TITLE', 120),
159
170
  message: validateText(message, 'MESSAGE'),
@@ -163,32 +174,29 @@ export function createMcpToolHandlers(overrides = {}) {
163
174
  ],
164
175
  includeRefine: allowRefine === true
165
176
  });
166
- return persistRemote({ kind: 'approval', remote });
167
177
  },
168
178
 
169
179
  async requestChoice({ title = 'Nofax choice', message, options }) {
170
180
  const current = await config();
171
181
  const normalized = validateChoiceOptions(options);
172
- const remote = await deps.createRemoteRequestImpl({
182
+ return createDurableRemote('choice', {
173
183
  config: current,
174
184
  title: validateText(title, 'TITLE', 120),
175
185
  message: validateText(message, 'MESSAGE'),
176
186
  options: normalized,
177
187
  includeRefine: false
178
188
  });
179
- return persistRemote({ kind: 'choice', remote });
180
189
  },
181
190
 
182
191
  async requestRefinement({ title = 'Nofax refinement', message }) {
183
192
  const current = await config();
184
- const remote = await deps.createRemoteRequestImpl({
193
+ return createDurableRemote('refinement', {
185
194
  config: current,
186
195
  title: validateText(title, 'TITLE', 120),
187
196
  message: validateText(message, 'MESSAGE'),
188
197
  options: [],
189
198
  includeRefine: true
190
199
  });
191
- return persistRemote({ kind: 'refinement', remote });
192
200
  },
193
201
 
194
202
  async waitForResponse({ requestId, waitSeconds }) {
@@ -213,11 +221,15 @@ export function createMcpToolHandlers(overrides = {}) {
213
221
  response,
214
222
  resolvedAt: new Date(deps.nowImpl()).toISOString()
215
223
  });
216
- await deps.sendResponseConfirmationImpl({
217
- config: current,
218
- response,
219
- title: request.kind === 'approval' ? 'Approval' : request.kind === 'refinement' ? 'Refinement' : 'Choice'
220
- });
224
+ const acceptedResponse = request.decision === response.decision
225
+ && (request.text ?? undefined) === (response.text ?? undefined);
226
+ if (acceptedResponse) {
227
+ await deps.sendResponseConfirmationImpl({
228
+ config: current,
229
+ response,
230
+ title: request.kind === 'approval' ? 'Approval' : request.kind === 'refinement' ? 'Refinement' : 'Choice'
231
+ });
232
+ }
221
233
  return terminalResult(request);
222
234
  }
223
235
  const remaining = deadline - deps.nowImpl();
package/src/ntfy.mjs CHANGED
@@ -96,6 +96,7 @@ export async function createRemoteRequest({
96
96
  options = [],
97
97
  includeRefine = false,
98
98
  shortcutName = config.refineShortcutName ?? DEFAULT_REFINE_SHORTCUT,
99
+ beforePublish = async () => {},
99
100
  fetchImpl = fetch
100
101
  }) {
101
102
  const normalized = normalizeOptions(options);
@@ -126,8 +127,10 @@ export async function createRemoteRequest({
126
127
  }
127
128
  }
128
129
 
130
+ const remote = { requestId, responseTopic, allowed };
131
+ await beforePublish(remote);
129
132
  await sendNotification({ config, title, message, actions, fetchImpl });
130
- return { requestId, responseTopic, allowed };
133
+ return remote;
131
134
  }
132
135
 
133
136
  function parseNtfyPoll(text, requestId, allowed) {
@@ -149,7 +152,7 @@ function parseNtfyPoll(text, requestId, allowed) {
149
152
  export async function pollRemoteResponse({ config, responseTopic, requestId, allowed, fetchImpl = fetch }) {
150
153
  let response;
151
154
  try {
152
- response = await fetchImpl(`${config.server}/${responseTopic}/json?poll=1&since=10m`, {
155
+ response = await fetchImpl(`${config.server}/${responseTopic}/json?poll=1&since=all`, {
153
156
  method: 'GET',
154
157
  headers: { accept: 'application/x-ndjson' }
155
158
  });