hirify-cli 0.5.0 → 0.5.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 +26 -0
- package/bin/lib/auth.js +0 -1
- package/bin/lib/cli.js +5 -3
- package/bin/lib/commands.js +31 -0
- package/bin/lib/store.js +12 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/skills/hirify/SKILL.md +11 -3
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ hirify filter guide
|
|
|
68
68
|
hirify vacancy search "senior go"
|
|
69
69
|
hirify vacancy read <slug>
|
|
70
70
|
hirify vacancy reveal <slug>
|
|
71
|
+
hirify vacancy hide <slug>...
|
|
72
|
+
hirify company hide "<name>"
|
|
71
73
|
hirify profile list
|
|
72
74
|
hirify vacancy apply <slug> --profile <id> --cover-file cover.txt
|
|
73
75
|
```
|
|
@@ -76,6 +78,8 @@ Lists and searches are free. Reading a vacancy in full, revealing its contact, a
|
|
|
76
78
|
have a separate allowance. `hirify account show` supplies the current values. Shortlist from cards,
|
|
77
79
|
read to assess fit, then reveal what fits. Applying sends a real application and cannot be recalled:
|
|
78
80
|
get the person's approval first. External vacancies are applied to through the destination from `reveal`.
|
|
81
|
+
Hiding a vacancy or a company is free and has no limit: it stops coming back in search and feeds,
|
|
82
|
+
here and on the website. `--undo` brings it back.
|
|
79
83
|
|
|
80
84
|
The server defines search filters. Read `hirify filter guide`, preview with `filters.preview`, refine,
|
|
81
85
|
and then run the final search. Do not infer filter names from an old example.
|
|
@@ -121,6 +125,28 @@ Explicitly safe reads get bounded retries for transient network errors and 429/5
|
|
|
121
125
|
Feedback accepts `--idempotency-key` when the server advertises caller-keyed deduplication;
|
|
122
126
|
keep the same key for a deliberate retry. The CLI prints a generated request reference otherwise.
|
|
123
127
|
|
|
128
|
+
## Codex and other sandboxed agents
|
|
129
|
+
|
|
130
|
+
Sign in once from your normal terminal. A local agent can reuse that sign-in when it has access
|
|
131
|
+
to the same configuration directory. It does not need a separate API key.
|
|
132
|
+
Reading an unexpired session does not create files or change permissions. Renewing an OAuth
|
|
133
|
+
session requires write access so the rotated token can be saved safely.
|
|
134
|
+
|
|
135
|
+
A Windows Codex sandbox can run with a restricted token or a different Windows account. Success
|
|
136
|
+
in `cmd` therefore does not prove the sandbox can access the saved sign-in. Keep credentials in
|
|
137
|
+
their normal private directory. If a command is denied, let Codex request approval for that specific
|
|
138
|
+
command using its normal permission mechanism. Do not disable the whole sandbox, grant `Everyone`
|
|
139
|
+
access, copy `auth.json` into the project, or paste a token into chat.
|
|
140
|
+
Read permission alone will not support token renewal. If approval is unavailable, report the
|
|
141
|
+
restriction instead of repeatedly running login. WSL and containers have separate home directories;
|
|
142
|
+
a Windows login is not automatically shared with them.
|
|
143
|
+
|
|
144
|
+
Compare `hirify version`, `hirify doctor`, and `hirify auth status --json --error-format=json`
|
|
145
|
+
inside the agent and in the terminal. These commands do not print tokens. `storage_unreadable`
|
|
146
|
+
means reading was denied or failed; `storage_unavailable`, `storage_lock_failed`, and
|
|
147
|
+
`storage_write_failed` identify preparation, locking, or saving failures. An access failure does
|
|
148
|
+
not by itself mean the sign-in expired. See [Codex Windows sandbox documentation](https://learn.chatgpt.com/docs/windows/windows-sandbox).
|
|
149
|
+
|
|
124
150
|
## Configuration and diagnostics
|
|
125
151
|
|
|
126
152
|
Access is stored in `auth.json` under `$XDG_CONFIG_HOME/hirify` when XDG_CONFIG_HOME is absolute,
|
package/bin/lib/auth.js
CHANGED
|
@@ -42,7 +42,6 @@ export function createAuth({ config, store, http, signal, output, event, secrets
|
|
|
42
42
|
}
|
|
43
43
|
async function access() {
|
|
44
44
|
if (config.envKey) { secrets.add(config.envKey); return config.envKey }
|
|
45
|
-
await store.secure()
|
|
46
45
|
let current = remember(store.read({ allowPending: true }))
|
|
47
46
|
if (!current) throw new CliError('auth_required', 'You are not signed in. Run hirify login. For CI or servers, set HIRIFY_KEY or use hirify auth --stdin.')
|
|
48
47
|
if (current.kind === 'oauth' && (current.refresh_pending || (current.expires_at && current.expires_at <= Date.now() / 1000 + 60))) current = await refresh(current)
|
package/bin/lib/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError, redact, safeText } from './errors.js'
|
|
2
|
-
const ARGUMENTS = { auth: '[<key>]', update: '[<version>]', 'vacancy search': '[<query>]', 'vacancy read': '<slug>', 'vacancy reveal': '<slug>', 'vacancy apply': '<slug>', 'feed show': '<id>', 'feed create': '<name>', 'feed deliver': '<id>', 'webhook create': '<name> <url>', 'feedback send': '<bug|feature> <title>', 'api call': '<capability>', 'capabilities show': '<capability>' }
|
|
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', 'stdin', 'telegram', 'no-telegram', 'no-webhook', 'check', 'rollback'])
|
|
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' },
|
|
@@ -17,6 +17,8 @@ export const COMMANDS = {
|
|
|
17
17
|
'vacancy read': { args: [1, 1], description: 'read one vacancy in full; uses a vacancy open' },
|
|
18
18
|
'vacancy reveal': { args: [1, 1], description: 'get the contact or link to apply; uses a reveal' },
|
|
19
19
|
'vacancy apply': { args: [1, 1], flags: ['profile', 'cover', 'cover-file'], description: 'send a real application; ask the person first' },
|
|
20
|
+
'vacancy hide': { args: [1, 100], flags: ['undo'], description: 'hide vacancies from your search and feeds; free, no limit' },
|
|
21
|
+
'company hide': { args: [0, 100], flags: ['vacancy', 'undo'], description: 'hide a company by name, or by --vacancy <slug>; free, no limit' },
|
|
20
22
|
'feed list': { args: [0, 0], description: 'your saved feeds' },
|
|
21
23
|
'feed show': { args: [1, 1], flags: ['limit', 'page'], description: 'vacancies in a feed' },
|
|
22
24
|
'feed create': { args: [1, 1], flags: ['filters', 'telegram', 'no-telegram', 'webhook'], description: 'save a search and its delivery settings' },
|
|
@@ -99,7 +101,7 @@ export function parse(argv) {
|
|
|
99
101
|
}
|
|
100
102
|
export function help(topic = '') {
|
|
101
103
|
const entries = Object.entries(COMMANDS).filter(([name]) => !topic || name === topic || name.startsWith(topic + ' '))
|
|
102
|
-
return `hirify - job search for AI agents\n\n${(entries.length ? entries : Object.entries(COMMANDS)).map(([name, spec]) => ` hirify ${(name + (ARGUMENTS[name] ? ' ' + ARGUMENTS[name] : '')).padEnd(35)} ${spec.description}${spec.flags?.length ? `\n ${spec.flags.map(f => '--' + f + (BOOLEAN_FLAGS.has(f) ? '' : ' <value>')).join(' ')}` : ''}`).join('\n')}\n\nGlobal options: --json, --fields a,b, --error-format json|text, --timeout <seconds>, --debug, --help.\nUse -- to end options; --name=value and --name value are supported.\nLists and search are free. Vacancy read, reveal and apply have separate allowances.\nCheck them with hirify account show. Apply sends a real application: ask the person first.\nHeadless/CI: HIRIFY_KEY or hirify auth --stdin. Manual login: --no-browser.\nSSH: forward the chosen --callback-port to the same loopback port on the CLI host.\nRules for your agent: npx skills add hirifyme/hirify-cli\n`
|
|
104
|
+
return `hirify - job search for AI agents\n\n${(entries.length ? entries : Object.entries(COMMANDS)).map(([name, spec]) => ` hirify ${(name + (ARGUMENTS[name] ? ' ' + ARGUMENTS[name] : '')).padEnd(35)} ${spec.description}${spec.flags?.length ? `\n ${spec.flags.map(f => '--' + f + (BOOLEAN_FLAGS.has(f) ? '' : ' <value>')).join(' ')}` : ''}`).join('\n')}\n\nGlobal options: --json, --fields a,b, --error-format json|text, --timeout <seconds>, --debug, --help.\nUse -- to end options; --name=value and --name value are supported.\nLists and search are free. Vacancy read, reveal and apply have separate allowances.\nCheck them with hirify account show. Apply sends a real application: ask the person first.\nLocal agents reuse saved sign-in when allowed. Token renewal needs write access.\nSandbox access denied: request permission for the command; do not copy tokens.\nHeadless/CI: HIRIFY_KEY or hirify auth --stdin. Manual login: --no-browser.\nSSH: forward the chosen --callback-port to the same loopback port on the CLI host.\nRules for your agent: npx skills add hirifyme/hirify-cli\n`
|
|
103
105
|
}
|
|
104
106
|
export function createOutput({ stdout = process.stdout, stderr = process.stderr, json = false, errorJSON = false, secrets = new Set() } = {}) {
|
|
105
107
|
let closed = false
|
package/bin/lib/commands.js
CHANGED
|
@@ -290,6 +290,36 @@ async function cmdVacancyReveal(args, words) {
|
|
|
290
290
|
})
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Hide vacancies or whole companies so they stop coming back in search and feeds. The same
|
|
295
|
+
* list the website's "Hide" button writes, so it works in both directions. Free and not
|
|
296
|
+
* limited; --undo brings them back. Up to 100 per call: the server takes them in one request.
|
|
297
|
+
*/
|
|
298
|
+
function printHidden(d, one, many, undo) {
|
|
299
|
+
const n = count(d.updated) ?? 0
|
|
300
|
+
const noun = n === 1 ? one : many
|
|
301
|
+
console.log(undo
|
|
302
|
+
? `${n} ${noun} unhidden.`
|
|
303
|
+
: `${n} ${noun} hidden. ${n === 1 ? 'It no longer appears' : 'They no longer appear'} in your search and feeds.`)
|
|
304
|
+
if (Array.isArray(d.not_found) && d.not_found.length) console.log(`not found: ${d.not_found.join(', ')}`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function cmdVacancyHide(args, words) {
|
|
308
|
+
if (!words.length) die('at least one vacancy slug is required: hirify vacancy hide <slug>...')
|
|
309
|
+
const undo = args.includes('--undo')
|
|
310
|
+
const body = await callCapability('vacancies.hide', { payload: { slugs: words, hidden: !undo } })
|
|
311
|
+
out(body, () => printHidden(body?.data ?? {}, 'vacancy', 'vacancies', undo))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function cmdCompanyHide(args, words) {
|
|
315
|
+
const vacancy = flag(args, '--vacancy')
|
|
316
|
+
if (!words.length && !vacancy) die('name the company, or pass one of its vacancies: hirify company hide "<name>" | --vacancy <slug>')
|
|
317
|
+
const undo = args.includes('--undo')
|
|
318
|
+
const payload = { hidden: !undo, ...(words.length ? { names: words } : {}), ...(vacancy ? { slugs: [vacancy] } : {}) }
|
|
319
|
+
const body = await callCapability('companies.hide', { payload })
|
|
320
|
+
out(body, () => printHidden(body?.data ?? {}, 'company', 'companies', undo))
|
|
321
|
+
}
|
|
322
|
+
|
|
293
323
|
/**
|
|
294
324
|
* Report a bug or ask for a feature. Free: it does not touch the reveal limit.
|
|
295
325
|
*
|
|
@@ -818,6 +848,7 @@ return {
|
|
|
818
848
|
intro: cmdIntro, skill: cmdSkill,
|
|
819
849
|
'account show': cmdAccountShow,
|
|
820
850
|
'vacancy search': cmdVacancySearch, 'vacancy read': cmdVacancyRead, 'vacancy reveal': cmdVacancyReveal, 'vacancy apply': cmdVacancyApply,
|
|
851
|
+
'vacancy hide': cmdVacancyHide, 'company hide': cmdCompanyHide,
|
|
821
852
|
'feed list': cmdFeedList, 'feed show': cmdFeedShow, 'feed create': cmdFeedCreate, 'feed deliver': cmdFeedDeliver,
|
|
822
853
|
'profile list': cmdProfileList, 'webhook list': cmdWebhookList, 'webhook create': cmdWebhookCreate,
|
|
823
854
|
'feedback send': cmdFeedbackSend, 'filter guide': cmdFilterGuide, 'api call': cmdApiCall,
|
package/bin/lib/store.js
CHANGED
|
@@ -7,6 +7,12 @@ import { CliError, aborted } from './errors.js'
|
|
|
7
7
|
import { DEFAULT_API, serverURL } from './config.js'
|
|
8
8
|
import { delay } from './http.js'
|
|
9
9
|
|
|
10
|
+
function storageError(code, operation, error) {
|
|
11
|
+
// Never include OS error messages: they may contain private paths or user input.
|
|
12
|
+
const reason = ['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'EDQUOT', 'ENOENT', 'ENOTDIR', 'ERR_ACCESS_DENIED'].includes(error?.code) ? ` (${error.code})` : ''
|
|
13
|
+
const access = operation === 'read' ? 'read access' : 'write access'
|
|
14
|
+
return new CliError(code, `Could not ${operation} the saved sign-in${reason}. This command needs ${access} to the Hirify configuration directory. If it runs in an agent sandbox, request permission for this command through the agent. Otherwise, check directory permissions and available disk space. Do not copy tokens into the project or chat.`)
|
|
15
|
+
}
|
|
10
16
|
function stat(path) { try { return lstatSync(path) } catch (e) { if (e.code === 'ENOENT') return null; throw e } }
|
|
11
17
|
function check(path, directory = false) {
|
|
12
18
|
const s = stat(path)
|
|
@@ -28,13 +34,13 @@ export function createStore(config, { signal } = {}) {
|
|
|
28
34
|
const file = join(config.dir, 'auth.json')
|
|
29
35
|
const legacy = join(config.dir, 'key')
|
|
30
36
|
let compromised = false
|
|
31
|
-
let secured = false
|
|
32
37
|
async function ensure() {
|
|
33
38
|
try { mkdirSync(config.dir, { recursive: true, mode: 0o700 }); await makePrivate(config.dir, true, signal) }
|
|
34
|
-
catch (e) { if (signal?.aborted) throw signal.reason; if (e instanceof CliError) throw e; throw
|
|
39
|
+
catch (e) { if (signal?.aborted) throw signal.reason; if (e instanceof CliError) throw e; throw storageError('storage_unavailable', 'prepare', e) }
|
|
35
40
|
}
|
|
36
41
|
function readRaw() {
|
|
37
42
|
try {
|
|
43
|
+
check(config.dir, true)
|
|
38
44
|
const stateStat = check(file)
|
|
39
45
|
if (stateStat) {
|
|
40
46
|
const raw = readFileSync(file, 'utf8')
|
|
@@ -50,7 +56,7 @@ export function createStore(config, { signal } = {}) {
|
|
|
50
56
|
if (key) return { kind: 'key', issuer: DEFAULT_API, access_token: key, generation: `legacy:${createHash('sha256').update(key).digest('hex')}` }
|
|
51
57
|
}
|
|
52
58
|
return { kind: 'signed-out', generation: 'absent' }
|
|
53
|
-
} catch (e) { if (e instanceof CliError) throw e; throw
|
|
59
|
+
} catch (e) { if (e instanceof CliError) throw e; throw storageError('storage_unreadable', 'read', e) }
|
|
54
60
|
}
|
|
55
61
|
function read({ allowPending = false } = {}) {
|
|
56
62
|
const state = readRaw()
|
|
@@ -76,7 +82,7 @@ export function createStore(config, { signal } = {}) {
|
|
|
76
82
|
}
|
|
77
83
|
if (process.platform !== 'win32') { const dir = openSync(config.dir, 'r'); try { fsyncSync(dir) } finally { closeSync(dir) } }
|
|
78
84
|
if (check(legacy)) unlinkSync(legacy)
|
|
79
|
-
} catch (e) { if (e instanceof CliError) throw e; throw
|
|
85
|
+
} catch (e) { if (e instanceof CliError) throw e; throw storageError('storage_write_failed', 'save', e) }
|
|
80
86
|
finally { if (fd !== undefined) closeSync(fd); try { unlinkSync(temp) } catch {} }
|
|
81
87
|
}
|
|
82
88
|
async function locked(fn) {
|
|
@@ -87,7 +93,7 @@ export function createStore(config, { signal } = {}) {
|
|
|
87
93
|
while (!release) {
|
|
88
94
|
aborted(signal)
|
|
89
95
|
try { release = await lockfile.lock(file, { realpath: false, lockfilePath: lockPath, stale: 120000, update: 10000, onCompromised: () => { compromised = true } }) }
|
|
90
|
-
catch (e) { if (e.code !== 'ELOCKED') throw
|
|
96
|
+
catch (e) { if (e.code !== 'ELOCKED') throw storageError('storage_lock_failed', 'lock', e); await delay(40 + Math.random() * 80, signal) }
|
|
91
97
|
}
|
|
92
98
|
try { aborted(signal); return await fn() } finally { await release().catch(() => {}) }
|
|
93
99
|
}
|
|
@@ -113,6 +119,5 @@ export function createStore(config, { signal } = {}) {
|
|
|
113
119
|
return state.generation
|
|
114
120
|
})
|
|
115
121
|
}
|
|
116
|
-
|
|
117
|
-
return { file, read, readRaw, write, locked, commit, logout, beginLogin, secure }
|
|
122
|
+
return { file, read, readRaw, write, locked, commit, logout, beginLogin }
|
|
118
123
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hirify-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hirify-cli",
|
|
9
|
-
"version": "0.5.
|
|
9
|
+
"version": "0.5.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.5.
|
|
3
|
+
"version": "0.5.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"
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"build": "node scripts/build.mjs",
|
|
11
11
|
"test:package": "node scripts/verify-package.mjs",
|
|
12
12
|
"prepublishOnly": "node scripts/prepublish-check.mjs",
|
|
13
|
-
"test:install": "node scripts/install-update-smoke.mjs"
|
|
13
|
+
"test:install": "node scripts/install-update-smoke.mjs",
|
|
14
|
+
"test:codex": "node scripts/codex-sandbox-smoke.mjs"
|
|
14
15
|
},
|
|
15
16
|
"type": "module",
|
|
16
17
|
"dependencies": {
|
package/skills/hirify/SKILL.md
CHANGED
|
@@ -17,8 +17,9 @@ for reproducible work use an exact version and `HIRIFY_NO_AUTO_UPDATE=1`.
|
|
|
17
17
|
|
|
18
18
|
A person runs `hirify login` and confirms the printed link. The CLI tries a browser only when
|
|
19
19
|
appropriate. Do not run login for them. `--no-browser` is manual callback login, not device flow.
|
|
20
|
-
SSH needs `--callback-port` with a matching SSH local port forward.
|
|
21
|
-
|
|
20
|
+
SSH needs `--callback-port` with a matching SSH local port forward. Local agents reuse the person's
|
|
21
|
+
saved sign-in when permitted to access the same configuration directory. CI and separate containers
|
|
22
|
+
can use `HIRIFY_KEY` or `hirify auth --stdin`; never put a key in a command, log or chat.
|
|
22
23
|
`hirify auth status --json` identifies the local source. Environment keys override saved access;
|
|
23
24
|
logout is local and does not unset them. Use `hirify login --force` to replace expired access.
|
|
24
25
|
|
|
@@ -48,6 +49,7 @@ no permission. Sending, saving and configuring do.
|
|
|
48
49
|
3. `vacancy read <slug>` to assess fit from the full text rather than a headline.
|
|
49
50
|
4. `vacancy reveal <slug>` only for vacancies that still fit after reading.
|
|
50
51
|
5. Apply only after the person's approval and the rules below.
|
|
52
|
+
6. `vacancy hide` / `company hide` what the user rejected; free, unlimited.
|
|
51
53
|
|
|
52
54
|
## Searching
|
|
53
55
|
|
|
@@ -107,7 +109,13 @@ errors; do not combine it with debug if expecting one object. Exit 0 means succe
|
|
|
107
109
|
2 unsupported server manifest, 130/143 interruption. Read the stable error code and message.
|
|
108
110
|
|
|
109
111
|
- `interaction_required`: ask the person to sign in or configure a key; do not wait on browser login.
|
|
110
|
-
-
|
|
112
|
+
- Storage access failure: compare `doctor`, `version`, and `auth status` with the normal terminal.
|
|
113
|
+
Request permission for the specific command through the agent's approval mechanism. Reading a
|
|
114
|
+
current token needs read access; renewal needs write access. Do not treat storage errors as an
|
|
115
|
+
expired login, copy credentials into the workspace, broaden Windows ACLs, or disable the sandbox.
|
|
116
|
+
Codex on Windows may use a different account; WSL/containers do not share the Windows home.
|
|
117
|
+
- Authentication failure: inspect `auth status`; ask for `login --force` only when renewal requires
|
|
118
|
+
a new sign-in. Never expose a token.
|
|
111
119
|
- 403: permission or plan restriction. 429: pace or a named allowance; inspect `account show`.
|
|
112
120
|
- Network/TLS/proxy failure: fix connectivity or trust configuration; never disable TLS verification.
|
|
113
121
|
- `outcome_unknown`: a metered action or mutation may have succeeded. Check before repeating;
|