aiakiv 0.3.1 → 0.3.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 +59 -18
- package/dist/bind.js +21 -2
- package/dist/config/json.js +11 -3
- package/dist/doctor.js +126 -19
- package/dist/main.js +26 -9
- package/dist/mcpProbe.js +25 -2
- package/dist/setup.js +9 -8
- package/dist/skills.js +90 -128
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/skills/aiakiv-cards/SKILL.md +152 -0
- package/skills/aiakiv-console/SKILL.md +253 -0
- package/skills/aiakiv-contents/SKILL.md +840 -0
- package/skills/aiakiv-dashboard/SKILL.md +172 -0
- package/skills/aiakiv-graph-query/SKILL.md +278 -0
- package/skills/aiakiv-links/SKILL.md +123 -0
- package/skills/aiakiv-onboarding/SKILL.md +176 -0
- package/skills/aiakiv-save-and-recall/SKILL.md +165 -0
- package/skills/bundle.json +47 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Gemini CLI 와 Antigravity 는 전역 설치도 합니다. 설계는 K2G `docs/2
|
|
|
6
6
|
```
|
|
7
7
|
npx aiakiv bind --client codex|claude|gemini|cursor --project <이름> [--dir <폴더>] [--expires-days N] [--no-browser] [--no-skills]
|
|
8
8
|
npx aiakiv setup --client gemini|antigravity[,...] [--expires-days N] [--no-browser] [--no-skills] [--new-key]
|
|
9
|
-
npx aiakiv doctor [--client <클라이언트>[,...]] [--dir <폴더>] [--out aiakiv-verification-test.md] [--no-probe]
|
|
9
|
+
npx aiakiv doctor [--client <클라이언트>[,...]] [--dir <폴더>] [--out aiakiv-verification-test.md] [--no-probe] [--allow-url <주소>]
|
|
10
10
|
npx aiakiv local export <팀> --to <파일|폴더> [--domain <이름>] [--yes] [--container <이름>]
|
|
11
11
|
npx aiakiv local import <파일|폴더> --team <팀> [--as <사용자>] [--domain <이름>] [--dry-run] [--resume] [--yes] [--container <이름>]
|
|
12
12
|
npx aiakiv enterprise precheck [--config <toml>] [--image <참조> | --image-tar <파일>] [--device] [--no-browser] [--no-login]
|
|
@@ -40,23 +40,27 @@ bind 는 웹 콘솔의 "폴더 ZIP 받기" 와 같은 한 벌을 그 폴더에
|
|
|
40
40
|
|
|
41
41
|
### 폴더 스킬
|
|
42
42
|
|
|
43
|
-
- 스킬
|
|
44
|
-
`aiakiv-console`, `aiakiv-contents`)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
- 스킬 여덟 개(`aiakiv-onboarding`, `aiakiv-save-and-recall`, `aiakiv-graph-query`, `aiakiv-links`, `aiakiv-cards`,
|
|
44
|
+
`aiakiv-console`, `aiakiv-contents`, `aiakiv-dashboard`)는 이 CLI 의 npm 묶음 안 `skills/` 에 들어 있습니다.
|
|
45
|
+
bind 와 setup 은 그것을 쓰고, 네트워크에서 스킬을 받지 않습니다. 받는 곳은 npm 하나이고, 묶음이 바뀌지 않았는지는
|
|
46
|
+
npm 이 확인합니다(2026-11-10 결정. 0.3.1 까지는 GitHub 저장소 `AiAkiv/aiakiv-plugins` 의 main 가지를 그때그때
|
|
47
|
+
받았습니다).
|
|
48
|
+
- `skills/bundle.json` 에 파일마다 경로, 크기, sha256 과 스킬 버전(플러그인 매니페스트의 version)이 적혀 있습니다.
|
|
49
|
+
CLI 는 이 목록에 적힌 파일만 쓰고, 크기나 sha256 이 다르거나, 이름이 아래 여덟 개 밖이거나, 경로가 안전하지 않으면
|
|
50
|
+
하나도 쓰지 않습니다. 화면에는 스킬 버전을 냅니다(예: `폴더 스킬 8개: .agents/skills (이 CLI 에 든 스킬, 스킬 버전 0.4.40)`).
|
|
51
|
+
`doctor` 도 이 CLI 에 든 스킬의 버전을 적습니다.
|
|
49
52
|
- 자리는 콘솔 ZIP 과 같습니다. Claude Code 는 `.claude/skills`, Codex 와 Gemini CLI 는 `.agents/skills` 한 벌입니다
|
|
50
53
|
(둘이 같은 자리를 읽어 두 벌 두면 Codex 에서 겹쳐 보입니다).
|
|
51
54
|
- Cursor 도 `.agents/skills` 에 넣습니다. Cursor 문서(cursor.com/docs/context/skills)가 이 자리를 프로젝트 스킬
|
|
52
55
|
자리로 적고 있고, Skills CLI 의 cursor 설정도 같은 자리입니다(2026-10-14 확인). 콘솔 ZIP 에는 Cursor 가 없습니다.
|
|
53
56
|
- 같은 이름의 스킬 폴더는 통째로 바꾸고(그 안의 예전 파일은 사라집니다), 그 밖의 파일과 다른 스킬은 건드리지 않습니다.
|
|
54
57
|
같은 이름 자리가 심볼릭 링크나 정션이면 링크만 지우고 가리키던 곳은 그대로 둡니다.
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
`npx -y skills add AiAkiv/aiakiv-plugins -a <claude-code|codex|gemini-cli|cursor> -s '*' -y`
|
|
59
|
-
(`--all` 과 `-g` 는 쓰지 않습니다. `--all` 은 모든 에이전트 폴더에 퍼지고, `-g` 는 전역에 넣습니다).
|
|
58
|
+
- 쓰는 스킬은 CLI 에 적힌 여덟 이름(`src/skills.ts` 의 `SKILL_NAMES`)뿐입니다. 사용자가 손으로 만든 다른 이름의
|
|
59
|
+
스킬 폴더는 건드리지 않습니다(2026-11-10 보안 점검 낮음 6). setup 의 전역 스킬도 같습니다.
|
|
60
|
+
- 스킬을 쓰지 못해도 bind 는 멈추지 않습니다(연결 설정은 이미 썼고 종료 코드는 0). 대신 그 폴더에서 손으로 칠 명령을
|
|
61
|
+
보여 줍니다: `npx -y skills add AiAkiv/aiakiv-plugins -a <claude-code|codex|gemini-cli|cursor> -s '*' -y`
|
|
62
|
+
(`--all` 과 `-g` 는 쓰지 않습니다. `--all` 은 모든 에이전트 폴더에 퍼지고, `-g` 는 전역에 넣습니다). 이 명령은
|
|
63
|
+
Skills CLI 가 GitHub 저장소에서 받으므로, 사람이 직접 고를 때만 씁니다.
|
|
60
64
|
- `--no-skills` 면 스킬만 건너뜁니다. 연결 설정, `.claude/settings.json`, `.gitignore` 는 그대로 씁니다.
|
|
61
65
|
|
|
62
66
|
### Claude 의 enabledMcpjsonServers
|
|
@@ -71,6 +75,11 @@ Codex 와 Gemini 는 설정 파일에 키가 들어갑니다. 그 폴더가 git
|
|
|
71
75
|
`/.codex/config.toml`, `/.gemini/settings.json` 줄을 그 폴더의 `.gitignore` 에 더합니다(이미 무시되면 더하지 않습니다).
|
|
72
76
|
둘 다 아니면 더하지 않고 경고만 합니다.
|
|
73
77
|
|
|
78
|
+
`.gemini/settings.json` 에 주석이나 끝 쉼표가 있어 다시 쓰면서 원본을 `.gemini/settings.json.aiakiv-backup` 으로
|
|
79
|
+
남겼을 때는, 그 사본에도 예전 키가 있을 수 있으므로 `/.gemini/settings.json.aiakiv-backup` 줄도 같은 규칙으로 더합니다
|
|
80
|
+
(2026-11-10 보안 점검 낮음 7). 더할 수 없으면 사본에 대해서도 따로 경고합니다. 이 줄을 더하지 않으려면
|
|
81
|
+
`AIAKIV_GITIGNORE_BACKUP=0` 을 줍니다. macOS 와 Linux 에서는 이 사본도 키 파일처럼 주인만 읽게(0600) 둡니다.
|
|
82
|
+
|
|
74
83
|
### 연결 설정의 모양
|
|
75
84
|
|
|
76
85
|
| 클라이언트 | 설정 파일(폴더 기준) | 인증 | 쓰는 내용 |
|
|
@@ -140,8 +149,8 @@ CLI 는 폐기된 개수와 키 id 를 알립니다. 그래서 쓸 수 있는
|
|
|
140
149
|
- MCP 주소는 `AIAKIV_MCP_URL`(기본 `https://mcp.aiakiv.com/mcp`, 물음표 표시 없는 주소)입니다.
|
|
141
150
|
- 서버 이름은 전역이라 `AiAkiv` 입니다. 폴더의 `AiAkiv-<프로젝트>` 와 겹치지 않습니다.
|
|
142
151
|
- Antigravity 는 `serverUrl` 만 받습니다(`url`, `httpUrl`, `type` 은 거부합니다). 파일이 없으면 폴더째 만듭니다.
|
|
143
|
-
- 전역 스킬은 폴더 스킬과 같은
|
|
144
|
-
|
|
152
|
+
- 전역 스킬은 폴더 스킬과 같은 묶음(이 CLI 에 든 것)을 자리마다 씁니다. 같은 이름 스킬 폴더는 통째로 바꾸고 나머지는 둡니다.
|
|
153
|
+
쓰기에 실패해도 설정 쓰기는 성공으로 두고, Gemini CLI 는 `npx -y skills add AiAkiv/aiakiv-plugins -a gemini-cli -s '*' -g -y`
|
|
145
154
|
를, Antigravity 는 스킬 폴더를 직접 복사하라고 안내합니다(Skills CLI 의 antigravity 전역 자리는 `~/.gemini/antigravity/skills`
|
|
146
155
|
라 setup 의 자리와 다릅니다. `~/.gemini/antigravity-cli/skills` 는 `-a antigravity-cli` 로 안내합니다).
|
|
147
156
|
- `get_save_target` 은 키 id 를 돌려주지 않으므로, 새로 받을 때 키 id 와 키의 지문(sha256 앞 16자, 평문 아님)을
|
|
@@ -167,14 +176,24 @@ CLI 는 폐기된 개수와 키 id 를 알립니다. 그래서 쓸 수 있는
|
|
|
167
176
|
- 스킬 위치(`~/.claude/skills`, `~/.claude/plugins/cache/aiakiv/aiakiv/*/skills`, `~/.agents/skills`,
|
|
168
177
|
`~/.cursor/skills`, `~/.gemini/skills`, `~/.gemini/config/skills`, `~/.gemini/antigravity-cli/skills`,
|
|
169
178
|
`$CODEX_HOME/skills`, 폴더의 `.claude/skills`, `.agents/skills`)를 봅니다.
|
|
170
|
-
-
|
|
179
|
+
- 이 CLI 에 든 스킬의 개수와 스킬 버전을 적습니다(`skills/bundle.json` 과 파일이 맞지 않으면 그 까닭).
|
|
180
|
+
- bind 가 넣는 폴더 스킬 자리(`.claude/skills`, `.agents/skills`)에 여덟 개가 다 있는지 적습니다. 그 자리를 읽는
|
|
171
181
|
클라이언트가 이 폴더에 고정돼 있는데 빠진 스킬이 있으면 다음 단계에 bind 를 다시 하거나 칠 명령을 적습니다.
|
|
172
182
|
- 폴더 `.mcp.json` 의 AiAkiv 항목마다 `.claude/settings.json`(또는 `.claude/settings.local.json`)의
|
|
173
183
|
`enabledMcpjsonServers` 에 그 이름이 있는지 적습니다.
|
|
174
184
|
- 키로 묶인 항목은 MCP `get_save_target` 을 직접 불러 팀, 프로젝트, binding 을 적습니다
|
|
175
185
|
(`initialize`, `notifications/initialized`, `tools/call` 순서, 프로토콜 판 `2025-06-18`, 응답이 SSE 여도 읽음).
|
|
176
186
|
`--no-probe` 면 부르지 않습니다. OAuth 항목은 "클라이언트 안에서 get_save_target 을 부르라" 는 다음 단계를 적습니다.
|
|
187
|
+
- 폴더의 설정 파일은 남이 준 것일 수 있습니다(받은 저장소). 그래서 폴더 파일의 키는 주소의 방식, 호스트, 포트가
|
|
188
|
+
기본 MCP 주소(`https://mcp.aiakiv.com`)나 `AIAKIV_MCP_URL`, 명령 줄의 `--allow-url <주소>`(여러 번 줄 수 있음)와
|
|
189
|
+
같을 때만 보냅니다. 폴더 파일의 `bearer_token_env_var` 는 `AIAKIV_API_KEY` 일 때만 읽습니다. 그 밖이면 보내지도
|
|
190
|
+
읽지도 않고, 어느 파일의 어느 주소나 이름을 왜 건너뛰었는지 적습니다. 홈의 전역 파일은 사용자 자신의 설정이라
|
|
191
|
+
그대로 봅니다. 서버가 다른 주소로 넘기면(redirect) 따라가지 않고 멈춥니다(같은 주소 안의 307, 308 만 따라감).
|
|
177
192
|
- 결과는 `aiakiv-verification-test.md`(기본은 그 폴더)에 씁니다. 키로 묶인 연결이 실패하면 종료 코드 1 입니다.
|
|
193
|
+
- 결과 파일과 화면은 문의에 그대로 붙여 넣어도 되는 모양으로 씁니다. 키와 토큰은 앞 8자만, 홈 폴더 경로는 `~` 로
|
|
194
|
+
(경로에 든 계정 이름이 빠집니다), 이메일은 앞 두 글자와 도메인만(`ra…@example.com`) 적습니다. 이메일은 개인 팀 이름에
|
|
195
|
+
들어 있을 수 있습니다. 계정 id(`get_save_target` 의 `account`)는 적지 않습니다. 가리지 않고 그대로 보려면
|
|
196
|
+
`AIAKIV_DOCTOR_UNMASKED=1` 을 줍니다(키는 그래도 앞 8자만 적습니다).
|
|
178
197
|
|
|
179
198
|
## local export, local import: 자체 설치판 팀 기억 내보내기와 받기
|
|
180
199
|
|
|
@@ -284,7 +303,9 @@ npx aiakiv enterprise precheck --config ./aiakiv-enterprise.toml
|
|
|
284
303
|
(키 id 만, doctor 는 앞 8자 + `…`).
|
|
285
304
|
- CLI 는 토큰이나 키를 자기 저장소에 두지 않습니다. `login` 명령이 없습니다. setup 이 남기는
|
|
286
305
|
`~/.gemini/aiakiv-setup.json` 에는 키 id 와 지문만 있고 평문은 없습니다.
|
|
287
|
-
- macOS, Linux 에서는 키가 든 설정 파일을 주인만 읽게(0600) 씁니다.
|
|
306
|
+
- macOS, Linux 에서는 키가 든 설정 파일을 주인만 읽게(0600) 씁니다. Windows 는 이 권한을 쓰지 않아 적용되지 않습니다
|
|
307
|
+
(파일은 사용자 폴더의 기본 권한을 따릅니다). 권한 시험(`tests/fsutil.test.mjs`, `tests/bind.test.mjs` 의 사본 권한)도
|
|
308
|
+
Windows 에서는 건너뜁니다.
|
|
288
309
|
- 키가 든 설정 파일은 git 저장소이거나 `.gitignore` 가 있는 폴더면 `.gitignore` 에 더하고, 아니면 경고합니다.
|
|
289
310
|
**키가 든 파일을 저장소에 올리지 마세요.**
|
|
290
311
|
|
|
@@ -301,9 +322,10 @@ npx aiakiv enterprise precheck --config ./aiakiv-enterprise.toml
|
|
|
301
322
|
|---|---|---|
|
|
302
323
|
| `AIAKIV_MCP_URL` | `https://mcp.aiakiv.com/mcp` | MCP 주소. 끝의 `/mcp` 는 빼고 줘도 됩니다. |
|
|
303
324
|
| `AIAKIV_CONTROL_URL` | `https://app.aiakiv.com` | 웹 콘솔 주소(인계 화면과 키 교환). |
|
|
304
|
-
| `AIAKIV_SKILLS_TARBALL_URL` | `https://codeload.github.com/AiAkiv/aiakiv-plugins/tar.gz/refs/heads/main` | 폴더 스킬과 전역 스킬(setup)을 받는 tar.gz 주소(http 나 https). 시험은 로컬 서버로 바꿉니다. |
|
|
305
325
|
| `AIAKIV_ENTERPRISE_MANIFEST_URL` | `https://www.aiakiv.com/download/enterprise/<판>/manifest.json` | `enterprise precheck` 가 이미지를 내려받을 때 읽는 목록 파일 주소(http 나 https). 시험은 로컬 서버로 바꿉니다. |
|
|
306
326
|
| `CODEX_HOME` | `~/.codex` | Codex 설정 위치(플러그인 캐시, 스킬 확인에 씁니다). |
|
|
327
|
+
| `AIAKIV_GITIGNORE_BACKUP` | (없음) | `0` 이면 키 파일의 원본 사본(`<파일>.aiakiv-backup`)을 `.gitignore` 에 더하지 않습니다. |
|
|
328
|
+
| `AIAKIV_DOCTOR_UNMASKED` | (없음) | `1` 이면 doctor 가 홈 폴더 경로와 이메일을 가리지 않습니다. |
|
|
307
329
|
|
|
308
330
|
## 개발
|
|
309
331
|
|
|
@@ -314,9 +336,28 @@ npm test # build 뒤 node --test (외부 시험 도구 없음)
|
|
|
314
336
|
```
|
|
315
337
|
|
|
316
338
|
의존성은 TOML 파서 `smol-toml` 하나입니다. 인자 해석은 `node:util` 의 `parseArgs`, HTTP 는 내장 `http` 와 `fetch`,
|
|
317
|
-
폴더 스킬의 tar.gz 는 내장 `zlib` 와 작은 tar 읽기(`src/tar.ts`, ustar 와 pax 긴 이름),
|
|
318
339
|
브라우저 열기는 운영체제 명령(Windows `cmd /c start`, macOS `open`, Linux `xdg-open`)을 씁니다.
|
|
319
340
|
|
|
341
|
+
### 스킬 옮겨 적기
|
|
342
|
+
|
|
343
|
+
스킬의 정본은 K2G 저장소의 `packaging/plugins/aiakiv/skills/` 이고, 플러그인 저장소 `AiAkiv/aiakiv-plugins` 는 그것을
|
|
344
|
+
게시한 것입니다. 이 저장소의 `skills/` 는 정본을 옮겨 적은 것이라 손으로 고치지 않습니다.
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
npm run build
|
|
348
|
+
node scripts/sync-skills.mjs <K2G>/packaging/plugins # 옮겨 적기
|
|
349
|
+
node scripts/sync-skills.mjs <K2G>/packaging/plugins --check # 같은지만 보기(다르면 종료 코드 1)
|
|
350
|
+
AIAKIV_SKILLS_SOURCE=<K2G>/packaging/plugins npm test # 시험에서도 정본과 같은지 보기
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- 스크립트는 플러그인 버전 네 곳(Claude 와 Cursor 의 `plugin.json`, 두 `marketplace.json` 의 aiakiv 항목)이 같지
|
|
354
|
+
않거나, 정본의 스킬 폴더 이름이 `SKILL_NAMES` 와 다르거나, 스킬 폴더에 링크가 있으면 아무것도 쓰지 않고 멈춥니다.
|
|
355
|
+
- 텍스트 파일은 CRLF 를 LF 로 바꿔 적습니다. 정본 저장소가 git 에 LF 로 두므로, Windows 에서 CRLF 로 풀린 폴더에서
|
|
356
|
+
돌려도 git 에 든 것과 같은 바이트가 됩니다.
|
|
357
|
+
- 스킬이 바뀌면 순서는 이렇습니다. K2G 에서 스킬을 고치고 플러그인 버전 네 곳을 올립니다. 이 스크립트로 옮겨 적고
|
|
358
|
+
`npm test` 를 돌립니다. CLI 버전(`package.json`, `src/version.ts`)을 올려 npm 에 냅니다. 새 스킬을 더할 때는
|
|
359
|
+
`src/skills.ts` 의 `SKILL_NAMES` 에 먼저 이름을 넣습니다.
|
|
360
|
+
|
|
320
361
|
## 라이선스
|
|
321
362
|
|
|
322
363
|
MIT
|
package/dist/bind.js
CHANGED
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 연결 설정 뒤에 웹 콘솔 "폴더 ZIP 받기" 와 같은 나머지를 더한다(정본 k2g-console-web
|
|
11
11
|
// `src/lib/folderBundle.ts`):
|
|
12
|
-
// - 폴더 스킬(skills.ts). `--no-skills` 면 건너뛴다.
|
|
12
|
+
// - 폴더 스킬(skills.ts, 이 CLI 의 npm 묶음에 든 것). `--no-skills` 면 건너뛴다. 쓰기에 실패해도 bind 는
|
|
13
|
+
// 성공으로 끝난다.
|
|
13
14
|
// - claude: `.claude/settings.json` 의 enabledMcpjsonServers 에 서버 이름(폴더를 신뢰한 뒤 묻지 않고 켠다).
|
|
14
15
|
// - codex, gemini: 키가 든 설정 파일을 `.gitignore` 에 더한다(git 저장소이거나 `.gitignore` 가 있을 때).
|
|
15
16
|
// ZIP 의 README.md 는 쓰지 않는다(안내는 화면에 낸다).
|
|
16
17
|
import { appendFileSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
17
18
|
import { dirname, join, resolve } from "node:path";
|
|
18
19
|
import { CLAUDE_FOLDER_SETTINGS, FOLDER_CONFIG } from "./clients/types.js";
|
|
19
|
-
import { addToJsonArray, jsonServers, readJsonObject, upsertJsonServer } from "./config/json.js";
|
|
20
|
+
import { addToJsonArray, BACKUP_SUFFIX, jsonServers, readJsonObject, upsertJsonServer } from "./config/json.js";
|
|
20
21
|
import { upsertCodexServer } from "./config/toml.js";
|
|
21
22
|
import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, oldKeysKeptNotice, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
|
|
22
23
|
import { installFolderSkills } from "./skills.js";
|
|
@@ -169,6 +170,8 @@ export async function ensureGitIgnored(ctx, dir, rel, line = `/${rel}`) {
|
|
|
169
170
|
appendFileSync(gi, `${lead}${line}${eol}`, "utf8");
|
|
170
171
|
return "added";
|
|
171
172
|
}
|
|
173
|
+
/** 키 파일의 원본 사본(`<파일>.aiakiv-backup`)을 .gitignore 에 더하는 것을 끄는 환경 변수. 값이 `0` 일 때만 끈다. */
|
|
174
|
+
export const BACKUP_GITIGNORE_ENV = "AIAKIV_GITIGNORE_BACKUP";
|
|
172
175
|
function safeMcpUrl(ctx, given) {
|
|
173
176
|
if (typeof given === "string") {
|
|
174
177
|
try {
|
|
@@ -268,6 +271,22 @@ async function bindKey(ctx, client, project, dir, opts) {
|
|
|
268
271
|
if (ignore === "warn") {
|
|
269
272
|
ctx.err(`주의: ${rel} 이 .gitignore 에 없습니다. 키가 든 파일이니 저장소에 올리지 마세요(.gitignore 에 /${rel} 을 더하세요).`);
|
|
270
273
|
}
|
|
274
|
+
// 원본 사본에도 예전 키가 있을 수 있다(2026-11-10 보안 점검 낮음 7). 원래 파일과 같이 .gitignore 에 더한다.
|
|
275
|
+
if (backup && (ctx.env[BACKUP_GITIGNORE_ENV] ?? "").trim() !== "0") {
|
|
276
|
+
const backupRel = `${rel}${BACKUP_SUFFIX}`;
|
|
277
|
+
let b;
|
|
278
|
+
try {
|
|
279
|
+
b = await ensureGitIgnored(ctx, dir, backupRel);
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
ctx.err(`.gitignore 를 고치지 못했습니다(${e.message}).`);
|
|
283
|
+
b = "warn";
|
|
284
|
+
}
|
|
285
|
+
if (b === "added")
|
|
286
|
+
ctx.out(`더했습니다: ${join(dir, ".gitignore")} 에 /${backupRel}`);
|
|
287
|
+
if (b === "warn")
|
|
288
|
+
ctx.err(`주의: ${backupRel} 이 .gitignore 에 없습니다. 예전 키가 든 사본이니 확인한 뒤 지우세요.`);
|
|
289
|
+
}
|
|
271
290
|
if (opts.skills !== false)
|
|
272
291
|
await installFolderSkills(ctx, client, dir);
|
|
273
292
|
else
|
package/dist/config/json.js
CHANGED
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
// 클라이언트가 JSONC(주석, 끝 쉼표)를 받아 주는 파일이 실제로 있다(이 PC 의 ~/.cursor/mcp.json 에
|
|
6
6
|
// 끝 쉼표가 있었다, 2026-10-14). 그래서 읽기는 JSONC 까지 받아 주고, 그런 파일을 다시 쓸 때는
|
|
7
7
|
// 주석과 끝 쉼표가 사라지므로 원본을 `<파일>.aiakiv-backup` 으로 남기고 알린다.
|
|
8
|
-
import { copyFileSync } from "node:fs";
|
|
8
|
+
import { chmodSync, copyFileSync } from "node:fs";
|
|
9
9
|
import { readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
10
10
|
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11
11
|
export class ConfigError extends Error {
|
|
12
12
|
}
|
|
13
|
+
/** 주석이나 끝 쉼표가 있던 원본을 다시 쓰기 전에 남기는 사본의 이름 끝. */
|
|
14
|
+
export const BACKUP_SUFFIX = ".aiakiv-backup";
|
|
15
|
+
export function backupPath(file) {
|
|
16
|
+
return `${file}${BACKUP_SUFFIX}`;
|
|
17
|
+
}
|
|
13
18
|
// 문자열 끝 찾기(백슬래시 이스케이프를 건너뛴다). 닫는 따옴표의 위치를 돌려준다.
|
|
14
19
|
function stringEnd(s, start) {
|
|
15
20
|
let j = start + 1;
|
|
@@ -114,8 +119,11 @@ export function upsertJsonServer(file, name, entry, opts = {}) {
|
|
|
114
119
|
doc.mcpServers = servers;
|
|
115
120
|
let backup;
|
|
116
121
|
if (read?.lossy) {
|
|
117
|
-
backup =
|
|
122
|
+
backup = backupPath(file);
|
|
118
123
|
copyFileSync(file, backup);
|
|
124
|
+
// 원본에 예전 키가 있을 수 있다. 키 파일과 같이 주인만 읽게 둔다(POSIX). copyFileSync 는 원본의 권한을 따른다.
|
|
125
|
+
if (opts.secret && process.platform !== "win32")
|
|
126
|
+
chmodSync(backup, 0o600);
|
|
119
127
|
}
|
|
120
128
|
writeFileAtomic(file, `${JSON.stringify(doc, null, 2)}\n`, opts);
|
|
121
129
|
return { createdFile: read === null, replaced, ...(backup ? { backup } : {}) };
|
|
@@ -143,7 +151,7 @@ export function addToJsonArray(file, key, value) {
|
|
|
143
151
|
doc[key] = [...(cur ?? []), value];
|
|
144
152
|
let backup;
|
|
145
153
|
if (read?.lossy) {
|
|
146
|
-
backup =
|
|
154
|
+
backup = backupPath(file);
|
|
147
155
|
copyFileSync(file, backup);
|
|
148
156
|
}
|
|
149
157
|
writeFileAtomic(file, `${JSON.stringify(doc, null, 2)}\n`);
|
package/dist/doctor.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
//
|
|
8
8
|
// 키와 토큰 평문은 화면에도 파일에도 쓰지 않는다(앞 8자 + …). 클라이언트 목록은 다른 서비스의
|
|
9
9
|
// 주소(키가 든 주소가 있을 수 있다)를 옮기지 않도록 AiAkiv 항목만 고른다.
|
|
10
|
+
//
|
|
11
|
+
// 폴더의 설정 파일은 남이 준 것일 수 있다(받은 저장소). 그래서 폴더 파일이 고른 것은 그대로 믿지 않는다.
|
|
12
|
+
// - 키를 실어 보내는 주소는 AiAkiv 의 알려진 주소(기본 MCP 주소, AIAKIV_MCP_URL)와 명령 줄의 --allow-url 과
|
|
13
|
+
// 출처(방식, 호스트, 포트)가 같을 때만 쓴다. 홈의 전역 파일(setup 이 쓰는 곳)은 사용자 자신의 설정이라 그대로 본다.
|
|
14
|
+
// - bearer_token_env_var 는 CLI 가 AiAkiv 키 이름으로 정한 환경 변수(KEY_ENV_VARS)만 읽는다.
|
|
15
|
+
// 다른 것은 보내지도 읽지도 않고, 어느 파일의 무엇을 왜 건너뛰었는지 적는다(2026-11-10 보안 점검 중간 2).
|
|
16
|
+
// .aiakiv/console.json 의 control_url 을 믿지 않는 것(consoleKeyFile.ts)과 같은 원칙이다.
|
|
10
17
|
import { existsSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
18
|
import { join, resolve } from "node:path";
|
|
12
19
|
import { classifyClaudeName, CLAUDE_KIND_LABEL, findClaude, installedPluginIds, parseClaudeMcpList, } from "./clients/claude.js";
|
|
@@ -19,8 +26,8 @@ import { readTomlObject, tomlServers } from "./config/toml.js";
|
|
|
19
26
|
import { CONSOLE_KEY_FILE, expiryText, LEGACY_KEY_FILE_MESSAGE, readConsoleKeyFile } from "./consoleKeyFile.js";
|
|
20
27
|
import { probeSaveTarget } from "./mcpProbe.js";
|
|
21
28
|
import { failReason } from "./proc.js";
|
|
22
|
-
import { FOLDER_SKILLS_DIR, SKILL_NAMES, SKILLS_AGENT, skillsManualCommand } from "./skills.js";
|
|
23
|
-
import { controlBase, mcpBase } from "./urls.js";
|
|
29
|
+
import { bundledSkillsLine, FOLDER_SKILLS_DIR, SKILL_NAMES, SKILLS_AGENT, skillsManualCommand } from "./skills.js";
|
|
30
|
+
import { controlBase, DEFAULT_MCP_URL, mcpBase } from "./urls.js";
|
|
24
31
|
import { VERSION } from "./version.js";
|
|
25
32
|
export const DEFAULT_REPORT = "aiakiv-verification-test.md";
|
|
26
33
|
export const DOCTOR_CLIENT_IDS = [...CLIENT_IDS, "antigravity"];
|
|
@@ -32,10 +39,83 @@ export function isFolderClient(c) {
|
|
|
32
39
|
function isSetupClient(c) {
|
|
33
40
|
return GLOBAL_CLIENT_IDS.includes(c);
|
|
34
41
|
}
|
|
42
|
+
/** 폴더 설정 파일의 bearer_token_env_var 로 읽어도 되는 환경 변수. CLI 가 AiAkiv 키 이름으로 정한 것뿐이다
|
|
43
|
+
* (도움말의 "환경 변수" 단락). 다른 이름은 다른 서비스의 키일 수 있어 읽지 않는다. */
|
|
44
|
+
export const KEY_ENV_VARS = ["AIAKIV_API_KEY"];
|
|
45
|
+
/** 키를 보내도 되는 주소의 출처(`https://host[:port]`). 주소가 아니거나, http, https 가 아니거나,
|
|
46
|
+
* 사용자 정보(`user:pass@`)가 들었으면 null. 글자 앞부분이 아니라 풀어 낸 방식, 호스트, 포트로 비교하려고 쓴다. */
|
|
47
|
+
export function keyOrigin(raw) {
|
|
48
|
+
let u;
|
|
49
|
+
try {
|
|
50
|
+
u = new URL(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
56
|
+
return null;
|
|
57
|
+
if (u.username || u.password)
|
|
58
|
+
return null;
|
|
59
|
+
return u.origin;
|
|
60
|
+
}
|
|
61
|
+
/** 폴더 파일의 키를 보내도 되는 출처: 기본 MCP 주소, AIAKIV_MCP_URL, 명령 줄의 --allow-url. */
|
|
62
|
+
export function allowedKeyOrigins(env, allowUrls = []) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const u of [DEFAULT_MCP_URL, `${mcpBase(env)}/mcp`, ...allowUrls]) {
|
|
65
|
+
const o = keyOrigin(u);
|
|
66
|
+
if (o && !out.includes(o))
|
|
67
|
+
out.push(o);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** 이 항목의 키를 보내지 않을 까닭(보내도 되면 null). 전역 파일은 사용자 자신의 설정이라 보지 않는다. */
|
|
72
|
+
export function keySendRefusal(e, allowed) {
|
|
73
|
+
if (e.scope !== "폴더")
|
|
74
|
+
return null;
|
|
75
|
+
if (e.envRefused)
|
|
76
|
+
return e.envRefused;
|
|
77
|
+
const origin = keyOrigin(e.url);
|
|
78
|
+
if (origin && allowed.includes(origin))
|
|
79
|
+
return null;
|
|
80
|
+
let why;
|
|
81
|
+
try {
|
|
82
|
+
const u = new URL(e.url);
|
|
83
|
+
why = u.username || u.password
|
|
84
|
+
? "주소에 사용자 정보(…@)가 들어 있어"
|
|
85
|
+
: u.protocol !== "https:" && u.protocol !== "http:"
|
|
86
|
+
? "http, https 주소가 아니라"
|
|
87
|
+
: `AiAkiv 주소(${allowed.join(", ")})가 아니라`;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
why = "주소로 읽을 수 없어";
|
|
91
|
+
}
|
|
92
|
+
// 사용자 정보에는 비밀번호가 들 수 있어 가리고 적는다.
|
|
93
|
+
const shown = maskUrl(e.url.replace(/^([^:/?#]+:\/\/)[^/?#]*@/, "$1…@")) || "(비어 있음)";
|
|
94
|
+
return (`${e.file} 의 주소 ${shown} 는 ${why} 키를 보내지 않았습니다. 남이 준 폴더라면 이 설정 파일부터 확인하세요.` +
|
|
95
|
+
(origin ? ` 내 서버가 맞다면 npx aiakiv doctor --allow-url ${origin} 로 다시 확인하세요.` : ""));
|
|
96
|
+
}
|
|
35
97
|
/** 키 가리기: 앞 8자 + …(짧으면 전부 가린다). */
|
|
36
98
|
export function maskSecret(v) {
|
|
37
99
|
return v.length > 12 ? `${v.slice(0, 8)}…` : "…";
|
|
38
100
|
}
|
|
101
|
+
/** 가리기를 끄는 환경 변수. 값이 `1` 일 때만 끈다(예전처럼 홈 폴더 경로와 이메일을 그대로 적는다). */
|
|
102
|
+
export const DOCTOR_UNMASKED_ENV = "AIAKIV_DOCTOR_UNMASKED";
|
|
103
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
104
|
+
/** 결과 파일과 화면에서 사람을 알아볼 수 있는 값을 가린다. 사용자가 이 기록을 문의에 붙여 넣으므로
|
|
105
|
+
* 붙여 넣어도 되는 모양으로 만든다(2026-11-10 보안 점검 후속).
|
|
106
|
+
* - 홈 폴더 경로는 `~`(경로에 든 계정 이름이 빠진다). 홈이 드라이브나 `/` 그 자체면 건드리지 않는다.
|
|
107
|
+
* - 이메일은 앞 두 글자와 도메인만(`ra…@example.com`). 개인 팀 이름이 `<이름> personal` 이라 이메일이 들 수 있다.
|
|
108
|
+
* 키와 토큰은 따로 앞 8자만 적는다(maskSecret). */
|
|
109
|
+
export function maskPersonal(text, home, platform = process.platform) {
|
|
110
|
+
let out = text;
|
|
111
|
+
const h = (home ?? "").replace(/[\\/]+$/, "");
|
|
112
|
+
if (h && !/^[A-Za-z]:$/.test(h)) {
|
|
113
|
+
const forms = [...new Set([h, h.replace(/\\/g, "/"), h.replace(/\//g, "\\")])].sort((a, b) => b.length - a.length);
|
|
114
|
+
const re = new RegExp(`(?<![\\w.-])(?:${forms.map(escapeRe).join("|")})(?![\\w.-])`, platform === "win32" ? "gi" : "g");
|
|
115
|
+
out = out.replace(re, "~");
|
|
116
|
+
}
|
|
117
|
+
return out.replace(/([A-Za-z0-9._%+-]+)@([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,})/g, (_m, local, domain) => `${local.slice(0, 2)}…@${domain}`);
|
|
118
|
+
}
|
|
39
119
|
/** 주소의 키처럼 보이는 쿼리 값을 가린다. */
|
|
40
120
|
export function maskUrl(url) {
|
|
41
121
|
return url.replace(/([?&][^=&#]*(?:key|token|secret|auth)[^=&#]*=)([^&#]*)/gi, (_m, k) => `${k}…`);
|
|
@@ -60,7 +140,18 @@ export function classifyEntry(base, raw, env) {
|
|
|
60
140
|
if (bearer)
|
|
61
141
|
return { ...base, url, auth: "apikey", key: bearer, keyMasked: maskSecret(bearer) };
|
|
62
142
|
if (typeof raw.bearer_token_env_var === "string") {
|
|
63
|
-
const
|
|
143
|
+
const name = raw.bearer_token_env_var;
|
|
144
|
+
if (base.scope === "폴더" && !KEY_ENV_VARS.includes(name)) {
|
|
145
|
+
return {
|
|
146
|
+
...base,
|
|
147
|
+
url,
|
|
148
|
+
auth: "apikey-env",
|
|
149
|
+
envVar: name,
|
|
150
|
+
envRefused: `${base.file} 이 고른 환경 변수 ${name} 은 AiAkiv 키 이름(${KEY_ENV_VARS.join(", ")})이 아니라 읽지 않았습니다. ` +
|
|
151
|
+
"남이 준 폴더라면 이 설정 파일부터 확인하세요.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const v = env[name];
|
|
64
155
|
return {
|
|
65
156
|
...base,
|
|
66
157
|
url,
|
|
@@ -306,6 +397,8 @@ function howLabel(e) {
|
|
|
306
397
|
if (e.auth === "apikey")
|
|
307
398
|
return `${AUTH_LABEL[e.auth]} ${e.keyMasked}`;
|
|
308
399
|
if (e.auth === "apikey-env") {
|
|
400
|
+
if (e.envRefused)
|
|
401
|
+
return `${AUTH_LABEL[e.auth]} ${e.envVar} (읽지 않음)`;
|
|
309
402
|
return `${AUTH_LABEL[e.auth]} ${e.envVar}${e.keyMasked ? ` = ${e.keyMasked}` : " (비어 있음)"}`;
|
|
310
403
|
}
|
|
311
404
|
if (e.auth === "oauth-folder")
|
|
@@ -322,10 +415,14 @@ export async function runDoctor(ctx, opts) {
|
|
|
322
415
|
const outFile = opts.out ? resolve(ctx.cwd, opts.out) : join(dir, DEFAULT_REPORT);
|
|
323
416
|
const scan = scanConfigs(ctx, dir, clients);
|
|
324
417
|
const probes = new Map();
|
|
418
|
+
const allowed = allowedKeyOrigins(ctx.env, opts.allowUrls);
|
|
325
419
|
for (const e of scan.entries) {
|
|
326
420
|
if (e.auth !== "apikey" && e.auth !== "apikey-env")
|
|
327
421
|
continue;
|
|
328
|
-
|
|
422
|
+
const refused = keySendRefusal(e, allowed);
|
|
423
|
+
if (refused)
|
|
424
|
+
probes.set(e, refused);
|
|
425
|
+
else if (!e.key)
|
|
329
426
|
probes.set(e, `환경 변수 ${e.envVar} 가 비어 있어 부르지 않았습니다`);
|
|
330
427
|
else if (opts.noProbe)
|
|
331
428
|
probes.set(e, "--no-probe 라 부르지 않았습니다");
|
|
@@ -337,13 +434,21 @@ export async function runDoctor(ctx, opts) {
|
|
|
337
434
|
const folderSkills = folderSkillChecks(dir, clients);
|
|
338
435
|
const enabled = claudeEnabledChecks(dir, scan.entries);
|
|
339
436
|
const consoleKey = consoleKeyLine(dir);
|
|
437
|
+
const bundled = bundledSkillsLine();
|
|
438
|
+
const unmasked = (ctx.env[DOCTOR_UNMASKED_ENV] ?? "").trim() === "1";
|
|
439
|
+
const mask = (s) => (unmasked ? s : maskPersonal(s, ctx.home));
|
|
340
440
|
const md = [];
|
|
341
441
|
md.push("# AiAkiv 확인 기록", "");
|
|
342
442
|
md.push(`- 일시: ${new Date().toISOString()}`);
|
|
343
443
|
md.push(`- 폴더: ${dir}`);
|
|
344
444
|
md.push(`- 확인한 클라이언트: ${clients.join(", ")}`);
|
|
345
445
|
md.push(`- aiakiv CLI ${VERSION}, MCP 기준 주소 ${mcpBase(ctx.env)}/mcp, 웹 콘솔 ${controlBase(ctx.env)}`);
|
|
346
|
-
md.push(
|
|
446
|
+
md.push(`- ${bundled}(bind 와 setup 이 쓰는 스킬)`);
|
|
447
|
+
md.push(`- 폴더 설정 파일의 키를 보내는 주소: ${allowed.join(", ")}(그 밖의 주소에는 보내지 않습니다)`);
|
|
448
|
+
md.push("- 키와 토큰은 앞 8자만 적었습니다.");
|
|
449
|
+
if (!unmasked)
|
|
450
|
+
md.push("- 홈 폴더는 ~ 로, 이메일은 앞 두 글자와 도메인만 적었습니다. 문의에 그대로 붙여 넣어도 됩니다.");
|
|
451
|
+
md.push("");
|
|
347
452
|
md.push("## 설정 파일의 AiAkiv 연결", "");
|
|
348
453
|
if (scan.entries.length) {
|
|
349
454
|
md.push("| 범위 | 클라이언트 | 파일 | 서버 이름 | 주소 | 인증 방식 |", "|---|---|---|---|---|---|");
|
|
@@ -469,37 +574,39 @@ export async function runDoctor(ctx, opts) {
|
|
|
469
574
|
md.push("");
|
|
470
575
|
let wrote = true;
|
|
471
576
|
try {
|
|
472
|
-
writeFileSync(outFile, md.join("\n"), "utf8");
|
|
577
|
+
writeFileSync(outFile, mask(md.join("\n")), "utf8");
|
|
473
578
|
}
|
|
474
579
|
catch (e) {
|
|
475
580
|
wrote = false;
|
|
476
|
-
ctx.err(`결과 파일을 쓰지 못했습니다(${e.message}).`);
|
|
581
|
+
ctx.err(mask(`결과 파일을 쓰지 못했습니다(${e.message}).`));
|
|
477
582
|
}
|
|
478
|
-
// 화면 요약.
|
|
479
|
-
ctx.out(
|
|
583
|
+
// 화면 요약. 결과 파일과 같이 가린다(화면을 복사해 붙여 넣는 사람도 있다).
|
|
584
|
+
const say = (s) => ctx.out(mask(s));
|
|
585
|
+
say(`AiAkiv 확인: ${dir}`);
|
|
480
586
|
if (!scan.entries.length)
|
|
481
|
-
|
|
587
|
+
say(" 설정 파일에 AiAkiv 연결 없음");
|
|
482
588
|
for (const e of scan.entries) {
|
|
483
589
|
const p = probes.get(e);
|
|
484
590
|
const tail = p === undefined ? "" : `, 확인 결과 ${typeof p === "string" ? p : probeLine(p, e)}`;
|
|
485
|
-
|
|
591
|
+
say(` [${e.scope} ${e.client}] ${e.name}: ${howLabel(e)}${tail}`);
|
|
486
592
|
}
|
|
487
593
|
for (const n of scan.notes)
|
|
488
|
-
|
|
594
|
+
say(` ${n}`);
|
|
489
595
|
for (const s of lists)
|
|
490
|
-
|
|
491
|
-
|
|
596
|
+
say(` ${s.title}: ${s.lines.join(" / ")}`);
|
|
597
|
+
say(` 스킬: ${skills.length ? skills.map((s) => `${s.root}(${s.names.length})`).join(", ") : "찾지 못함"}`);
|
|
492
598
|
if (folderSkills.length)
|
|
493
|
-
|
|
599
|
+
say(` 폴더 스킬: ${folderSkills.map((s) => `${s.rel} ${skillState(s)}`).join(", ")}`);
|
|
600
|
+
say(` ${bundled}`);
|
|
494
601
|
if (consoleKey)
|
|
495
|
-
|
|
602
|
+
say(` ${consoleKey}`);
|
|
496
603
|
for (const c of enabled.checks) {
|
|
497
|
-
|
|
604
|
+
say(` enabledMcpjsonServers ${c.name}: ${c.in.length ? c.in.join(", ") : "없음"}`);
|
|
498
605
|
}
|
|
499
606
|
for (const n of next)
|
|
500
|
-
|
|
607
|
+
say(` 다음: ${n}`);
|
|
501
608
|
if (wrote)
|
|
502
|
-
|
|
609
|
+
say(`결과 파일: ${outFile}`);
|
|
503
610
|
const failed = [...probes.values()].some((p) => typeof p !== "string" && !p.ok);
|
|
504
611
|
return failed || !wrote ? 1 : 0;
|
|
505
612
|
}
|
package/dist/main.js
CHANGED
|
@@ -5,13 +5,12 @@ import { CLIENT_IDS } from "./clients/types.js";
|
|
|
5
5
|
import { runBind } from "./bind.js";
|
|
6
6
|
import { ConsoleUsageError, runConsole, runConsoleKey } from "./console.js";
|
|
7
7
|
import { runDashboard } from "./dashboard.js";
|
|
8
|
-
import { DEFAULT_REPORT, DOCTOR_CLIENT_IDS, runDoctor } from "./doctor.js";
|
|
8
|
+
import { DEFAULT_REPORT, DOCTOR_CLIENT_IDS, keyOrigin, runDoctor } from "./doctor.js";
|
|
9
9
|
import { DEFAULT_PRECHECK_CONFIG, DEFAULT_PRECHECK_OUT, runEnterprisePrecheck } from "./enterprise/precheck.js";
|
|
10
10
|
import { realHost } from "./local/checks.js";
|
|
11
11
|
import { DEFAULT_ENGINE_PORT, runLocalDoctor } from "./local/doctor.js";
|
|
12
12
|
import { CONTAINER_EXPORT_DIR, CONTAINER_IMPORT_DIR, runLocalExport, runLocalImport } from "./local/portable.js";
|
|
13
13
|
import { runSetup } from "./setup.js";
|
|
14
|
-
import { DEFAULT_SKILLS_TARBALL_URL } from "./skills.js";
|
|
15
14
|
import { DEFAULT_CONTROL_URL, DEFAULT_MCP_URL } from "./urls.js";
|
|
16
15
|
import { ENTERPRISE_ENGINE_VERSION, VERSION } from "./version.js";
|
|
17
16
|
export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정하고 확인하고, 전역 설치와 콘솔 작업을 하는 명령
|
|
@@ -21,6 +20,7 @@ export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정
|
|
|
21
20
|
[--expires-days N] [--no-browser] [--no-skills]
|
|
22
21
|
npx aiakiv setup --client gemini|antigravity[,…] [--expires-days N] [--no-browser] [--no-skills] [--new-key]
|
|
23
22
|
npx aiakiv doctor [--client <클라이언트>[,…]] [--dir <폴더>] [--out ${DEFAULT_REPORT}] [--no-probe]
|
|
23
|
+
[--allow-url <주소>]
|
|
24
24
|
npx aiakiv console <액션> [--data '<json>' | --data @파일.json] [--json] [--dir <폴더>] [--yes]
|
|
25
25
|
npx aiakiv console describe [--json] [--dir <폴더>]
|
|
26
26
|
npx aiakiv console key [--expires-days N] [--dir <폴더>] [--no-browser]
|
|
@@ -35,22 +35,24 @@ export const HELP = `aiakiv ${VERSION}: 폴더에 AiAkiv 프로젝트를 고정
|
|
|
35
35
|
bind 폴더에 프로젝트를 고정합니다. 웹 콘솔의 "폴더 ZIP 받기" 와 같은 한 벌을 그 폴더에 직접 씁니다.
|
|
36
36
|
claude, cursor 는 ?project= 주소(OAuth)를 쓰고, codex, gemini 는 브라우저에서 확인 버튼을 눌러
|
|
37
37
|
프로젝트 키를 받아 씁니다. 키 평문은 화면에 내지 않고 그 폴더의 설정 파일에만 씁니다.
|
|
38
|
-
함께 쓰는 것: 폴더 스킬 여덟 개(claude 는 .claude/skills,
|
|
39
|
-
--no-skills 면 건너뜀), claude 는 .claude/settings.json 의 enabledMcpjsonServers,
|
|
38
|
+
함께 쓰는 것: 폴더 스킬 여덟 개(이 CLI 에 든 것이라 내려받지 않습니다. claude 는 .claude/skills,
|
|
39
|
+
나머지는 .agents/skills, --no-skills 면 건너뜀), claude 는 .claude/settings.json 의 enabledMcpjsonServers,
|
|
40
40
|
codex, gemini 는 키가 든 설정 파일을 .gitignore 에(git 저장소이거나 .gitignore 가 있을 때).
|
|
41
41
|
Antigravity 는 앱 전체에 설정 하나라 폴더 고정이 없습니다(setup 을 쓰세요).
|
|
42
42
|
setup Gemini CLI 와 Antigravity 의 전역 설치를 합니다. 브라우저에서 확인 버튼을 눌러 Main(저장 대상)을
|
|
43
43
|
따라가는 계정 전체 기억 키를 받고, 고른 클라이언트 모두의 전역 설정에 서버 이름 AiAkiv 로 씁니다.
|
|
44
44
|
Gemini CLI 는 ~/.gemini/settings.json(httpUrl), Antigravity 는 ~/.gemini/config/mcp_config.json
|
|
45
|
-
(serverUrl). 전역 스킬도 씁니다(Gemini CLI 는 ~/.gemini/skills, Antigravity 는 ~/.gemini/config/skills
|
|
45
|
+
(serverUrl). 전역 스킬도 씁니다(이 CLI 에 든 것. Gemini CLI 는 ~/.gemini/skills, Antigravity 는 ~/.gemini/config/skills
|
|
46
46
|
와, ~/.gemini/antigravity-cli 가 있으면 그 아래 skills. --no-skills 면 건너뜀).
|
|
47
47
|
계정 전체 키는 한 사람에게 하나만 살아 있어, 새로 받으면 앞선 키(다른 PC 것 포함)는 폐기됩니다.
|
|
48
48
|
그래서 두 전역 파일에 쓸 수 있는 키(binding main)가 이미 있으면 브라우저를 열지 않고 그 키를
|
|
49
49
|
다시 씁니다. --new-key 면 새로 받습니다. --expires-days 가 없으면 만료가 없습니다.
|
|
50
|
-
doctor 설정 파일, 클라이언트 등록 목록, 스킬
|
|
50
|
+
doctor 설정 파일, 클라이언트 등록 목록, 스킬 위치, 이 CLI 에 든 스킬의 버전을 확인하고 결과를 마크다운 파일로 씁니다.
|
|
51
51
|
키로 묶인 연결은 get_save_target 을 직접 불러 팀, 프로젝트, binding 을 적습니다.
|
|
52
52
|
--client 는 bind 의 넷과 antigravity 를 받습니다(기본은 모두). setup 이 쓴 전역 키는 binding 이
|
|
53
|
-
main 이어야 합니다.
|
|
53
|
+
main 이어야 합니다. 폴더 설정 파일은 남이 준 것일 수 있어, 그 파일의 키는 AiAkiv 주소(기본 MCP 주소나
|
|
54
|
+
AIAKIV_MCP_URL)에만 보내고 환경 변수는 AIAKIV_API_KEY 만 읽습니다. 자체 서버를 점검할 때는
|
|
55
|
+
--allow-url <주소> 로 그 주소를 직접 주세요(여러 번 줄 수 있습니다).
|
|
54
56
|
console 웹 콘솔의 작업(팀, 초대, 프로젝트, Main 전환, 연결, 카드 등)을 실행합니다. 액션 이름과
|
|
55
57
|
인자는 console describe 가 보여 줍니다. 별칭: teams, projects, links, permissions.
|
|
56
58
|
읽기는 bind 가 받은 프로젝트 키(그 키의 팀만)나 계정 키로 됩니다. 쓰기는 계정 키와, 콘솔
|
|
@@ -127,7 +129,6 @@ console 과 dashboard 의 종료 코드:
|
|
|
127
129
|
AIAKIV_MCP_URL MCP 주소(기본 ${DEFAULT_MCP_URL})
|
|
128
130
|
AIAKIV_CONTROL_URL 웹 콘솔 주소(기본 ${DEFAULT_CONTROL_URL})
|
|
129
131
|
AIAKIV_API_KEY console, dashboard 가 먼저 쓰는 키
|
|
130
|
-
AIAKIV_SKILLS_TARBALL_URL 폴더 스킬과 전역 스킬을 받는 주소(기본 ${DEFAULT_SKILLS_TARBALL_URL})
|
|
131
132
|
AIAKIV_ENTERPRISE_MANIFEST_URL enterprise precheck 가 이미지를 내려받을 때 읽는 목록 파일 주소
|
|
132
133
|
|
|
133
134
|
공통: -h, --help -v, --version`;
|
|
@@ -196,6 +197,15 @@ function parseDoctorClients(values) {
|
|
|
196
197
|
return c;
|
|
197
198
|
});
|
|
198
199
|
}
|
|
200
|
+
/** doctor 의 --allow-url. 사용자가 직접 준 주소라 받되, 키를 보낼 수 있는 주소(http, https, 사용자 정보 없음)만. */
|
|
201
|
+
function parseAllowUrls(values) {
|
|
202
|
+
return (values ?? []).map((v) => {
|
|
203
|
+
if (!keyOrigin(v.trim())) {
|
|
204
|
+
throw new UsageError(`--allow-url 은 http 나 https 주소이고 사용자 정보(…@)가 없어야 합니다(받은 값: ${v}).`);
|
|
205
|
+
}
|
|
206
|
+
return v.trim();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
199
209
|
export async function main(argv, ctx) {
|
|
200
210
|
const [cmd, ...rest] = argv;
|
|
201
211
|
try {
|
|
@@ -283,6 +293,7 @@ export async function main(argv, ctx) {
|
|
|
283
293
|
dir: { type: "string" },
|
|
284
294
|
out: { type: "string" },
|
|
285
295
|
"no-probe": { type: "boolean", default: false },
|
|
296
|
+
"allow-url": { type: "string", multiple: true },
|
|
286
297
|
help: { type: "boolean", short: "h" },
|
|
287
298
|
},
|
|
288
299
|
strict: true,
|
|
@@ -290,7 +301,13 @@ export async function main(argv, ctx) {
|
|
|
290
301
|
if (values.help)
|
|
291
302
|
return (ctx.out(HELP), 0);
|
|
292
303
|
const clients = parseDoctorClients(values.client);
|
|
293
|
-
return await runDoctor(ctx, {
|
|
304
|
+
return await runDoctor(ctx, {
|
|
305
|
+
clients,
|
|
306
|
+
dir: values.dir,
|
|
307
|
+
out: values.out,
|
|
308
|
+
noProbe: values["no-probe"],
|
|
309
|
+
allowUrls: parseAllowUrls(values["allow-url"]),
|
|
310
|
+
});
|
|
294
311
|
}
|
|
295
312
|
if (cmd === "console" && rest[0] === "key") {
|
|
296
313
|
const dropped = rest.slice(1).map((a) => /^--(project|manage|team)(=|$)/.exec(a)?.[1]).find(Boolean);
|
package/dist/mcpProbe.js
CHANGED
|
@@ -56,6 +56,29 @@ export function saveTargetFromResult(result) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
const str = (v) => (typeof v === "string" ? v : v == null ? undefined : String(v));
|
|
59
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
60
|
+
const MAX_REDIRECTS = 5;
|
|
61
|
+
/** 넘겨주기(redirect)는 같은 출처(방식, 호스트, 포트) 안의 307, 308 만 따라간다(서버의 `/` → `/mcp` 308 같은 것).
|
|
62
|
+
* 다른 출처로 넘기면 키를 싣지 않고 멈춘다. fetch 에 맡기면 Node 18 초기 판은 다른 호스트로 넘어갈 때도
|
|
63
|
+
* Authorization 을 그대로 실었다. 301, 302, 303 은 POST 를 GET 으로 바꾸는 응답이라 따라가지 않고 그대로 돌려준다. */
|
|
64
|
+
async function fetchSameOrigin(fetchImpl, url, init) {
|
|
65
|
+
let current = url;
|
|
66
|
+
for (let hop = 0;; hop += 1) {
|
|
67
|
+
const res = await fetchImpl(current, { ...init, redirect: "manual" });
|
|
68
|
+
if (!REDIRECTS.includes(res.status))
|
|
69
|
+
return res;
|
|
70
|
+
const location = res.headers.get("location");
|
|
71
|
+
if (!location || hop >= MAX_REDIRECTS || (res.status !== 307 && res.status !== 308))
|
|
72
|
+
return res;
|
|
73
|
+
const next = new URL(location, current);
|
|
74
|
+
if (next.origin !== new URL(current).origin) {
|
|
75
|
+
await res.text().catch(() => "");
|
|
76
|
+
throw new Error(`서버가 다른 주소(${next.origin})로 넘기려 해서 키를 보내지 않고 멈췄습니다`);
|
|
77
|
+
}
|
|
78
|
+
await res.text().catch(() => "");
|
|
79
|
+
current = next.href;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
59
82
|
export async function probeSaveTarget(fetchImpl, url, key, opts = {}) {
|
|
60
83
|
const timeoutMs = opts.timeoutMs ?? 20_000;
|
|
61
84
|
let session = null;
|
|
@@ -67,7 +90,7 @@ export async function probeSaveTarget(fetchImpl, url, key, opts = {}) {
|
|
|
67
90
|
"MCP-Protocol-Version": protocol,
|
|
68
91
|
...(session ? { "Mcp-Session-Id": session } : {}),
|
|
69
92
|
});
|
|
70
|
-
const post = async (msg) => fetchImpl
|
|
93
|
+
const post = async (msg) => fetchSameOrigin(fetchImpl, url, {
|
|
71
94
|
method: "POST",
|
|
72
95
|
headers: headers(),
|
|
73
96
|
body: JSON.stringify(msg),
|
|
@@ -127,7 +150,7 @@ export async function probeSaveTarget(fetchImpl, url, key, opts = {}) {
|
|
|
127
150
|
}
|
|
128
151
|
finally {
|
|
129
152
|
if (session) {
|
|
130
|
-
await fetchImpl
|
|
153
|
+
await fetchSameOrigin(fetchImpl, url, { method: "DELETE", headers: headers(), signal: AbortSignal.timeout(5_000) }).catch(() => undefined);
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
156
|
}
|