hirify-cli 0.4.3 → 0.4.4

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/hirify.js +82 -2
  2. package/package.json +1 -1
package/bin/hirify.js CHANGED
@@ -271,14 +271,73 @@ const options = (args) => {
271
271
  const count = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null)
272
272
 
273
273
  const out = (data, text) => {
274
- if (process.argv.includes('--json')) console.log(JSON.stringify(data, null, 2))
275
- else text()
274
+ if (process.argv.includes('--json')) {
275
+ // --json hands over the whole envelope, so a `meta.notice` on it travels structured and
276
+ // untouched. The human notice below is only for text mode.
277
+ console.log(JSON.stringify(data, null, 2))
278
+ return
279
+ }
280
+ text()
281
+ printNotice(data?.meta?.notice)
282
+ }
283
+
284
+ /**
285
+ * Print a structured notice the server attached to a successful answer (`meta.notice`). The
286
+ * call still succeeded, so this comes after the useful result, not in place of it. The CLI
287
+ * shows the text the server wrote and reads nothing into the policy behind it: it knows the
288
+ * notice object, not what produced it. No notice, or one with no message, prints nothing.
289
+ */
290
+ function printNotice(notice) {
291
+ const message = typeof notice?.message === 'string' ? notice.message.trim() : ''
292
+ if (!message) return
293
+ console.log(`\nNotice: ${message}`)
276
294
  }
277
295
 
278
296
  // Text our own server wrote for the agent to read: application rules, address rejections.
279
297
  // Passing it through beats paraphrasing, because paraphrasing drifts from the real rule.
280
298
  const serverMessage = (body) => (typeof body?.message === 'string' && body.message ? body.message : null)
281
299
 
300
+ /**
301
+ * The one-line report for a policy restriction: a canonical 403 that names itself
302
+ * `access_restricted`. It is kept apart from the quota and rate-limit walls on purpose,
303
+ * because those clear on their own and this one does not. The CLI states what the server
304
+ * sent - the message it wrote, when the restriction lifts, where to ask for a review - and
305
+ * adds no rule of its own about why it was applied.
306
+ */
307
+ function accessRestrictedMessage(body) {
308
+ const err = (body && typeof body.error === 'object' && body.error) || {}
309
+ const line = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null)
310
+ const parts = [line(err.message) || line(body?.message) || 'access on this account is restricted (403).']
311
+ const until = line(err.restricted_until)
312
+ if (until) parts.push(`It stays in place until ${until}.`)
313
+ const appeal = line(err.appeal_url)
314
+ if (appeal) parts.push(`To ask us to look at it again: ${appeal}`)
315
+ return parts.join('\n ')
316
+ }
317
+
318
+ /**
319
+ * The report for a blocking notice: a canonical 409 that names itself `action_required`.
320
+ * Unlike a `meta.notice` on a success, this one stands in place of the answer - the server is
321
+ * holding the result until the person confirms - so the CLI shows the text the server wrote and
322
+ * the one command that clears it, and reads nothing into the policy behind it. That command is
323
+ * the acknowledgement capability the server lists in its manifest, reached the same way as any
324
+ * other, so no route to it is written here. It carries the notice's own id and its first
325
+ * advertised action, both as the server sent them, so the printed line runs as it stands.
326
+ */
327
+ function actionRequiredMessage(body) {
328
+ const notice = (body && typeof body.error === 'object' && body.error && typeof body.error.notice === 'object' && body.error.notice) || {}
329
+ const line = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null)
330
+ const parts = [line(notice.message) || 'this needs your confirmation before it can go ahead (409).']
331
+
332
+ const id = line(notice.id)
333
+ const action = Array.isArray(notice.actions) ? notice.actions.map(line).find(Boolean) : null
334
+ const call = 'hirify api call security.notices.ack'
335
+ parts.push(id && action
336
+ ? `To continue, acknowledge this notice: ${call} --data '${JSON.stringify({ id, action })}'`
337
+ : `To continue, acknowledge this notice: ${call}`)
338
+ return parts.join('\n ')
339
+ }
340
+
282
341
  const b64url = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
283
342
  const now = () => Math.floor(Date.now() / 1000)
284
343
 
@@ -384,6 +443,27 @@ async function api(path, { method = 'GET', payload = null, allow = [], raw = fal
384
443
  return { status: res.status, body, text }
385
444
  }
386
445
 
446
+ // A policy restriction is a canonical 403 that names itself `access_restricted`. It is not a
447
+ // used-up allowance and not a rate limit, and retrying cannot lift it: the CLI reports it once
448
+ // and never retries. Checked ahead of `allow` so a caller that reads its own 403s still cannot
449
+ // take this one for an ordinary refusal. The clone leaves the body readable for the paths below.
450
+ if (res.status === 403) {
451
+ const restriction = await res.clone().json().catch(() => null)
452
+ if (restriction?.error?.code === 'access_restricted') die(accessRestrictedMessage(restriction))
453
+ }
454
+
455
+ // A blocking notice is a canonical 409 that names itself `action_required`. Unlike a
456
+ // `meta.notice` on a success, it stands in place of the answer: the server is holding the
457
+ // result until the person acknowledges the notice, so the CLI reports it, exits unsuccessfully,
458
+ // and never retries - retrying cannot clear it. Checked ahead of `allow`, like the restriction
459
+ // above, so a caller that reads its own 409s cannot take this one for an ordinary conflict; a
460
+ // 409 without this code is left to the handling below, unchanged. The clone leaves the body
461
+ // readable for those paths.
462
+ if (res.status === 409) {
463
+ const blocked = await res.clone().json().catch(() => null)
464
+ if (blocked?.error?.code === 'action_required') die(actionRequiredMessage(blocked))
465
+ }
466
+
387
467
  if (allow.includes(res.status)) {
388
468
  return {
389
469
  status: res.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hirify-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Hirify CLI for AI agents: browser sign-in, job feeds, search, reading a vacancy in full, contacts to apply to, and applying on Hirify.",
5
5
  "bin": {
6
6
  "hirify": "bin/hirify.js"