hirify-cli 0.5.0 → 0.5.1
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 +22 -0
- package/bin/lib/auth.js +0 -1
- package/bin/lib/cli.js +1 -1
- package/bin/lib/store.js +12 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/skills/hirify/SKILL.md +10 -3
package/README.md
CHANGED
|
@@ -121,6 +121,28 @@ Explicitly safe reads get bounded retries for transient network errors and 429/5
|
|
|
121
121
|
Feedback accepts `--idempotency-key` when the server advertises caller-keyed deduplication;
|
|
122
122
|
keep the same key for a deliberate retry. The CLI prints a generated request reference otherwise.
|
|
123
123
|
|
|
124
|
+
## Codex and other sandboxed agents
|
|
125
|
+
|
|
126
|
+
Sign in once from your normal terminal. A local agent can reuse that sign-in when it has access
|
|
127
|
+
to the same configuration directory. It does not need a separate API key.
|
|
128
|
+
Reading an unexpired session does not create files or change permissions. Renewing an OAuth
|
|
129
|
+
session requires write access so the rotated token can be saved safely.
|
|
130
|
+
|
|
131
|
+
A Windows Codex sandbox can run with a restricted token or a different Windows account. Success
|
|
132
|
+
in `cmd` therefore does not prove the sandbox can access the saved sign-in. Keep credentials in
|
|
133
|
+
their normal private directory. If a command is denied, let Codex request approval for that specific
|
|
134
|
+
command using its normal permission mechanism. Do not disable the whole sandbox, grant `Everyone`
|
|
135
|
+
access, copy `auth.json` into the project, or paste a token into chat.
|
|
136
|
+
Read permission alone will not support token renewal. If approval is unavailable, report the
|
|
137
|
+
restriction instead of repeatedly running login. WSL and containers have separate home directories;
|
|
138
|
+
a Windows login is not automatically shared with them.
|
|
139
|
+
|
|
140
|
+
Compare `hirify version`, `hirify doctor`, and `hirify auth status --json --error-format=json`
|
|
141
|
+
inside the agent and in the terminal. These commands do not print tokens. `storage_unreadable`
|
|
142
|
+
means reading was denied or failed; `storage_unavailable`, `storage_lock_failed`, and
|
|
143
|
+
`storage_write_failed` identify preparation, locking, or saving failures. An access failure does
|
|
144
|
+
not by itself mean the sign-in expired. See [Codex Windows sandbox documentation](https://learn.chatgpt.com/docs/windows/windows-sandbox).
|
|
145
|
+
|
|
124
146
|
## Configuration and diagnostics
|
|
125
147
|
|
|
126
148
|
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
|
@@ -99,7 +99,7 @@ export function parse(argv) {
|
|
|
99
99
|
}
|
|
100
100
|
export function help(topic = '') {
|
|
101
101
|
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`
|
|
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.\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
103
|
}
|
|
104
104
|
export function createOutput({ stdout = process.stdout, stderr = process.stderr, json = false, errorJSON = false, secrets = new Set() } = {}) {
|
|
105
105
|
let closed = false
|
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.1",
|
|
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.1",
|
|
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.1",
|
|
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
|
|
|
@@ -107,7 +108,13 @@ errors; do not combine it with debug if expecting one object. Exit 0 means succe
|
|
|
107
108
|
2 unsupported server manifest, 130/143 interruption. Read the stable error code and message.
|
|
108
109
|
|
|
109
110
|
- `interaction_required`: ask the person to sign in or configure a key; do not wait on browser login.
|
|
110
|
-
-
|
|
111
|
+
- Storage access failure: compare `doctor`, `version`, and `auth status` with the normal terminal.
|
|
112
|
+
Request permission for the specific command through the agent's approval mechanism. Reading a
|
|
113
|
+
current token needs read access; renewal needs write access. Do not treat storage errors as an
|
|
114
|
+
expired login, copy credentials into the workspace, broaden Windows ACLs, or disable the sandbox.
|
|
115
|
+
Codex on Windows may use a different account; WSL/containers do not share the Windows home.
|
|
116
|
+
- Authentication failure: inspect `auth status`; ask for `login --force` only when renewal requires
|
|
117
|
+
a new sign-in. Never expose a token.
|
|
111
118
|
- 403: permission or plan restriction. 429: pace or a named allowance; inspect `account show`.
|
|
112
119
|
- Network/TLS/proxy failure: fix connectivity or trust configuration; never disable TLS verification.
|
|
113
120
|
- `outcome_unknown`: a metered action or mutation may have succeeded. Check before repeating;
|