hirify-cli 0.6.0 → 0.6.2

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/README.md CHANGED
@@ -14,7 +14,8 @@ npx -y hirify-cli@latest init
14
14
 
15
15
  One command installs the CLI globally, installs the skill for the agents found on this computer
16
16
  (Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Antigravity) and signs you in through the browser.
17
- Run it yourself or let your agent run it. `--no-install` and `--no-login` skip a step.
17
+ Run it yourself or let your agent run it. It installs the newest release, whichever version
18
+ started it; a pinned version (`HIRIFY_VERSION_PIN`, `HIRIFY_NO_AUTO_UPDATE=1`) is kept.
18
19
 
19
20
  The same steps one by one:
20
21
 
package/bin/lib/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { CliError, redact, safeText } from './errors.js'
2
2
  const ARGUMENTS = { auth: '[<key>]', update: '[<version>]', 'vacancy search': '[<query>]', 'vacancy read': '<slug>', 'vacancy reveal': '<slug>', 'vacancy apply': '<slug>', 'vacancy hide': '<slug>...', 'company hide': '[<name>...]', 'feed show': '<id>', 'feed create': '<name>', 'feed deliver': '<id>', 'webhook create': '<name> <url>', 'feedback send': '<bug|feature> <title>', 'api call': '<capability>', 'capabilities show': '<capability>' }
3
3
  const GLOBAL = ['json', 'fields', 'error-format', 'debug', 'timeout', 'help']
4
- export const BOOLEAN_FLAGS = new Set(['json', 'debug', 'help', 'force', 'no-browser', 'no-install', 'no-login', 'stdin', 'telegram', 'no-telegram', 'no-webhook', 'check', 'rollback', 'undo'])
4
+ export const BOOLEAN_FLAGS = new Set(['json', 'debug', 'help', 'force', 'no-browser', 'stdin', 'telegram', 'no-telegram', 'no-webhook', 'check', 'rollback', 'undo'])
5
5
  export const COMMANDS = {
6
6
  intro: { args: [0, 0], description: 'what this can do, and in what order' },
7
7
  version: { args: [0, 0], description: 'installed version' },
8
- init: { args: [0, 0], flags: ['no-install', 'no-login', 'no-browser', 'callback-port'], description: 'install the CLI and the skill, then sign in' },
8
+ init: { args: [0, 0], flags: ['no-browser', 'callback-port'], description: 'install the CLI and the skill, then sign in' },
9
9
  skill: { args: [0, 0], description: 'install the working rules for your agent' },
10
10
  login: { args: [0, 0], flags: ['force', 'no-browser', 'callback-port'], description: 'sign in through your browser' },
11
11
  logout: { args: [0, 0], description: 'sign out on this computer' },
@@ -204,6 +204,14 @@ function detailRows(pairs) {
204
204
  .map(([label, text]) => `${(label + ':').padEnd(10)}${text}`)
205
205
  }
206
206
 
207
+ /**
208
+ * The word that names a vacancy, offered under both names a manifest may use for the path
209
+ * slot. Hirify moved vacancies.read and vacancies.reveal from `{slug}` to `{vacancy_id}` on
210
+ * 2026-09-30 and reads a non-numeric value in that slot as the slug, so one set of params
211
+ * serves the old manifest, the new one, a slug and a numeric id alike.
212
+ */
213
+ const vacancyRef = (word) => ({ slug: word, vacancy_id: word })
214
+
207
215
  /**
208
216
  * One vacancy in full, with the text a person reads on the site. This is the command an
209
217
  * agent shortlists with, so it is deliberately the cheap one: it counts against the day's
@@ -218,7 +226,7 @@ async function cmdVacancyRead(args, words) {
218
226
  '\n Slugs come from hirify vacancy search or hirify feed show.')
219
227
  }
220
228
 
221
- const res = await callCapability('vacancies.read', { params: { slug }, allow: [200, 404] })
229
+ const res = await callCapability('vacancies.read', { params: vacancyRef(slug), allow: [200, 404] })
222
230
  if (res.status === 404) die('there is no vacancy with that slug.')
223
231
 
224
232
  const d = res.body?.data ?? {}
@@ -276,7 +284,7 @@ function contactLine(c) {
276
284
  async function cmdVacancyReveal(args, words) {
277
285
  const [slug] = words
278
286
  if (!slug) die('a vacancy slug is required: hirify vacancy reveal <slug>')
279
- const body = await callCapability('vacancies.reveal', { params: { slug } })
287
+ const body = await callCapability('vacancies.reveal', { params: vacancyRef(slug) })
280
288
  const d = body?.data ?? {}
281
289
  out(body, () => {
282
290
  console.log(`company: ${d.company ?? '-'}`)
package/bin/lib/main.js CHANGED
@@ -73,30 +73,26 @@ export async function main(argv = process.argv.slice(2), { baseRoot = ownRoot, b
73
73
  const { installGlobally, installSkill, describeSkill } = await import('./setup.js')
74
74
  const result = {}
75
75
  let skillRoot = ownRoot
76
- if (!command.values['no-install']) {
77
- output.progress('Installing the Hirify CLI...')
78
- // A newer CLI found on this computer stays, and the skill is taken from it.
79
- const { root, ...cli } = await installGlobally({ version: pkg.version, signal, env: config.env })
80
- result.cli = cli
81
- if (root) skillRoot = root
82
- }
76
+ output.progress('Installing the Hirify CLI...')
77
+ const { root, ...cli } = await installGlobally({ version: pkg.version, latest: updater.check, signal, env: config.env })
78
+ result.cli = cli
79
+ // The skill comes from the CLI that is installed, so the two match.
80
+ if (cli.installed && cli.version !== pkg.version) skillRoot = root
83
81
  try { result.skill = installSkill({ packageRoot: skillRoot, env: config.env }) }
84
82
  catch (error) { if (!(error instanceof CliError)) throw error; result.skill = { error: error.code, message: error.message, agents: [] } }
85
- if (!command.values['no-login']) {
86
- let source = 'none'
87
- try { source = auth.status().source } catch {}
88
- if (['environment', 'key'].includes(source)) result.sign_in = { signed_in: true, already_signed_in: true, source }
89
- else try { result.sign_in = await auth.login({ noBrowser: Boolean(command.values['no-browser']), port: Number(command.values['callback-port'] || 0), consentMs: Math.min(command.timeout, 300000) }) }
90
- catch (error) { if (!(error instanceof CliError) || signal.aborted) throw error; result.sign_in = { signed_in: false, error: error.code, message: error.message } }
91
- }
83
+ try {
84
+ const { source } = auth.status()
85
+ // A configured key is a finished sign-in: no browser, no waiting.
86
+ result.sign_in = ['environment', 'key'].includes(source) ? { signed_in: true, already_signed_in: true, source } : await auth.login({ noBrowser: Boolean(command.values['no-browser']), port: Number(command.values['callback-port'] || 0), consentMs: Math.min(command.timeout, 300000) })
87
+ } catch (error) { if (!(error instanceof CliError) || signal.aborted) throw error; result.sign_in = { signed_in: false, error: error.code, message: error.message } }
92
88
  if (command.json) output.json(result)
93
89
  else {
94
- if (result.cli) output.console.log(result.cli.installed ? `Hirify CLI ${result.cli.version} is installed. The command is: hirify` : `The CLI could not be installed globally (${result.cli.reason}). Every command still works as: npx -y hirify-cli <command>`)
90
+ output.console.log(result.cli.installed ? `Hirify CLI ${result.cli.version} is installed. The command is: hirify` : `The CLI could not be installed globally (${result.cli.reason}). Every command still works as: npx -y hirify-cli <command>`)
95
91
  if (result.skill.error) output.console.log(result.skill.message); else for (const line of describeSkill(result.skill)) output.console.log(line)
96
- if (result.sign_in) output.console.log(result.sign_in.signed_in ? (result.sign_in.already_signed_in ? 'Already signed in.' : 'Signed in. Access saved on this computer.') : result.sign_in.message)
97
- if (result.sign_in?.signed_in) output.console.log('Next: ask your agent to look at your Hirify profile and saved searches.')
92
+ output.console.log(result.sign_in.signed_in ? (result.sign_in.already_signed_in ? 'Already signed in.' : 'Signed in. Access saved on this computer.') : result.sign_in.message)
93
+ if (result.sign_in.signed_in) output.console.log('Next: ask your agent to look at your Hirify profile and saved searches.')
98
94
  }
99
- if (result.cli?.installed === false || result.skill.error || result.skill.agents.some(item => !item.installed) || result.sign_in?.signed_in === false) exitCode = 1
95
+ if (!result.cli.installed || result.skill.error || result.skill.agents.some(item => !item.installed) || !result.sign_in.signed_in) exitCode = 1
100
96
  } else if (command.name === 'auth') {
101
97
  secrets.add(command.words[0])
102
98
  await store.commit({ kind: 'key', access_token: command.words[0] })
package/bin/lib/setup.js CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, cpSync, rmSync, symlinkSync, lstatSync, realpathSync, readFileSync } from 'node:fs'
1
+ import { existsSync, mkdirSync, cpSync, rmSync, symlinkSync, realpathSync, readFileSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
- import { join, relative } from 'node:path'
3
+ import { dirname, join, relative } from 'node:path'
4
4
  import semver from 'semver'
5
5
  import { CliError } from './errors.js'
6
6
  import { PACKAGE_NAME, runProcess } from './update.js'
@@ -26,20 +26,24 @@ export function agentDirectories({ env = process.env, home = homedir() } = {}) {
26
26
  ]
27
27
  }
28
28
 
29
- const entry = path => { try { return lstatSync(path) } catch (error) { if (error.code === 'ENOENT') return null; throw error } }
30
- const real = path => { try { return realpathSync(path) } catch { return path } }
31
29
  // Removing a link removes the link, never the directory it points to.
32
- const remove = path => { if (entry(path)) rmSync(path, { recursive: true, force: true }) }
33
- const readJSON = file => { try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return null } }
30
+ const remove = path => rmSync(path, { recursive: true, force: true })
34
31
  // OS messages may carry private paths, so only the code is reported.
35
32
  const reason = error => ['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'EDQUOT', 'ENOTDIR', 'ENOENT', 'EEXIST', 'ELOOP'].includes(error?.code) ? error.code : 'unknown'
36
33
 
34
+ // A junction needs no elevated rights on Windows; elsewhere the link stays relative.
35
+ const linkSkill = (path, target, platform) => (platform === 'win32'
36
+ ? symlinkSync(realpathSync(path), target, 'junction')
37
+ : symlinkSync(relative(dirname(target), realpathSync(path)), target, 'dir'))
38
+
37
39
  /**
38
40
  * Install the skill carried by this package: one shared copy under `~/.agents/skills`, linked into
39
41
  * every agent found here. The skill comes from the package of the CLI it is for, so the two match.
40
- * An agent that cannot be written to is reported and does not stop the others.
42
+ * An agent that cannot be written to is reported and does not stop the others. An agent that
43
+ * cannot take a link gets a copy, and the result says so: a copy does not follow the shared one
44
+ * when the CLI updates, so the person knows to run `hirify skill` again.
41
45
  */
42
- export function installSkill({ packageRoot, env = process.env, home = homedir(), platform = process.platform } = {}) {
46
+ export function installSkill({ packageRoot, env = process.env, home = homedir(), platform = process.platform, link = linkSkill } = {}) {
43
47
  const source = join(packageRoot, 'skills', SKILL_NAME)
44
48
  if (!existsSync(join(source, 'SKILL.md'))) throw new CliError('skill_missing', 'This installation does not carry the skill. Install it with: npx skills add hirifyme/hirify-cli')
45
49
  const shared = join(home, '.agents', 'skills')
@@ -55,41 +59,47 @@ export function installSkill({ packageRoot, env = process.env, home = homedir(),
55
59
  try {
56
60
  mkdirSync(agent.skills, { recursive: true })
57
61
  // An agent whose skills directory already is the shared one needs no link of its own.
58
- const parent = real(agent.skills)
59
- if (parent === real(shared)) { agents.push({ agent: agent.name, installed: true }); continue }
62
+ const parent = realpathSync(agent.skills)
63
+ if (parent === realpathSync(shared)) { agents.push({ agent: agent.name, installed: true }); continue }
60
64
  const target = join(parent, SKILL_NAME)
61
65
  remove(target)
62
- try {
63
- // A junction needs no elevated rights on Windows; elsewhere the link stays relative.
64
- if (platform === 'win32') symlinkSync(real(path), target, 'junction')
65
- else symlinkSync(relative(parent, real(path)), target, 'dir')
66
- } catch { cpSync(source, target, { recursive: true }) }
67
- agents.push({ agent: agent.name, installed: true })
66
+ let copied = false
67
+ try { link(path, target, platform) } catch { cpSync(source, target, { recursive: true }); copied = true }
68
+ agents.push(copied ? { agent: agent.name, installed: true, copied: true } : { agent: agent.name, installed: true })
68
69
  } catch (error) { agents.push({ agent: agent.name, installed: false, reason: reason(error) }) }
69
70
  }
70
71
  return { skill: SKILL_NAME, path, agents }
71
72
  }
72
73
 
73
74
  /**
74
- * Put the CLI on PATH with the person's own npm and its settings: the environment goes to npm as
75
- * it is, because what npx adds to it is the same configuration, already resolved. A failure is a
76
- * result, not an error: without a global installation every command still runs as
77
- * `npx -y hirify-cli <command>`. A newer global installation is kept, never downgraded, and its
78
- * `root` says where the skill that matches it is.
75
+ * Put the newest release of the CLI on PATH, whichever package started the command, with the
76
+ * person's own npm and its settings: the environment goes to npm as it is, because what npx adds
77
+ * to it is the same configuration, already resolved. A pinned version and switched-off updates
78
+ * keep the running release, as they do for automatic updates. A failure is a result, not an
79
+ * error: without a global installation every command still runs as `npx -y hirify-cli <command>`.
80
+ * An installation that is already at that release or past it is kept. `root` is where the
81
+ * installed package is, and with it the skill that matches it.
79
82
  */
80
- export async function installGlobally({ version, signal, env = process.env, run = runProcess } = {}) {
83
+ export async function installGlobally({ version, latest, signal, env = process.env, run = runProcess } = {}) {
81
84
  const options = { signal, env }
85
+ if (!env.HIRIFY_VERSION_PIN && env.HIRIFY_NO_AUTO_UPDATE !== '1') {
86
+ try { version = (await latest()).version }
87
+ catch (error) { if (signal?.aborted || !(error instanceof CliError)) throw error; return { installed: false, reason: error.code } }
88
+ }
82
89
  let root
83
90
  try { root = await run('npm', ['root', '--global'], { ...options, timeout: 10000 }) }
84
91
  catch (error) { if (signal?.aborted) throw error; return { installed: false, reason: 'npm_unavailable' } }
85
92
  if (root.code !== 0 || !root.stdout.trim()) return { installed: false, reason: 'npm_unavailable' }
86
93
  const location = join(root.stdout.trim(), PACKAGE_NAME)
87
- const current = readJSON(join(location, 'package.json'))
88
- if (current?.name === PACKAGE_NAME && semver.valid(current.version) && semver.gte(current.version, version)) return { installed: true, version: current.version, changed: false, ...(semver.gt(current.version, version) && { root: location }) }
94
+ let current
95
+ // Only a package that is not there counts as absent; one that cannot be read is reported.
96
+ try { current = JSON.parse(readFileSync(join(location, 'package.json'), 'utf8')) }
97
+ catch (error) { if (error.code !== 'ENOENT') return { installed: false, reason: 'global_package_unreadable' } }
98
+ if (current?.name === PACKAGE_NAME && semver.valid(current.version) && semver.gte(current.version, version)) return { installed: true, version: current.version, changed: false, root: location }
89
99
  let result
90
100
  try { result = await run('npm', ['install', '--global', '--ignore-scripts', '--no-audit', '--no-fund', `${PACKAGE_NAME}@${version}`], { ...options, timeout: 180000 }) }
91
101
  catch (error) { if (signal?.aborted) throw error; return { installed: false, reason: 'install_failed' } }
92
- return result.code === 0 ? { installed: true, version, changed: true } : { installed: false, reason: 'install_failed' }
102
+ return result.code === 0 ? { installed: true, version, changed: true, root: location } : { installed: false, reason: 'install_failed' }
93
103
  }
94
104
 
95
105
  /** Lines a person reads after `hirify skill` or `hirify init`. */
@@ -98,5 +108,7 @@ export function describeSkill(result) {
98
108
  const failed = result.agents.filter(item => !item.installed).map(item => `${item.agent} (${item.reason})`)
99
109
  const lines = [done.length ? `The skill is installed for ${done.join(', ')}.` : `The skill is saved in ${result.path}. No supported agent was found on this computer.`]
100
110
  if (failed.length) lines.push(`It could not be installed for ${failed.join(', ')}. Check that the agent's skills directory is a folder you can write to, then run hirify skill.`)
111
+ const copies = result.agents.filter(item => item.copied).map(item => item.agent)
112
+ if (copies.length) lines.push(`For ${copies.join(', ')} the skill is a copy, not a link: after the CLI updates, run hirify skill to refresh it.`)
101
113
  return lines
102
114
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hirify-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "hirify-cli",
9
- "version": "0.6.0",
9
+ "version": "0.6.2",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "cross-spawn": "7.0.6",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hirify-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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"