careermate 0.0.4 → 0.0.6
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/AGENTS.md +11 -7
- package/INSTALL.md +18 -16
- package/README.md +28 -26
- package/THIRD_PARTY_NOTICES.md +2 -3
- package/bin/careermate.mjs +8 -1
- package/dist/career-os/README.md +8 -3
- package/dist/career-os/eop/career-description.md +136 -0
- package/dist/career-os/eop/cover-letter.md +7 -5
- package/dist/career-os/eop/fit-analysis.md +15 -5
- package/dist/career-os/eop/interview-prep.md +209 -21
- package/dist/career-os/eop/job-analysis.md +5 -3
- package/dist/career-os/eop/profile-extraction.md +5 -4
- package/dist/career-os/knowledge/cover-letter.md +4 -4
- package/dist/career-os/knowledge/fit-matching.md +8 -8
- package/dist/career-os/knowledge/interview-behavioral.md +76 -8
- package/dist/career-os/knowledge/interview-technical.md +106 -4
- package/dist/career-os/knowledge/linkedin-profile.md +1 -1
- package/dist/career-os/knowledge/networking-referrals.md +1 -1
- package/dist/career-os/knowledge/offer-evaluation-decision.md +10 -10
- package/dist/career-os/knowledge/portfolio.md +3 -3
- package/dist/career-os/knowledge/verifiers/ats-compat.md +8 -4
- package/dist/career-os/knowledge/verifiers/human-voice.md +2 -2
- package/dist/career-os/knowledge/verifiers/recency-staleness.md +1 -1
- package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +2 -2
- package/dist/career-os/knowledge/verifiers/truthfulness.md +1 -1
- package/dist/doctor.mjs +1019 -127
- package/dist/mcp.mjs +1036 -126
- package/dist/migrate.mjs +52 -1
- package/dist/public/app.js +42 -16
- package/dist/public/i18n/en/applications.js +23 -0
- package/dist/public/i18n/en/common.js +22 -0
- package/dist/public/i18n/en/documents.js +133 -0
- package/dist/public/i18n/en/home.js +75 -0
- package/dist/public/i18n/en/interview.js +69 -0
- package/dist/public/i18n/en/jobs.js +152 -0
- package/dist/public/i18n/en/nav.js +18 -0
- package/dist/public/i18n/en/profile.js +201 -0
- package/dist/public/i18n/en/settings.js +127 -0
- package/dist/public/i18n/en/status.js +22 -0
- package/dist/public/i18n/en.js +17 -0
- package/dist/public/i18n/ko/applications.js +23 -0
- package/dist/public/i18n/ko/common.js +22 -0
- package/dist/public/i18n/ko/documents.js +133 -0
- package/dist/public/i18n/ko/home.js +63 -0
- package/dist/public/i18n/ko/interview.js +69 -0
- package/dist/public/i18n/ko/jobs.js +152 -0
- package/dist/public/i18n/ko/nav.js +18 -0
- package/dist/public/i18n/ko/profile.js +201 -0
- package/dist/public/i18n/ko/settings.js +127 -0
- package/dist/public/i18n/ko/status.js +23 -0
- package/dist/public/i18n/ko.js +18 -0
- package/dist/public/i18n.js +55 -0
- package/dist/public/index.html +1 -0
- package/dist/public/lib.js +25 -17
- package/dist/public/pages/applications.js +17 -21
- package/dist/public/pages/documents.js +153 -107
- package/dist/public/pages/home.js +88 -53
- package/dist/public/pages/interview.js +105 -51
- package/dist/public/pages/jobs.js +330 -106
- package/dist/public/pages/profile.js +157 -152
- package/dist/public/pages/settings.js +270 -72
- package/dist/public/styles.css +38 -3
- package/dist/web.mjs +859 -103
- package/docs/FAQ.md +26 -5
- package/docs/MCP_TOOLS.md +26 -12
- package/docs/START_WORKFLOW.md +2 -2
- package/docs/SUPPORTED_AI_APPS.md +13 -0
- package/docs/WORKFLOWS.md +80 -23
- package/docs/career-os/README.md +8 -3
- package/docs/career-os/eop/career-description.md +136 -0
- package/docs/career-os/eop/cover-letter.md +7 -5
- package/docs/career-os/eop/fit-analysis.md +15 -5
- package/docs/career-os/eop/interview-prep.md +209 -21
- package/docs/career-os/eop/job-analysis.md +5 -3
- package/docs/career-os/eop/profile-extraction.md +5 -4
- package/docs/career-os/knowledge/cover-letter.md +4 -4
- package/docs/career-os/knowledge/fit-matching.md +8 -8
- package/docs/career-os/knowledge/interview-behavioral.md +76 -8
- package/docs/career-os/knowledge/interview-technical.md +106 -4
- package/docs/career-os/knowledge/linkedin-profile.md +1 -1
- package/docs/career-os/knowledge/networking-referrals.md +1 -1
- package/docs/career-os/knowledge/offer-evaluation-decision.md +10 -10
- package/docs/career-os/knowledge/portfolio.md +3 -3
- package/docs/career-os/knowledge/verifiers/ats-compat.md +8 -4
- package/docs/career-os/knowledge/verifiers/human-voice.md +2 -2
- package/docs/career-os/knowledge/verifiers/recency-staleness.md +1 -1
- package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +2 -2
- package/docs/career-os/knowledge/verifiers/truthfulness.md +1 -1
- package/package.json +2 -2
- package/packages/core/src/onboarding.ts +2 -1
- package/packages/core/src/services.ts +138 -21
- package/packages/core/src/summary.ts +59 -1
- package/packages/core/src/verify/lint.ts +4 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +0 -0
- package/packages/db/src/schema.ts +17 -0
- package/packages/exporters/src/index.ts +28 -17
- package/packages/knowledge/src/index.ts +46 -7
- package/packages/mcp-tools/src/bridge.ts +194 -13
- package/packages/mcp-tools/src/tools.ts +390 -38
- package/packages/mcp-tools/src/update.ts +31 -16
- package/packages/parsers/src/documents.ts +12 -1
- package/packages/prompts/src/humanize.ts +46 -0
- package/packages/prompts/src/index.ts +5 -1
- package/packages/prompts/src/install.ts +4 -4
- package/packages/prompts/src/onboarding.ts +4 -4
- package/packages/prompts/src/system.ts +17 -8
- package/packages/shared/src/enums.ts +14 -4
- package/packages/shared/src/schemas.ts +41 -0
- package/packages/workflows/src/definitions.ts +47 -10
- package/scripts/init.ts +284 -13
- package/dist/site/analytics.js +0 -97
- package/dist/site/favicon.svg +0 -1
- package/dist/site/fonts/PretendardVariable.woff2 +0 -0
- package/dist/site/icon-180.png +0 -0
- package/dist/site/icon-512.png +0 -0
- package/dist/site/index.html +0 -565
- package/dist/site/landing.js +0 -54
- package/dist/site/llms-install.txt +0 -218
- package/dist/site/robots.txt +0 -4
- package/dist/site/sitemap.xml +0 -21
- package/dist/site/start.html +0 -353
- package/dist/site/style.css +0 -559
- package/dist/site/vercel.json +0 -20
package/scripts/init.ts
CHANGED
|
@@ -80,15 +80,34 @@ function cursorConfigPath(): string {
|
|
|
80
80
|
return path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Cursor 도구 사전허용 파일(연결용 mcp.json과 별개). Cursor ≥3.6는 `permissions.json`의
|
|
85
|
+
* `mcpAllowlist`("server:tool")로 특정 MCP 도구를 프롬프트 없이 결정적으로 자동 실행한다.
|
|
86
|
+
* 전역 mcp.json에 서버를 등록하므로 전역 permissions.json에 맞춰 쓴다(careermate: 네임스페이스로만 한정).
|
|
87
|
+
*/
|
|
88
|
+
function cursorPermissionsPath(): string {
|
|
89
|
+
return path.join(os.homedir(), '.cursor', 'permissions.json');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 사용자가 init을 실행한 '작업 폴더'. bin/careermate.mjs는 tsx 해석을 위해 자식 cwd를 패키지
|
|
94
|
+
* 루트로 고정하므로, 원래 폴더를 CAREERMATE_INIT_CWD로 받아 우선한다(없으면 process.cwd()).
|
|
95
|
+
* Claude Code는 사용자가 실제로 여는 폴더의 .mcp.json·.claude 설정만 읽으므로 이 폴더가 기준이다.
|
|
96
|
+
*/
|
|
97
|
+
function userCwd(): string {
|
|
98
|
+
const fromBin = process.env.CAREERMATE_INIT_CWD?.trim();
|
|
99
|
+
return fromBin && fromBin.length > 0 ? fromBin : process.cwd();
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
/**
|
|
84
103
|
* Claude Code project scope 설정: 사용자가 지금 작업 중인 폴더에 둔다.
|
|
85
104
|
*
|
|
86
105
|
* `npx -y careermate init`은 패키지를 임시 캐시에 풀어 실행하므로 ROOT는 사용자의
|
|
87
106
|
* 프로젝트가 아니라 설치물 위치다. Claude Code는 실제로 열 폴더의 `.mcp.json`만
|
|
88
|
-
* 승인/로드하므로
|
|
107
|
+
* 승인/로드하므로 사용자의 작업 폴더(userCwd)를 기준으로 한다.
|
|
89
108
|
*/
|
|
90
109
|
function claudeCodeConfigPath(): string {
|
|
91
|
-
return path.join(
|
|
110
|
+
return path.join(userCwd(), '.mcp.json');
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
/** Codex CLI 글로벌 설정(CODEX_HOME 우선). */
|
|
@@ -228,10 +247,16 @@ function tomlString(s: string): string {
|
|
|
228
247
|
return JSON.stringify(s);
|
|
229
248
|
}
|
|
230
249
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Codex 설정의 careermate 블록(헤더 + command + args). 우리가 형태를 통제하므로 단순하다.
|
|
252
|
+
* autoApprove면 `default_tools_approval_mode = "auto"`를 더해 이 서버 도구를 프롬프트 없이 실행한다
|
|
253
|
+
* (SENSITIVE 도구는 별도 [..tools.<tool>] 서브테이블에서 "prompt"로 되돌린다 — writeCodexToml 참고).
|
|
254
|
+
*/
|
|
255
|
+
function codexBlock(server: ServerEntry, autoApprove: boolean): string {
|
|
233
256
|
const args = server.args.map(tomlString).join(', ');
|
|
234
|
-
|
|
257
|
+
const lines = [`[mcp_servers.${PKG}]`, `command = ${tomlString(server.command)}`, `args = [${args}]`];
|
|
258
|
+
if (autoApprove) lines.push('default_tools_approval_mode = "auto"');
|
|
259
|
+
return lines.join('\n');
|
|
235
260
|
}
|
|
236
261
|
|
|
237
262
|
/**
|
|
@@ -265,8 +290,46 @@ function upsertCodexBlock(raw: string, block: string): { next: string; found: bo
|
|
|
265
290
|
return { next, found: true };
|
|
266
291
|
}
|
|
267
292
|
|
|
268
|
-
/**
|
|
269
|
-
function
|
|
293
|
+
/** 점 표기 TOML 테이블 헤더(`a.b.c`)를 공백 허용 정규식으로(우리가 만든 테이블만 안전 교체). */
|
|
294
|
+
function tomlTableHeaderRe(dotted: string): RegExp {
|
|
295
|
+
const parts = dotted.split('.').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
296
|
+
return new RegExp(`^\\s*\\[\\s*${parts.join('\\s*\\.\\s*')}\\s*\\]\\s*$`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 이름이 정확한 TOML 테이블 하나를 멱등 삽입/교체한다(`upsertCodexBlock`의 일반화).
|
|
301
|
+
* 블록 경계는 "다음 `[` 테이블 헤더"까지 → 우리 테이블만 건드리고 사용자의 다른 테이블은 보존.
|
|
302
|
+
* SENSITIVE 도구의 `[mcp_servers.careermate.tools.<tool>]` 프롬프트 복원 서브테이블에 쓴다.
|
|
303
|
+
*/
|
|
304
|
+
function upsertTomlTable(raw: string, dotted: string, body: string[]): string {
|
|
305
|
+
const lines = raw.length ? raw.split(/\r?\n/) : [];
|
|
306
|
+
const headerRe = tomlTableHeaderRe(dotted);
|
|
307
|
+
const tableRe = /^\s*\[/;
|
|
308
|
+
const block = [`[${dotted}]`, ...body];
|
|
309
|
+
const start = lines.findIndex((l) => headerRe.test(l));
|
|
310
|
+
if (start === -1) {
|
|
311
|
+
const trimmed = raw.replace(/\s+$/, '');
|
|
312
|
+
return (trimmed.length ? `${trimmed}\n\n` : '') + `${block.join('\n')}\n`;
|
|
313
|
+
}
|
|
314
|
+
let end = lines.length;
|
|
315
|
+
for (let j = start + 1; j < lines.length; j++) {
|
|
316
|
+
if (tableRe.test(lines[j])) {
|
|
317
|
+
end = j;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
let next = [...lines.slice(0, start), ...block, ...lines.slice(end)].join('\n');
|
|
322
|
+
if (!next.endsWith('\n')) next += '\n';
|
|
323
|
+
return next;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Codex CLI: `~/.codex/config.toml` 의 `[mcp_servers.careermate]` 테이블.
|
|
328
|
+
* allowTools면 서버 블록에 `default_tools_approval_mode = "auto"`를 넣어 careermate 도구를 자동승인하고,
|
|
329
|
+
* SENSITIVE 도구만 `[mcp_servers.careermate.tools.<tool>] approval_mode = "prompt"`로 되돌린다
|
|
330
|
+
* (Claude Code 사전허용과 같은 보안 분리). 전역 approval_policy/sandbox/trust는 건드리지 않는다.
|
|
331
|
+
*/
|
|
332
|
+
function writeCodexToml(target: ClientTarget, server: ServerEntry, allowTools: boolean): WriteResult {
|
|
270
333
|
const existed = fs.existsSync(target.configPath);
|
|
271
334
|
let raw = '';
|
|
272
335
|
if (existed) {
|
|
@@ -276,7 +339,13 @@ function writeCodexToml(target: ClientTarget, server: ServerEntry): WriteResult
|
|
|
276
339
|
raw = '';
|
|
277
340
|
}
|
|
278
341
|
}
|
|
279
|
-
const { next, found } = upsertCodexBlock(raw, codexBlock(server));
|
|
342
|
+
const { next: afterBlock, found } = upsertCodexBlock(raw, codexBlock(server, allowTools));
|
|
343
|
+
let next = afterBlock;
|
|
344
|
+
if (allowTools) {
|
|
345
|
+
for (const tool of SENSITIVE_TOOLS) {
|
|
346
|
+
next = upsertTomlTable(next, `mcp_servers.${PKG}.tools.${tool}`, ['approval_mode = "prompt"']);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
280
349
|
const changed = next.trim() !== raw.trim();
|
|
281
350
|
|
|
282
351
|
let backedUp: string | null = null;
|
|
@@ -286,8 +355,157 @@ function writeCodexToml(target: ClientTarget, server: ServerEntry): WriteResult
|
|
|
286
355
|
return { changed, backedUp, replacedExisting: found && changed };
|
|
287
356
|
}
|
|
288
357
|
|
|
289
|
-
function writeTarget(target: ClientTarget, server: ServerEntry): WriteResult {
|
|
290
|
-
return target.format === 'codex-toml'
|
|
358
|
+
function writeTarget(target: ClientTarget, server: ServerEntry, allowTools: boolean): WriteResult {
|
|
359
|
+
return target.format === 'codex-toml'
|
|
360
|
+
? writeCodexToml(target, server, allowTools)
|
|
361
|
+
: writeMcpJson(target, server);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Claude Code 도구 사전허용 분류.
|
|
366
|
+
* 첫 사용 마찰의 핵심은 careermate MCP 도구를 처음 부를 때마다 뜨는 승인 프롬프트다. init이 작업
|
|
367
|
+
* 폴더의 .claude/settings.local.json에 SAFE 도구를 미리 허용하면 그 프롬프트가 사라진다.
|
|
368
|
+
* 거의 모든 도구는 ~/.careermate 안에서만 읽고 쓰거나 careermate 자신의 폴더·대시보드(127.0.0.1)만
|
|
369
|
+
* 여는 로컬 동작이라 SAFE다(인박스 폴더·대시보드 열기, 인박스 안 문서 읽기 포함 — 모두 careermate 내부).
|
|
370
|
+
* SENSITIVE는 정말로 손이 careermate 밖으로 나가거나 되돌릴 수 없는 것들이고, 이것만 확인을 유지한다:
|
|
371
|
+
* - read_document : 사용자가 준 임의 절대경로 파일 읽기(careermate 밖). 인젝션 시 민감 파일 탈취
|
|
372
|
+
* 증폭 — packages/parsers의 denylist는 ~/.ssh·.env 등 일부만 막는다.
|
|
373
|
+
* - update_careermate: npm install 실행(외부 프로세스·네트워크·공급망 표면).
|
|
374
|
+
* - delete_* (cover_letter/job_posting/resume/experience/project/skill) : 되돌릴 수 없는 데이터 삭제.
|
|
375
|
+
* 내부 데이터지만 비가역이라 권한 프롬프트(=사용자 본인의 확인)에 가치가 있다
|
|
376
|
+
* (코드의 confirm='DELETE'는 모델 자가확인일 뿐, 사용자 확인이 아니다).
|
|
377
|
+
* 새 MCP 도구를 추가하면 둘 중 하나로 분류해야 한다(scripts/test-init.ts가 manifest와 대조해 누락을 잡는다).
|
|
378
|
+
*/
|
|
379
|
+
const SENSITIVE_TOOLS: readonly string[] = [
|
|
380
|
+
'read_document',
|
|
381
|
+
'update_careermate',
|
|
382
|
+
'delete_cover_letter',
|
|
383
|
+
'delete_job_posting',
|
|
384
|
+
'delete_resume',
|
|
385
|
+
'delete_experience',
|
|
386
|
+
'delete_project',
|
|
387
|
+
'delete_skill',
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
const SAFE_TOOLS: readonly string[] = [
|
|
391
|
+
'get_onboarding_status', 'get_profile', 'save_profile', 'add_resume', 'get_resumes',
|
|
392
|
+
'add_experience', 'get_experiences', 'add_project', 'get_projects', 'add_skill', 'get_skills',
|
|
393
|
+
'get_cover_letters', 'save_cover_letter_version', 'save_job_posting', 'get_job_posting', 'list_jobs',
|
|
394
|
+
'get_application_context', 'save_fit_analysis', 'update_application_status', 'save_interview_prep',
|
|
395
|
+
'export_cover_letter', 'export_resume', 'export_profile', 'list_recent_activity', 'get_workflow_guide', 'get_playbook', 'get_verifier',
|
|
396
|
+
'validate_cover_letter', 'set_verify_mode', 'get_writing_style_guide', 'check_for_update',
|
|
397
|
+
// careermate 자신의 인박스 폴더·대시보드(127.0.0.1)만 여는 로컬 동작 + 인박스 내부 문서 읽기 → 사전허용 안전.
|
|
398
|
+
'read_inbox', 'open_inbox', 'open_dashboard', 'open_application',
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
/** Claude Code 작업 폴더의 로컬(비공유) 설정 파일. 권한·신뢰는 머신/사용자 로컬 산출물이다. */
|
|
402
|
+
function claudeSettingsLocalPath(): string {
|
|
403
|
+
return path.join(userCwd(), '.claude', 'settings.local.json');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 작업 폴더가 git repo이거나 이미 .gitignore가 있으면 주어진 경로를 무시 목록에 보장한다(best-effort).
|
|
408
|
+
* settings.local.json은 머신 로컬 권한이라 공유 대상이 아니므로 실수로 커밋되지 않게 한다.
|
|
409
|
+
*/
|
|
410
|
+
function ensureGitignored(file: string): void {
|
|
411
|
+
const cwd = userCwd();
|
|
412
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
413
|
+
const isRepo = fs.existsSync(path.join(cwd, '.git'));
|
|
414
|
+
if (!isRepo && !fs.existsSync(gitignorePath)) return;
|
|
415
|
+
const rel = path.relative(cwd, file).split(path.sep).join('/');
|
|
416
|
+
let body = '';
|
|
417
|
+
try {
|
|
418
|
+
body = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
419
|
+
} catch {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const already = body.split(/\r?\n/).some((l) => {
|
|
423
|
+
const t = l.trim();
|
|
424
|
+
return t === rel || t === `/${rel}`;
|
|
425
|
+
});
|
|
426
|
+
if (already) return;
|
|
427
|
+
const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
|
|
428
|
+
try {
|
|
429
|
+
fs.appendFileSync(gitignorePath, `${prefix}${rel}\n`, 'utf8');
|
|
430
|
+
} catch {
|
|
431
|
+
/* best-effort: 무시 등록 실패해도 설치는 계속 */
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Claude Code 사전허용: 작업 폴더의 .claude/settings.local.json에
|
|
437
|
+
* (1) enabledMcpjsonServers: ["careermate"] — 재시작 후 'project 서버 신뢰' 1회 프롬프트 제거
|
|
438
|
+
* (2) permissions.allow에 SAFE 도구(mcp__careermate__<tool>) — 데이터 도구 첫 호출 프롬프트 제거
|
|
439
|
+
* 를 병합 기록한다(기존 키·사용자 설정 보존, 변경 시 타임스탬프 백업). 폴더를 처음 '신뢰'하는
|
|
440
|
+
* 1회 다이얼로그는 Claude Code가 강제하므로 사라지지 않는다(보안 게이트 유지).
|
|
441
|
+
*/
|
|
442
|
+
function writeClaudeAllowlist(): WriteResult {
|
|
443
|
+
const file = claudeSettingsLocalPath();
|
|
444
|
+
const existed = fs.existsSync(file);
|
|
445
|
+
const json = existed ? readJsonObject(file) : {};
|
|
446
|
+
const before = JSON.stringify(json);
|
|
447
|
+
|
|
448
|
+
// (1) 우리가 방금 쓴 .mcp.json의 careermate 서버를 미리 신뢰(다른 서버는 건드리지 않음).
|
|
449
|
+
const enabled = Array.isArray(json.enabledMcpjsonServers)
|
|
450
|
+
? (json.enabledMcpjsonServers as unknown[]).filter((x): x is string => typeof x === 'string')
|
|
451
|
+
: [];
|
|
452
|
+
if (!enabled.includes(PKG)) enabled.push(PKG);
|
|
453
|
+
json.enabledMcpjsonServers = enabled;
|
|
454
|
+
|
|
455
|
+
// (2) SAFE 도구만 사전허용에 추가. 기존 사용자 allow는 보존하고, 어떤 경우에도 SENSITIVE는 추가하지 않는다.
|
|
456
|
+
const perms =
|
|
457
|
+
json.permissions && typeof json.permissions === 'object' && !Array.isArray(json.permissions)
|
|
458
|
+
? (json.permissions as Record<string, unknown>)
|
|
459
|
+
: {};
|
|
460
|
+
const allow = new Set<string>(
|
|
461
|
+
Array.isArray(perms.allow)
|
|
462
|
+
? (perms.allow as unknown[]).filter((x): x is string => typeof x === 'string')
|
|
463
|
+
: [],
|
|
464
|
+
);
|
|
465
|
+
for (const tool of SAFE_TOOLS) {
|
|
466
|
+
if (SENSITIVE_TOOLS.includes(tool)) continue; // 방어: 분류 실수로 SAFE에 민감 도구가 섞여도 추가하지 않음
|
|
467
|
+
allow.add(`mcp__${PKG}__${tool}`);
|
|
468
|
+
}
|
|
469
|
+
perms.allow = [...allow];
|
|
470
|
+
json.permissions = perms;
|
|
471
|
+
|
|
472
|
+
const changed = JSON.stringify(json) !== before;
|
|
473
|
+
let backedUp: string | null = null;
|
|
474
|
+
if (existed && changed) backedUp = backupFile(file);
|
|
475
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
476
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
|
|
477
|
+
ensureGitignored(file);
|
|
478
|
+
return { changed, backedUp, replacedExisting: false };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Cursor(≥3.6) 도구 사전허용: `~/.cursor/permissions.json`의 `mcpAllowlist`에 SAFE 도구를
|
|
483
|
+
* "careermate:<tool>"로 병합한다(기존 항목·다른 서버는 보존). SENSITIVE는 어떤 경우에도 넣지 않아
|
|
484
|
+
* Claude Code와 같은 보안 분리를 유지한다. 구버전 Cursor는 이 파일을 무시할 뿐 해롭지 않다.
|
|
485
|
+
*/
|
|
486
|
+
function writeCursorAllowlist(): WriteResult {
|
|
487
|
+
const file = cursorPermissionsPath();
|
|
488
|
+
const existed = fs.existsSync(file);
|
|
489
|
+
const json = existed ? readJsonObject(file) : {};
|
|
490
|
+
const before = JSON.stringify(json);
|
|
491
|
+
|
|
492
|
+
const list = new Set<string>(
|
|
493
|
+
Array.isArray(json.mcpAllowlist)
|
|
494
|
+
? (json.mcpAllowlist as unknown[]).filter((x): x is string => typeof x === 'string')
|
|
495
|
+
: [],
|
|
496
|
+
);
|
|
497
|
+
for (const tool of SAFE_TOOLS) {
|
|
498
|
+
if (SENSITIVE_TOOLS.includes(tool)) continue; // 방어: 민감 도구는 절대 자동승인하지 않음
|
|
499
|
+
list.add(`${PKG}:${tool}`);
|
|
500
|
+
}
|
|
501
|
+
json.mcpAllowlist = [...list];
|
|
502
|
+
|
|
503
|
+
const changed = JSON.stringify(json) !== before;
|
|
504
|
+
let backedUp: string | null = null;
|
|
505
|
+
if (existed && changed) backedUp = backupFile(file);
|
|
506
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
507
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
|
|
508
|
+
return { changed, backedUp, replacedExisting: false };
|
|
291
509
|
}
|
|
292
510
|
|
|
293
511
|
function flagValue(argv: string[], name: string): string | undefined {
|
|
@@ -317,7 +535,11 @@ function printConfigs(server: ServerEntry): void {
|
|
|
317
535
|
console.log(` · 또는 명령으로: ${shellJoin(['claude', 'mcp', 'add', '--scope', 'project', '--transport', 'stdio', PKG, '--', ...cmd])}`);
|
|
318
536
|
|
|
319
537
|
console.log('\n② Codex CLI — ~/.codex/config.toml 에 추가:');
|
|
320
|
-
console.log(codexBlock(server));
|
|
538
|
+
console.log(codexBlock(server, true));
|
|
539
|
+
console.log(' · default_tools_approval_mode = "auto" 는 careermate 도구를 자동승인합니다(민감/파괴 도구는 아래로 프롬프트 유지).');
|
|
540
|
+
for (const tool of SENSITIVE_TOOLS) {
|
|
541
|
+
console.log(`[mcp_servers.${PKG}.tools.${tool}]\napproval_mode = "prompt"`);
|
|
542
|
+
}
|
|
321
543
|
console.log(`\n · 또는 명령으로: ${shellJoin(['codex', 'mcp', 'add', PKG, '--', ...cmd])}`);
|
|
322
544
|
}
|
|
323
545
|
|
|
@@ -326,6 +548,7 @@ function main(): void {
|
|
|
326
548
|
const useNpx = argv.includes('--npx') || runningUnderNpx();
|
|
327
549
|
const printOnly = argv.includes('--print');
|
|
328
550
|
const forceAll = argv.includes('--all-clients');
|
|
551
|
+
const skipAllow = argv.includes('--no-allow-tools');
|
|
329
552
|
const which = flagValue(argv, '--client') ?? 'all';
|
|
330
553
|
|
|
331
554
|
const server = serverEntry(useNpx);
|
|
@@ -344,7 +567,8 @@ function main(): void {
|
|
|
344
567
|
const connectedIds = new Set<string>();
|
|
345
568
|
for (const t of targets) {
|
|
346
569
|
try {
|
|
347
|
-
|
|
570
|
+
// Codex는 등록과 동시에 도구 사전허용(default auto + SENSITIVE prompt)을 같은 toml에 쓴다.
|
|
571
|
+
const { changed, backedUp, replacedExisting } = writeTarget(t, server, !skipAllow);
|
|
348
572
|
connected += 1;
|
|
349
573
|
connectedIds.add(t.id);
|
|
350
574
|
console.log(
|
|
@@ -365,10 +589,57 @@ function main(): void {
|
|
|
365
589
|
return;
|
|
366
590
|
}
|
|
367
591
|
|
|
592
|
+
// Claude Code: 도구 사전허용을 작업 폴더에 함께 써, 재시작 후 도구별 승인 프롬프트를 없앤다.
|
|
593
|
+
// (민감/파괴 도구는 SAFE 목록에서 제외돼 그대로 확인을 받는다. --no-allow-tools로 끌 수 있다.)
|
|
594
|
+
let allowlisted = false;
|
|
595
|
+
if (connectedIds.has('claude-code') && !skipAllow) {
|
|
596
|
+
try {
|
|
597
|
+
const r = writeClaudeAllowlist();
|
|
598
|
+
allowlisted = true;
|
|
599
|
+
console.log(
|
|
600
|
+
`• Claude Code 도구 사전허용 ${r.changed ? '기록됨' : '이미 최신'}: ${claudeSettingsLocalPath()}` +
|
|
601
|
+
(r.backedUp ? `\n (기존 설정 백업: ${r.backedUp})` : ''),
|
|
602
|
+
);
|
|
603
|
+
console.log(' (커리어 데이터 도구는 미리 허용 — 파일 읽기·업데이트 등 민감한 작업은 그때그때 확인을 받습니다)');
|
|
604
|
+
} catch (e) {
|
|
605
|
+
console.log(`• Claude Code 사전허용은 건너뜁니다(설치는 계속): ${e instanceof Error ? e.message : e}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Codex: 사전허용은 위 writeCodexToml(config.toml)에서 이미 처리됨 — 사용자에게만 한 줄 안내.
|
|
610
|
+
if (connectedIds.has('codex') && !skipAllow) {
|
|
611
|
+
console.log('• Codex CLI 도구 사전허용 기록됨(config.toml) — 커리어 데이터 도구는 자동승인, 민감/파괴 작업은 확인을 받습니다');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Cursor(≥3.6): 연결용 mcp.json과 별개인 permissions.json에 SAFE 도구만 자동승인 등록.
|
|
615
|
+
if (connectedIds.has('cursor') && !skipAllow) {
|
|
616
|
+
try {
|
|
617
|
+
const r = writeCursorAllowlist();
|
|
618
|
+
console.log(
|
|
619
|
+
`• Cursor 도구 사전허용 ${r.changed ? '기록됨' : '이미 최신'}: ${cursorPermissionsPath()}` +
|
|
620
|
+
(r.backedUp ? `\n (기존 설정 백업: ${r.backedUp})` : ''),
|
|
621
|
+
);
|
|
622
|
+
console.log(' (Cursor 3.6+ 에서 적용 — 커리어 데이터 도구 자동승인, 민감/파괴 작업은 확인. 구버전은 무시됩니다)');
|
|
623
|
+
} catch (e) {
|
|
624
|
+
console.log(`• Cursor 사전허용은 건너뜁니다(설치는 계속): ${e instanceof Error ? e.message : e}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Gemini CLI·Antigravity: 도구 사전허용은 의도적으로 건드리지 않는다. 둘 다 per-tool 자동승인이 없고
|
|
629
|
+
// 서버 단위 trust(전부 자동승인)뿐이라, SENSITIVE(파일읽기·삭제·업데이트)를 함께 자동승인하게 되어
|
|
630
|
+
// Claude/Codex/Cursor에서 지킨 보안 분리가 깨진다. 안전하게 기본(프롬프트 유지)을 둔다.
|
|
631
|
+
if ((connectedIds.has('gemini') || connectedIds.has('antigravity')) && !skipAllow) {
|
|
632
|
+
console.log('• Gemini CLI/Antigravity는 도구별 사전허용이 없어, 첫 도구 사용 때 1회 "허용"만 눌러주세요(보안상 일괄 자동승인은 피했습니다)');
|
|
633
|
+
}
|
|
634
|
+
|
|
368
635
|
console.log('\n✅ 거의 끝났습니다. 다음만 해주세요:');
|
|
369
636
|
console.log(' 1) 연결한 AI 클라이언트를 완전히 종료했다가 다시 켜기');
|
|
370
637
|
if (connectedIds.has('claude-code')) {
|
|
371
|
-
console.log(
|
|
638
|
+
console.log(
|
|
639
|
+
allowlisted
|
|
640
|
+
? ' (Claude Code: 이 폴더를 처음 열 때 "신뢰"만 한 번 확인하면 됩니다 — 커리어 데이터 도구는 미리 허용해 두었습니다.)'
|
|
641
|
+
: ' (Claude Code는 이 명령을 실행한 폴더의 .mcp.json을 처음 띄울 때 1회 "승인"을 물어봅니다 — 승인해 주세요.)',
|
|
642
|
+
);
|
|
372
643
|
}
|
|
373
644
|
console.log(' 2) AI에게: "get_onboarding_status 호출해서 연결됐는지 확인해줘"');
|
|
374
645
|
console.log(' 3) 내 데이터 대시보드 열기: careermate start');
|
package/dist/site/analytics.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// CareerMate marketing-site analytics — careermate.life ONLY.
|
|
3
|
-
//
|
|
4
|
-
// SCOPE: this file loads on the PUBLIC marketing/demo site (site/) that
|
|
5
|
-
// Vercel serves. It must NEVER be loaded by the local dashboard (apps/web, bound
|
|
6
|
-
// to 127.0.0.1) — that would break the core promise in AGENTS.md:
|
|
7
|
-
// "절대 사용자 데이터를 외부로 업로드하지 마라."
|
|
8
|
-
// There is no user career data on this site; only anonymous page/traffic signals.
|
|
9
|
-
//
|
|
10
|
-
// What runs here:
|
|
11
|
-
// 1) Vercel Web Analytics — cookieless, no PII, no consent banner required.
|
|
12
|
-
// 2) Vercel Speed Insights — Core Web Vitals, also cookieless.
|
|
13
|
-
// 3) Google Analytics 4 — OPTIONAL. Off until GA_MEASUREMENT_ID is filled in.
|
|
14
|
-
// Runs under Google Consent Mode v2 with analytics_storage DENIED by default,
|
|
15
|
-
// i.e. cookieless pings, so it needs no consent banner and stays consistent
|
|
16
|
-
// with the local-first / privacy-first brand. (Flip consent to 'granted'
|
|
17
|
-
// later only if you add a real consent UI.)
|
|
18
|
-
//
|
|
19
|
-
// The marketing pages (index.html, start.html) are classic multi-page navigations,
|
|
20
|
-
// so a single pageview on load is enough. The demo (/demo/) is a hash-router SPA,
|
|
21
|
-
// so we emit a virtual pageview on every hashchange — otherwise every sub-route
|
|
22
|
-
// (#/jobs, #/applications, …) would collapse into one "/demo" view.
|
|
23
|
-
// =============================================================================
|
|
24
|
-
(function () {
|
|
25
|
-
'use strict';
|
|
26
|
-
|
|
27
|
-
// ── 1. Vercel Web Analytics (cookieless) ───────────────────────────────────
|
|
28
|
-
// Queue shim per Vercel's HTML snippet; the injected script drains window.vaq.
|
|
29
|
-
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
|
|
30
|
-
loadScript('/_vercel/insights/script.js');
|
|
31
|
-
|
|
32
|
-
// ── 2. Vercel Speed Insights (cookieless) — OFF by default ──────────────────
|
|
33
|
-
// Vercel's Hobby plan allows Speed Insights on only ONE project at a time, and it's
|
|
34
|
-
// more valuable on the real app than on this rarely-changing static site (use the free
|
|
35
|
-
// PageSpeed Insights for ad-hoc checks here). Flip to true to load it (needs Enable in
|
|
36
|
-
// the Vercel dashboard too); leaving it false avoids a dead 404 script request.
|
|
37
|
-
var ENABLE_SPEED_INSIGHTS = false;
|
|
38
|
-
if (ENABLE_SPEED_INSIGHTS) {
|
|
39
|
-
window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); };
|
|
40
|
-
loadScript('/_vercel/speed-insights/script.js');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── 3. Google Analytics 4 (optional, cookieless via Consent Mode v2) ────────
|
|
44
|
-
// CareerMate's GA4 measurement ID — a public, client-side identifier (not a secret).
|
|
45
|
-
// Set to '' to disable GA entirely; the Vercel analytics above still runs.
|
|
46
|
-
var GA_MEASUREMENT_ID = 'G-HK9KWTYCBD';
|
|
47
|
-
if (/^G-[A-Z0-9]+$/.test(GA_MEASUREMENT_ID)) initGoogleAnalytics(GA_MEASUREMENT_ID);
|
|
48
|
-
|
|
49
|
-
// ── Demo SPA: report hash route changes to Vercel as a custom event ─────────
|
|
50
|
-
// Vercel's script auto-tracks History API navigations but not hashchange.
|
|
51
|
-
if (location.pathname.indexOf('/demo') === 0) {
|
|
52
|
-
window.addEventListener('hashchange', function () {
|
|
53
|
-
window.va('event', { name: 'demo_route', data: { route: currentRoute() } });
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
58
|
-
function loadScript(src) {
|
|
59
|
-
var s = document.createElement('script');
|
|
60
|
-
s.src = src;
|
|
61
|
-
s.defer = true;
|
|
62
|
-
document.head.appendChild(s);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function currentRoute() {
|
|
66
|
-
return (location.hash || '').replace(/^#\/?/, '').split('/')[0] || 'home';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function initGoogleAnalytics(id) {
|
|
70
|
-
window.dataLayer = window.dataLayer || [];
|
|
71
|
-
function gtag() { window.dataLayer.push(arguments); }
|
|
72
|
-
window.gtag = gtag;
|
|
73
|
-
// Deny storage by default → GA runs cookieless. No banner required.
|
|
74
|
-
gtag('consent', 'default', {
|
|
75
|
-
ad_storage: 'denied',
|
|
76
|
-
ad_user_data: 'denied',
|
|
77
|
-
ad_personalization: 'denied',
|
|
78
|
-
analytics_storage: 'denied',
|
|
79
|
-
});
|
|
80
|
-
gtag('js', new Date());
|
|
81
|
-
// send_page_view:false so we control pageviews (incl. the demo's hash routes).
|
|
82
|
-
gtag('config', id, { send_page_view: false, anonymize_ip: true });
|
|
83
|
-
loadScript('https://www.googletagmanager.com/gtag/js?id=' + id);
|
|
84
|
-
|
|
85
|
-
sendGaPageview();
|
|
86
|
-
window.addEventListener('hashchange', sendGaPageview);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function sendGaPageview() {
|
|
90
|
-
if (!window.gtag) return;
|
|
91
|
-
window.gtag('event', 'page_view', {
|
|
92
|
-
page_path: location.pathname + location.search + location.hash,
|
|
93
|
-
page_location: location.href,
|
|
94
|
-
page_title: document.title,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
})();
|
package/dist/site/favicon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#0f7256"/><text x="12" y="17" font-size="14" text-anchor="middle" fill="#fff" font-family="Segoe UI,sans-serif" font-weight="bold">C</text></svg>
|
|
Binary file
|
package/dist/site/icon-180.png
DELETED
|
Binary file
|
package/dist/site/icon-512.png
DELETED
|
Binary file
|