jobhunt-kit 0.3.0 → 0.5.0
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +58 -11
- package/bin/agents.mjs +66 -0
- package/bin/install-options.mjs +21 -18
- package/bin/jobhunt-kit.mjs +17 -6
- package/installer-assets/gitignore.txt +1 -0
- package/installer-assets/runtime-lock.json +56 -2
- package/installer-assets/workspace-lock.json +56 -2
- package/package.json +3 -1
- package/plugins/jobhunt-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/jobhunt-kit/.codex-plugin/plugin.json +1 -1
- package/plugins/jobhunt-kit/package-lock.json +56 -2
- package/plugins/jobhunt-kit/package.json +2 -1
- package/plugins/jobhunt-kit/references/cli.md +30 -2
- package/plugins/jobhunt-kit/references/harnesses.md +12 -0
- package/plugins/jobhunt-kit/references/privacy.md +24 -0
- package/plugins/jobhunt-kit/references/storage.md +11 -5
- package/plugins/jobhunt-kit/schemas/draft.schema.json +13 -0
- package/plugins/jobhunt-kit/schemas/finish.schema.json +11 -0
- package/plugins/jobhunt-kit/schemas/policy.schema.json +36 -0
- package/plugins/jobhunt-kit/schemas/profile.schema.json +80 -0
- package/plugins/jobhunt-kit/schemas/status.schema.json +12 -0
- package/plugins/jobhunt-kit/schemas/vacancy.schema.json +29 -0
- package/plugins/jobhunt-kit/scripts/commands.mjs +57 -11
- package/plugins/jobhunt-kit/scripts/extract-resume.mjs +5 -3
- package/plugins/jobhunt-kit/scripts/files.mjs +69 -0
- package/plugins/jobhunt-kit/scripts/maintenance.mjs +292 -0
- package/plugins/jobhunt-kit/scripts/profile.mjs +23 -20
- package/plugins/jobhunt-kit/scripts/resume.mjs +42 -38
- package/plugins/jobhunt-kit/scripts/setup.mjs +8 -10
- package/plugins/jobhunt-kit/scripts/tracker.mjs +188 -33
- package/plugins/jobhunt-kit/scripts/validation.mjs +30 -0
- package/plugins/jobhunt-kit/skills/job-apply/SKILL.md +4 -2
- package/plugins/jobhunt-kit/skills/job-resume/SKILL.md +3 -2
- package/plugins/jobhunt-kit/skills/job-track/SKILL.md +8 -0
- package/scripts/check-package.mjs +7 -0
- package/scripts/pack-smoke.mjs +53 -0
- package/tests/hardening.test.mjs +337 -0
- package/tests/installer.test.mjs +47 -2
- package/tests/tracker.test.mjs +3 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobhunt-kit-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jobhunt-kit-plugin",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.5.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
+
"ajv": "8.20.0",
|
|
11
12
|
"hirify-cli": "0.4.5",
|
|
12
13
|
"mammoth": "1.12.2",
|
|
13
14
|
"pdf-parse": "2.4.5"
|
|
@@ -224,6 +225,22 @@
|
|
|
224
225
|
"node": ">=10.0.0"
|
|
225
226
|
}
|
|
226
227
|
},
|
|
228
|
+
"node_modules/ajv": {
|
|
229
|
+
"version": "8.20.0",
|
|
230
|
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
|
231
|
+
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
|
232
|
+
"license": "MIT",
|
|
233
|
+
"dependencies": {
|
|
234
|
+
"fast-deep-equal": "^3.1.3",
|
|
235
|
+
"fast-uri": "^3.0.1",
|
|
236
|
+
"json-schema-traverse": "^1.0.0",
|
|
237
|
+
"require-from-string": "^2.0.2"
|
|
238
|
+
},
|
|
239
|
+
"funding": {
|
|
240
|
+
"type": "github",
|
|
241
|
+
"url": "https://github.com/sponsors/epoberezkin"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
227
244
|
"node_modules/argparse": {
|
|
228
245
|
"version": "1.0.10",
|
|
229
246
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
|
@@ -280,6 +297,28 @@
|
|
|
280
297
|
"underscore": "^1.13.1"
|
|
281
298
|
}
|
|
282
299
|
},
|
|
300
|
+
"node_modules/fast-deep-equal": {
|
|
301
|
+
"version": "3.1.3",
|
|
302
|
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
303
|
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
|
304
|
+
"license": "MIT"
|
|
305
|
+
},
|
|
306
|
+
"node_modules/fast-uri": {
|
|
307
|
+
"version": "3.1.7",
|
|
308
|
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
|
|
309
|
+
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
|
|
310
|
+
"funding": [
|
|
311
|
+
{
|
|
312
|
+
"type": "github",
|
|
313
|
+
"url": "https://github.com/sponsors/fastify"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"type": "opencollective",
|
|
317
|
+
"url": "https://opencollective.com/fastify"
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"license": "BSD-3-Clause"
|
|
321
|
+
},
|
|
283
322
|
"node_modules/hirify-cli": {
|
|
284
323
|
"version": "0.4.5",
|
|
285
324
|
"resolved": "https://registry.npmjs.org/hirify-cli/-/hirify-cli-0.4.5.tgz",
|
|
@@ -310,6 +349,12 @@
|
|
|
310
349
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
|
311
350
|
"license": "MIT"
|
|
312
351
|
},
|
|
352
|
+
"node_modules/json-schema-traverse": {
|
|
353
|
+
"version": "1.0.0",
|
|
354
|
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
|
355
|
+
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
|
356
|
+
"license": "MIT"
|
|
357
|
+
},
|
|
313
358
|
"node_modules/jszip": {
|
|
314
359
|
"version": "3.10.1",
|
|
315
360
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
|
@@ -440,6 +485,15 @@
|
|
|
440
485
|
"util-deprecate": "~1.0.1"
|
|
441
486
|
}
|
|
442
487
|
},
|
|
488
|
+
"node_modules/require-from-string": {
|
|
489
|
+
"version": "2.0.2",
|
|
490
|
+
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
|
491
|
+
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
|
492
|
+
"license": "MIT",
|
|
493
|
+
"engines": {
|
|
494
|
+
"node": ">=0.10.0"
|
|
495
|
+
}
|
|
496
|
+
},
|
|
443
497
|
"node_modules/safe-buffer": {
|
|
444
498
|
"version": "5.1.2",
|
|
445
499
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobhunt-kit-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.0.0"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
+
"ajv": "8.20.0",
|
|
10
11
|
"hirify-cli": "0.4.5",
|
|
11
12
|
"mammoth": "1.12.2",
|
|
12
13
|
"pdf-parse": "2.4.5"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Команды рабочих сценариев
|
|
2
2
|
|
|
3
|
-
Установка: `npx jobhunt-kit install [папка]` с выбором
|
|
3
|
+
Установка: `npx jobhunt-kit install [папка]` с выбором агентов и области.
|
|
4
|
+
Список: `npx jobhunt-kit agents`. Все для выбранной области: `--agents all`.
|
|
5
|
+
`--providers` поддерживается как синоним `--agents`.
|
|
4
6
|
Без вопросов: `npx jobhunt-kit install --agents codex,claude --scope project`.
|
|
5
7
|
Глобально: `npx jobhunt-kit install --agents claude --scope global` (без пути).
|
|
6
8
|
В глобальном режиме устанавливаются только навыки; зависимости и данные создаёт
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
| job-resume | Копия по SHA-256, извлечение PDF/DOCX/TXT/MD, простые проверки текста, сохранение отчёта, запись завершённой проверки | Визуальная оценка, истинность фактов, соответствие роли и содержательные правки |
|
|
24
26
|
| job-search | Сбор профиля, бюджетов и известных вакансий; журнал запусков/результатов, запрет второго активного запуска через CLI | Актуальные фильтры Hirify, preview, живой поиск, оценка требований и ранжирование |
|
|
25
27
|
| job-apply | Снимок черновика, экспорт текста/ответов, согласование, резервирование и однократный вызов CLI, запись исхода | Написание письма из фактов, проверка живого профиля/квоты, получение разрешения, разбор ответа сервиса |
|
|
26
|
-
| job-track | Списки/история/статусы/Markdown
|
|
28
|
+
| job-track | Списки/история/статусы/Markdown, integrity/recovery, backup/restore, privacy-команды; заполнение prompt для расписания | Интерпретация сообщения кандидата, создание задачи в планировщике после запроса |
|
|
27
29
|
|
|
28
30
|
Технические функции: `profile.mjs`, `resume.mjs`, `extract-resume.mjs`, `commands.mjs`;
|
|
29
31
|
история и защита отправки переиспользуют `tracker.mjs` и `send-packet.mjs`.
|
|
@@ -127,3 +129,29 @@ schedule.json: `{"frequency":"По будням в 09:00","timezone":"Europe/Ber
|
|
|
127
129
|
materials/scheduled-search.md, а не включённая задача; auto-разрешение не выдаётся.
|
|
128
130
|
Нужен --workspace с установленным шаблоном, чтобы prompt не ссылался на временный
|
|
129
131
|
кеш npx. Планировщик выбирает агент по явному запросу пользователя.
|
|
132
|
+
|
|
133
|
+
## Целостность, резервирование и приватность
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
npx jobhunt-kit doctor
|
|
137
|
+
npx jobhunt-kit recover inspect
|
|
138
|
+
npx jobhunt-kit recover apply --input recovery.json
|
|
139
|
+
npx jobhunt-kit recover packet <attempt_id>
|
|
140
|
+
npx jobhunt-kit backup ../jobhunt-backup
|
|
141
|
+
npx jobhunt-kit backup-verify ../jobhunt-backup
|
|
142
|
+
npx jobhunt-kit restore --input restore.json
|
|
143
|
+
npx jobhunt-kit privacy map
|
|
144
|
+
npx jobhunt-kit privacy export ../jobhunt-private-export
|
|
145
|
+
npx jobhunt-kit privacy redact --input redact.json
|
|
146
|
+
npx jobhunt-kit privacy purge --input purge.json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`doctor` проверяет JSON Schema, SQLite integrity/foreign keys, активный run,
|
|
150
|
+
неразрешённые attempts и незавершённые файловые операции. Recovery ничего не угадывает:
|
|
151
|
+
для run нужны id/status/reason, для attempt — slug/outcome/evidence.
|
|
152
|
+
|
|
153
|
+
Restore требует `{"source":"...","confirmation":"RESTORE VERIFIED JOBHUNT BACKUP"}`
|
|
154
|
+
и перед заменой создаёт автоматический сырой pre-restore quarantine-снимок. Redact требует
|
|
155
|
+
`REDACT LOCAL JOBHUNT PII` и массив dotted profile fields. Purge требует фразу
|
|
156
|
+
`PURGE LOCAL JOBHUNT DATA`, scopes и отдельный backup_directory; удаление без
|
|
157
|
+
проверяемой резервной копии не выполняется.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Установка в агенты
|
|
2
|
+
|
|
3
|
+
Пути сверены с [каталогом провайдеров Impeccable](https://github.com/pbakaus/impeccable/blob/main/crates/skills/src/providers.rs)
|
|
4
|
+
и [его README](https://github.com/pbakaus/impeccable#installation), 2026-09-08.
|
|
5
|
+
Для Veto README описывает только глобальный каталог.
|
|
6
|
+
Для Copilot глобальный путь `.copilot/skills` соответствует
|
|
7
|
+
[документации GitHub](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills).
|
|
8
|
+
|
|
9
|
+
Установщик копирует переносимый SKILL.md и ресурсы. Он не меняет настройки
|
|
10
|
+
доверия, не устанавливает сами приложения и не запускает их. Автоматические тесты
|
|
11
|
+
проверяют все пути, комплектность файлов и конфликты; загрузка навыка в каждом
|
|
12
|
+
из приложений отдельно не проверена.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Локальные данные и приватность
|
|
2
|
+
|
|
3
|
+
Jobhunt Kit не шифрует данные. Рабочая папка `D` должна быть доступна только
|
|
4
|
+
пользователю и не должна синхронизироваться или публиковаться без его решения.
|
|
5
|
+
|
|
6
|
+
| Путь | Возможные персональные данные | Назначение |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `profile.json` | Имя, контакты, опыт, ответы, разрешения на работу | Авторитетный профиль кандидата |
|
|
9
|
+
| `policy.json` | Текст auto-разрешения, срок и лимит | Авторитетная политика отправки |
|
|
10
|
+
| `history.sqlite` | Вакансии, письма, ответы, approvals, application snapshots; legacy v1 мог хранить полные снимки профиля | Авторитетная история |
|
|
11
|
+
| `resumes/` | Неизменяемые копии резюме | Проверка и отправочный контекст |
|
|
12
|
+
| `materials/` | Извлечённый текст, письма, ответы, attempt packets | Рабочие материалы агента |
|
|
13
|
+
| `reports/` | Названия вакансий, ссылки и статусы | Производное представление истории |
|
|
14
|
+
|
|
15
|
+
`privacy map` возвращает абсолютные пути этой карты. `privacy export` создаёт ту же
|
|
16
|
+
проверяемую копию, что backup. `privacy redact` меняет перечисленные dotted-поля
|
|
17
|
+
профиля, сбрасывает подтверждение и удаляет полные profile snapshots/user authorization
|
|
18
|
+
из legacy events; для полного удаления истории использовать purge scope `history`.
|
|
19
|
+
|
|
20
|
+
`privacy purge` требует точную фразу подтверждения и отдельный `backup_directory`.
|
|
21
|
+
До удаления создаётся и проверяется полная резервная копия. Scopes: `profile`,
|
|
22
|
+
`history`, `resumes`, `materials`, `reports`. Очистка `history` всегда очищает и
|
|
23
|
+
производные `reports`, отзывает auto-разрешение и подтверждение профиля. Purge не удаляет внешний файл, из которого ранее
|
|
24
|
+
импортировали резюме, и не отзывает уже отправленные заявления на стороне Hirify.
|
|
@@ -30,7 +30,8 @@ P/D — абсолютные пути из workflow.md. JSON-ответы ком
|
|
|
30
30
|
Дата confirmed_at — ISO 8601. Profile-команда отдаёт hash текущего JSON для разрешения
|
|
31
31
|
auto. Хеш зависит от сериализации объекта: изменение/перестановка ключей может
|
|
32
32
|
потребовать нового согласования (консервативное поведение).
|
|
33
|
-
resume.path — путь
|
|
33
|
+
resume.path — только относительный путь внутри D, штатно `resumes/<sha>.<ext>`;
|
|
34
|
+
внешний файл сначала импортируется командой resume. sha256 — хеш байтов файла.
|
|
34
35
|
Пример вычисления в PowerShell: `(Get-FileHash <file> -Algorithm SHA256).Hash.ToLower()`.
|
|
35
36
|
readiness и confirmed — утверждения агента по фактической проверке, не автоматическая
|
|
36
37
|
проверка истинности биографии. Не отмечать их только ради прохождения команды.
|
|
@@ -109,12 +110,17 @@ External или недостающие ответы → needs_user. После
|
|
|
109
110
|
можно dismissed. После отправки: interview / rejected / withdrawn / offer;
|
|
110
111
|
из offer: accepted / declined / withdrawn. Status — локальная запись, не действие сервиса.
|
|
111
112
|
|
|
112
|
-
Схема SQLite
|
|
113
|
+
Схема SQLite v2: runs, jobs, aliases, observations, attempts, dispatches, events,
|
|
114
|
+
file_revisions и pending_file_writes. Миграция v1→v2 сохраняет данные, закрывает
|
|
115
|
+
старые дублирующиеся running runs, записывается в schema_migrations и добавляет
|
|
116
|
+
immutable application snapshots.
|
|
113
117
|
В attempts UNIQUE(job_id), в dispatches PRIMARY KEY(attempt_id): повтор не проходит
|
|
114
118
|
после перезапуска процесса. Сбой после намерения требует ручного разбора.
|
|
115
119
|
Нет универсальной транзакции между SQLite и Hirify: невозможно обещать exactly-once
|
|
116
120
|
доставку через сеть. Здесь предотвращаются автоматические повторные отправки.
|
|
117
121
|
|
|
118
|
-
Историю читать через команды. Для резервирования
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
Историю читать через команды. Для резервирования использовать backup и backup-verify.
|
|
123
|
+
Restore сначала создаёт сырой автоматический pre-restore quarantine-снимок, не требующий
|
|
124
|
+
валидности текущих данных. Перед активацией восстановленной истории restore отзывает
|
|
125
|
+
auto-разрешение и подтверждение профиля. Doctor проверяет JSON Schema,
|
|
126
|
+
SQLite, foreign keys, незавершённые runs/attempts и журнал атомарных файловых операций.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "jobhunt-kit/draft",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["slug", "cover_letter", "hirify_profile_id", "claims", "answers", "unresolved"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"slug": { "type": "string", "minLength": 1 }, "cover_letter": { "type": "string", "minLength": 1 },
|
|
8
|
+
"hirify_profile_id": { "type": ["integer", "null"], "minimum": 1 },
|
|
9
|
+
"claims": { "type": "array", "items": { "type": "object", "required": ["text", "evidence_id"], "properties": { "text": { "type": "string", "minLength": 1 }, "evidence_id": { "type": "string", "minLength": 1 } }, "additionalProperties": false } },
|
|
10
|
+
"answers": { "type": "array" }, "unresolved": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
11
|
+
},
|
|
12
|
+
"additionalProperties": true
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "jobhunt-kit/finish",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["attempt_id", "outcome", "evidence"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"attempt_id": { "type": "string", "minLength": 1 }, "outcome": { "enum": ["submitted", "failed", "unknown"] },
|
|
8
|
+
"evidence": { "type": "string", "minLength": 1 }, "application_id": { "type": "string", "minLength": 1 }
|
|
9
|
+
},
|
|
10
|
+
"additionalProperties": true
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "jobhunt-kit/policy",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema_version", "mode", "search", "auto"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema_version": { "const": 1 },
|
|
8
|
+
"mode": { "enum": ["review_each", "auto"] },
|
|
9
|
+
"search": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["target", "max_cards", "max_reads", "max_reveals", "max_minutes"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"target": { "type": "integer", "minimum": 1 }, "max_cards": { "type": "integer", "minimum": 1 },
|
|
14
|
+
"max_reads": { "type": "integer", "minimum": 1 }, "max_reveals": { "type": "integer", "minimum": 0 },
|
|
15
|
+
"max_minutes": { "type": "integer", "minimum": 1 }
|
|
16
|
+
},
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
},
|
|
19
|
+
"auto": {
|
|
20
|
+
"anyOf": [
|
|
21
|
+
{ "type": "null" },
|
|
22
|
+
{
|
|
23
|
+
"type": "object", "required": ["user_authorization", "profile_hash", "expires_at", "max_attempts_per_utc_day"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"user_authorization": { "type": "string", "minLength": 1 },
|
|
26
|
+
"profile_hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
27
|
+
"expires_at": { "type": "string", "format": "date-time" },
|
|
28
|
+
"max_attempts_per_utc_day": { "type": "integer", "minimum": 1 }
|
|
29
|
+
},
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "jobhunt-kit/profile",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema_version", "confirmed_at", "identity", "search", "languages", "experience", "education", "skills", "achievements", "evidence", "answers", "availability", "writing", "hirify_profile_id", "resume"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema_version": { "const": 1 },
|
|
8
|
+
"confirmed_at": { "type": ["string", "null"], "format": "date-time" },
|
|
9
|
+
"identity": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["name", "email", "phone", "city", "country", "timezone", "contact_links"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"name": { "type": ["string", "null"] }, "email": { "type": ["string", "null"] },
|
|
14
|
+
"phone": { "type": ["string", "null"] }, "city": { "type": ["string", "null"] },
|
|
15
|
+
"country": { "type": ["string", "null"] }, "timezone": { "type": ["string", "null"] },
|
|
16
|
+
"contact_links": { "type": "array", "items": { "type": "string" } }
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
},
|
|
20
|
+
"search": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["roles", "levels", "countries", "work_modes", "employment_types", "industries", "excluded_companies", "excluded_keywords", "work_authorization", "hard_constraints", "preferences", "feed_ids", "queries", "salary"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"roles": { "$ref": "#/$defs/strings" }, "levels": { "$ref": "#/$defs/strings" },
|
|
25
|
+
"countries": { "$ref": "#/$defs/strings" }, "work_modes": { "$ref": "#/$defs/strings" },
|
|
26
|
+
"employment_types": { "$ref": "#/$defs/strings" }, "industries": { "$ref": "#/$defs/strings" },
|
|
27
|
+
"excluded_companies": { "$ref": "#/$defs/strings" }, "excluded_keywords": { "$ref": "#/$defs/strings" },
|
|
28
|
+
"work_authorization": { "$ref": "#/$defs/strings" }, "hard_constraints": { "$ref": "#/$defs/strings" },
|
|
29
|
+
"preferences": { "$ref": "#/$defs/strings" }, "feed_ids": { "type": "array", "items": { "type": ["string", "integer"] } },
|
|
30
|
+
"queries": { "$ref": "#/$defs/strings" },
|
|
31
|
+
"relocation": {}, "travel": {}, "working_hours": {}, "sponsorship": {},
|
|
32
|
+
"unknown_salary": { "type": "string" }, "unknown_critical_requirement": { "type": "string" },
|
|
33
|
+
"salary": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["min", "target", "currency", "period", "tax_basis"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"min": { "type": ["number", "null"], "minimum": 0 }, "target": { "type": ["number", "null"], "minimum": 0 },
|
|
38
|
+
"currency": { "type": ["string", "null"] }, "period": { "type": ["string", "null"] }, "tax_basis": { "type": ["string", "null"] }
|
|
39
|
+
},
|
|
40
|
+
"additionalProperties": false
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"additionalProperties": false
|
|
44
|
+
},
|
|
45
|
+
"languages": { "type": "array", "items": { "$ref": "#/$defs/object" } },
|
|
46
|
+
"experience": { "type": "array", "items": { "$ref": "#/$defs/evidenced" } },
|
|
47
|
+
"education": { "type": "array", "items": { "$ref": "#/$defs/evidenced" } },
|
|
48
|
+
"skills": { "type": "array", "items": { "$ref": "#/$defs/evidenced" } },
|
|
49
|
+
"achievements": { "type": "array", "items": { "$ref": "#/$defs/evidenced" } },
|
|
50
|
+
"evidence": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": {
|
|
53
|
+
"type": "object", "required": ["id", "text", "source", "confirmed"],
|
|
54
|
+
"properties": { "id": { "type": "string", "minLength": 1 }, "text": { "type": "string", "minLength": 1 }, "source": { "type": "string", "minLength": 1 }, "confirmed": { "type": "boolean" } },
|
|
55
|
+
"additionalProperties": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"answers": { "type": "array", "items": { "$ref": "#/$defs/evidenced" } },
|
|
59
|
+
"availability": { "$ref": "#/$defs/object" }, "writing": { "$ref": "#/$defs/object" },
|
|
60
|
+
"hirify_profile_id": { "type": ["integer", "null"], "minimum": 1 },
|
|
61
|
+
"resume": {
|
|
62
|
+
"type": "object", "required": ["path", "sha256", "review_status"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"path": { "type": ["string", "null"] }, "sha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" },
|
|
65
|
+
"review_status": { "enum": ["not_reviewed", "needs_changes", "ready"] }
|
|
66
|
+
},
|
|
67
|
+
"additionalProperties": false
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"$defs": {
|
|
71
|
+
"strings": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
72
|
+
"object": { "type": "object", "additionalProperties": true },
|
|
73
|
+
"evidenced": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": { "evidence_ids": { "type": "array", "items": { "type": "string", "minLength": 1 } } },
|
|
76
|
+
"additionalProperties": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"additionalProperties": false
|
|
80
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "jobhunt-kit/status",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["slug", "status", "note"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"slug": { "type": "string", "minLength": 1 },
|
|
8
|
+
"status": { "enum": ["dismissed", "needs_user", "submitted_external", "interview", "rejected", "withdrawn", "offer", "accepted", "declined"] },
|
|
9
|
+
"note": { "type": "string", "minLength": 1 }
|
|
10
|
+
},
|
|
11
|
+
"additionalProperties": true
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "jobhunt-kit/vacancy",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["run_id", "slug", "title", "url", "route", "match"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"run_id": { "type": "string", "minLength": 1 }, "slug": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$" },
|
|
8
|
+
"title": { "type": "string", "minLength": 1 }, "company": { "type": ["string", "null"] },
|
|
9
|
+
"url": { "type": "string", "format": "uri" }, "original_url": { "type": "string", "format": "uri" },
|
|
10
|
+
"apply_url": { "type": "string", "format": "uri" }, "route": { "enum": ["hosted", "external", "unknown"] },
|
|
11
|
+
"description": { "type": "string" }, "read_at": { "type": "string", "format": "date-time" },
|
|
12
|
+
"match": {
|
|
13
|
+
"type": "object", "required": ["verdict"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"verdict": { "enum": ["unreviewed", "suitable", "review", "rejected"] },
|
|
16
|
+
"reasons": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
17
|
+
"requirements": {
|
|
18
|
+
"type": "array", "items": {
|
|
19
|
+
"type": "object", "required": ["requirement", "result", "evidence"],
|
|
20
|
+
"properties": { "requirement": { "type": "string", "minLength": 1 }, "result": { "enum": ["pass", "fail", "unknown"] }, "evidence": { "type": "string" } },
|
|
21
|
+
"additionalProperties": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"additionalProperties": true
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"additionalProperties": true
|
|
29
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
-
import { readFileSync,
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { resolve, join, dirname, basename } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
@@ -8,27 +8,36 @@ import { readJSON, writeJSON, validateProfile, saveProfile, confirmProfile } fro
|
|
|
8
8
|
import { importResume, checkResume, recordResumeReview } from './resume.mjs';
|
|
9
9
|
import { cliPath } from './hirify.mjs';
|
|
10
10
|
import { sendPacket } from './send-packet.mjs';
|
|
11
|
+
import { backupData, privacyMap, purgeData, redactData, restoreData, verifyBackup } from './maintenance.mjs';
|
|
12
|
+
import { atomicWrite, atomicWriteJSON, localPath } from './files.mjs';
|
|
13
|
+
import { validateSchema } from './validation.mjs';
|
|
11
14
|
|
|
12
15
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
16
|
const required = (value, name) => { if (!value) throw new Error(`${name} required`); return value; };
|
|
14
17
|
function using(dir, fn) { const s = new Tracker(dir); try { return fn(s); } finally { s.close(); } }
|
|
18
|
+
function persistAttemptPacket(dir, store, attemptId) {
|
|
19
|
+
const packet = store.attemptPacket(attemptId);
|
|
20
|
+
const path = localPath(dir, `materials/attempt-${attemptId}.json`);
|
|
21
|
+
atomicWriteJSON(path, packet, { base: dir });
|
|
22
|
+
return { packet_path: path, attempt_id: attemptId, sent: false };
|
|
23
|
+
}
|
|
15
24
|
function exportDraft(dir, job) {
|
|
16
25
|
if (!job.draft) throw new Error('Prepare a draft first');
|
|
17
26
|
const version = createHash('sha256').update(JSON.stringify(job.draft)).digest('hex').slice(0, 16);
|
|
18
27
|
const folder = join(dir, 'materials', `application-${job.id}-${version}`);
|
|
19
28
|
mkdirSync(folder, { recursive: true });
|
|
20
|
-
|
|
29
|
+
atomicWrite(join(folder, 'cover-letter.txt'), job.draft.cover_letter, { base: dir });
|
|
21
30
|
writeJSON(join(folder, 'answers.json'), job.draft.answers || []);
|
|
22
31
|
writeJSON(join(folder, 'draft.json'), job.draft);
|
|
23
32
|
const destination = job.payload.apply_url || job.payload.original_url || job.payload.url;
|
|
24
|
-
|
|
33
|
+
atomicWrite(join(folder, 'handoff.md'), [
|
|
25
34
|
'# Материалы отклика', '', `Вакансия: ${job.payload.title}`, `Ссылка: ${destination}`, '',
|
|
26
35
|
`Маршрут: ${job.payload.route}. Статус: ${job.status}.`,
|
|
27
36
|
'Письмо: cover-letter.txt. Ответы: answers.json. Полный снимок: draft.json.', '',
|
|
28
37
|
'## Требует действия', ...(job.draft.unresolved || []).map(x => `- ${x}`), '',
|
|
29
38
|
job.payload.route === 'external' ? 'Отклик отправляет пользователь. Если ссылка на форму ещё не раскрыта, сначала получить её через Hirify reveal.' : 'Перед отправкой проверить профиль Hirify и текущую квоту; требуется действующее согласование.',
|
|
30
39
|
'', 'Экспорт файлов не отправляет отклик.'
|
|
31
|
-
].join('\n'));
|
|
40
|
+
].join('\n'), { base: dir });
|
|
32
41
|
return { directory: folder, handoff: join(folder, 'handoff.md'), sent: false };
|
|
33
42
|
}
|
|
34
43
|
function searchContext(dir, workspace) {
|
|
@@ -37,6 +46,7 @@ function searchContext(dir, workspace) {
|
|
|
37
46
|
const validation = validateProfile(p.profile);
|
|
38
47
|
if (!validation.valid) throw new Error(validation.errors.join('; '));
|
|
39
48
|
const policy = readJSON(join(dir, 'policy.json'));
|
|
49
|
+
validateSchema('policy', policy);
|
|
40
50
|
for (const k of ['target', 'max_cards', 'max_reads', 'max_reveals', 'max_minutes']) {
|
|
41
51
|
if (!Number.isInteger(policy.search?.[k]) || policy.search[k] < (k === 'max_reveals' ? 0 : 1)) throw new Error(`Invalid search budget: ${k}`);
|
|
42
52
|
}
|
|
@@ -50,7 +60,7 @@ function searchContext(dir, workspace) {
|
|
|
50
60
|
const task = join(dir, 'materials', 'search-task.md');
|
|
51
61
|
const localPlugin = join(workspace, 'plugins', 'jobhunt-kit');
|
|
52
62
|
const plugin = existsSync(join(localPlugin, 'skills', 'job-search', 'SKILL.md')) ? localPlugin : ROOT;
|
|
53
|
-
|
|
63
|
+
atomicWrite(task, `Use job-search at ${join(plugin, 'skills', 'job-search', 'SKILL.md')}.\nRead ${path} and ${join(dir, 'profile.json')}.\nCheck live Hirify quotas and filter guide, preview filters, then search. Record runs and observations using the CLI.\nNo live search has been performed by this context command.\n`, { base: dir });
|
|
54
64
|
return { ...context, context_path: path, task_path: task };
|
|
55
65
|
});
|
|
56
66
|
}
|
|
@@ -69,7 +79,7 @@ function scheduleTemplate(dir, workspace, input) {
|
|
|
69
79
|
'<абсолютный workspace>': workspace, '<абсолютный путь плагина>': plugin,
|
|
70
80
|
'<абсолютный путь локальных данных>': dir, '<search / prepare / auto>': input.mode })) prompt = prompt.replaceAll(key, String(value));
|
|
71
81
|
const path = join(dir, 'materials', 'scheduled-search.md');
|
|
72
|
-
|
|
82
|
+
atomicWrite(path, prompt, { base: dir });
|
|
73
83
|
return { path, scheduled: false, auto_permission_granted: false, next: 'Review prompt and create a task using your agent scheduler' };
|
|
74
84
|
}
|
|
75
85
|
export function runCommand(args, { cwd = process.cwd(), transport } = {}) {
|
|
@@ -85,12 +95,29 @@ export function runCommand(args, { cwd = process.cwd(), transport } = {}) {
|
|
|
85
95
|
let cli; try { cli = { available: true, path: cliPath(dir), version: '0.4.5' }; }
|
|
86
96
|
catch (e) { cli = { available: false, reason: e.message }; }
|
|
87
97
|
const initialized = existsSync(join(dir, 'profile.json'));
|
|
98
|
+
let integrity = null;
|
|
99
|
+
let profile = null;
|
|
100
|
+
if (initialized) {
|
|
101
|
+
try { integrity = using(dir, store => store.integrity()); }
|
|
102
|
+
catch (error) { integrity = { database_ok: false, error: error.message }; }
|
|
103
|
+
try { profile = validateProfile(readJSON(join(dir, 'profile.json'))); }
|
|
104
|
+
catch (error) { profile = { valid: false, errors: [error.message], warnings: [] }; }
|
|
105
|
+
}
|
|
88
106
|
return { node: process.versions.node, workspace, data: dir, initialized, cli,
|
|
89
|
-
profile
|
|
107
|
+
profile, integrity, network_checked: false,
|
|
90
108
|
next: initialized ? 'profile check' : 'profile init' };
|
|
91
109
|
}
|
|
92
110
|
if (command === 'profile' && argument) throw new Error('Unexpected profile argument; use --input or --note');
|
|
93
111
|
if (command === 'profile' && action === 'init') return initData(dir);
|
|
112
|
+
if (command === 'backup-verify') {
|
|
113
|
+
if (!action || argument) throw new Error('backup-verify requires one backup directory');
|
|
114
|
+
return verifyBackup(resolve(cwd, action));
|
|
115
|
+
}
|
|
116
|
+
if (command === 'restore') {
|
|
117
|
+
if (action || argument) throw new Error('restore uses --input with source and confirmation');
|
|
118
|
+
const request = input();
|
|
119
|
+
return restoreData(dir, resolve(cwd, required(request.source, 'source')), request.confirmation);
|
|
120
|
+
}
|
|
94
121
|
if (!existsSync(join(dir, 'profile.json'))) throw new Error(`No profile at ${dir}. Run profile init first.`);
|
|
95
122
|
if (command === 'profile') {
|
|
96
123
|
if (argument) throw new Error('Unexpected profile argument; use --input or --note');
|
|
@@ -115,7 +142,6 @@ export function runCommand(args, { cwd = process.cwd(), transport } = {}) {
|
|
|
115
142
|
const methods = { start: 'runStart', event: 'runEvent', record: 'put', finish: 'runFinish' };
|
|
116
143
|
if (!methods[action]) throw new Error('search: plan | start | event | record | finish (JSON via --input)');
|
|
117
144
|
return using(dir, s => {
|
|
118
|
-
if (action === 'start' && s.runs().some(r => r.status === 'running')) throw new Error('Finish or resolve the previous running search before starting another');
|
|
119
145
|
return s[methods[action]](input());
|
|
120
146
|
});
|
|
121
147
|
}
|
|
@@ -125,9 +151,8 @@ export function runCommand(args, { cwd = process.cwd(), transport } = {}) {
|
|
|
125
151
|
if (action === 'export') return using(dir, s => exportDraft(dir, s.job(required(argument, 'slug'))));
|
|
126
152
|
if (action === 'begin') return using(dir, s => {
|
|
127
153
|
const packet = s.begin({ slug: required(argument, 'slug') });
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return { packet_path: path, attempt_id: packet.attempt_id, sent: false };
|
|
154
|
+
try { return persistAttemptPacket(dir, s, packet.attempt_id); }
|
|
155
|
+
catch (error) { throw new Error(`Attempt reserved but packet write failed. Recover with: recover packet ${packet.attempt_id}. ${error.message}`); }
|
|
131
156
|
});
|
|
132
157
|
const methods = { prepare: 'prepare', approve: 'approve', finish: 'finish', resolve: 'resolveUnknown' };
|
|
133
158
|
if (methods[action]) return using(dir, s => s[methods[action]](input()));
|
|
@@ -146,6 +171,27 @@ export function runCommand(args, { cwd = process.cwd(), transport } = {}) {
|
|
|
146
171
|
if (action) throw new Error(`${command} takes no positional arguments`);
|
|
147
172
|
return using(dir, s => s[command]());
|
|
148
173
|
}
|
|
174
|
+
if (command === 'backup') {
|
|
175
|
+
if (!action || argument) throw new Error('backup requires one destination directory');
|
|
176
|
+
return backupData(dir, resolve(cwd, action));
|
|
177
|
+
}
|
|
178
|
+
if (command === 'recover') {
|
|
179
|
+
if (!action || action === 'inspect') return using(dir, store => store.integrity());
|
|
180
|
+
if (action === 'apply') return using(dir, store => store.recover(input()));
|
|
181
|
+
if (action === 'packet') return using(dir, store => persistAttemptPacket(dir, store, required(argument, 'attempt_id')));
|
|
182
|
+
throw new Error('recover: inspect | apply --input file | packet <attempt_id>');
|
|
183
|
+
}
|
|
184
|
+
if (command === 'privacy') {
|
|
185
|
+
if (!action || action === 'map') return privacyMap(dir);
|
|
186
|
+
if (action === 'export') return backupData(dir, resolve(cwd, required(argument, 'destination')));
|
|
187
|
+
if (['redact', 'purge'].includes(action) && argument) throw new Error(`privacy ${action} uses --input`);
|
|
188
|
+
if (action === 'redact') return redactData(dir, input());
|
|
189
|
+
if (action === 'purge') {
|
|
190
|
+
const request = input();
|
|
191
|
+
return purgeData(dir, { ...request, backup_directory: resolve(cwd, required(request.backup_directory, 'backup_directory')) });
|
|
192
|
+
}
|
|
193
|
+
throw new Error('privacy: map | export <directory> | redact/purge --input file');
|
|
194
|
+
}
|
|
149
195
|
if (command === 'policy') {
|
|
150
196
|
if (argument) throw new Error('Unexpected policy argument');
|
|
151
197
|
if (!action || action === 'show') return readJSON(join(dir, 'policy.json'));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Parser subprocess: local bytes only, bounded by parent timeout/memory/output limits.
|
|
3
|
-
import {
|
|
4
|
-
import { extname, join, resolve } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import { localPath, MAX_RESUME_BYTES, readBytes } from './files.mjs';
|
|
6
7
|
|
|
7
8
|
try {
|
|
8
9
|
const [dir, file] = process.argv.slice(2);
|
|
@@ -10,7 +11,8 @@ try {
|
|
|
10
11
|
const bundled = createRequire(import.meta.url);
|
|
11
12
|
const runtime = existsSync(local) ? createRequire(local) : bundled;
|
|
12
13
|
const load = name => { try { return runtime(name); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; return bundled(name); } };
|
|
13
|
-
const
|
|
14
|
+
const safeFile = localPath(dir, relative(resolve(dir), resolve(file)));
|
|
15
|
+
const bytes = readBytes(safeFile, { base: resolve(dir), maxBytes: MAX_RESUME_BYTES });
|
|
14
16
|
let result;
|
|
15
17
|
if (extname(file).toLowerCase() === '.pdf') {
|
|
16
18
|
const { PDFParse } = load('pdf-parse');
|