aiakiv 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,10 @@ Gemini CLI 와 Antigravity 는 전역 설치도 합니다. 설계는 K2G `docs/2
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
9
  npx aiakiv doctor [--client <클라이언트>[,...]] [--dir <폴더>] [--out aiakiv-verification-test.md] [--no-probe]
10
+ npx aiakiv local export <팀> --to <파일|폴더> [--domain <이름>] [--yes] [--container <이름>]
11
+ npx aiakiv local import <파일|폴더> --team <팀> [--as <사용자>] [--domain <이름>] [--dry-run] [--resume] [--yes] [--container <이름>]
12
+ npx aiakiv enterprise precheck [--config <toml>] [--image <참조> | --image-tar <파일>] [--device] [--no-browser] [--no-login]
13
+ [--only <영역,...>] [--skip <id 접두어,...>] [--redact-claims] [--out <폴더>] [--json]
10
14
  ```
11
15
 
12
16
  전역 설치(계정 연결과 전역 스킬)는 Gemini CLI 와 Antigravity 만 `setup` 으로 합니다. 그 밖은 Claude Code 는
@@ -172,6 +176,107 @@ CLI 는 폐기된 개수와 키 id 를 알립니다. 그래서 쓸 수 있는
172
176
  `--no-probe` 면 부르지 않습니다. OAuth 항목은 "클라이언트 안에서 get_save_target 을 부르라" 는 다음 단계를 적습니다.
173
177
  - 결과는 `aiakiv-verification-test.md`(기본은 그 폴더)에 씁니다. 키로 묶인 연결이 실패하면 종료 코드 1 입니다.
174
178
 
179
+ ## local export, local import: 자체 설치판 팀 기억 내보내기와 받기
180
+
181
+ 자체 설치판(무료 5인 엔진) 팀의 기억을 `.mweft.tar.gz` 파일로 내보내고, 그 파일(또는 호스티드에서 내보낸 파일)을 팀에
182
+ 가져옵니다. 본체는 엔진 컨테이너 안의 `aiakiv-admin export`, `aiakiv-admin import` 이고, 이 명령은 지금까지 손으로 하던
183
+ `docker compose exec engine aiakiv-admin ...` 와 `docker compose cp` 를 대신합니다. 설계는 K2G
184
+ `docs/211_blue_print-172-free_engine_5_users.md` §5.1, §5.2, §7, 엔진 명령의 동작은 K2G `packaging/selfhost/aiakiv-admin.md`
185
+ §5.10, §5.11 에 있습니다.
186
+
187
+ ```
188
+ npx aiakiv local export 연구팀 --to ./연구팀.mweft.tar.gz
189
+ npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 --dry-run # 계획만(종료 코드 0)
190
+ npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 # --yes 가 없어 계획만(종료 코드 2)
191
+ npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 --yes # 줄마다 진행, 마지막 줄이 결과
192
+ npx aiakiv local import ./연구팀.mweft.tar.gz --team 새팀 --resume # 재임베딩이 끊겼을 때
193
+ ```
194
+
195
+ 1. **엔진 컨테이너 찾기**: `docker info` 로 Docker 를 보고, `docker ps -a` 에서 엔진 이미지(`aiakiv/engine`,
196
+ `aiakiv/engine-enterprise`)로 도는 compose 서비스 `engine`(또는 compose 밖에서 띄운 것)을 찾습니다. 같은 이미지로 도는
197
+ 유료 기업판 `control` 은 고르지 않습니다. 떠 있는 엔진이 여럿이면 종료 코드 2 로 멈추고, `--container <이름>` 으로
198
+ 고릅니다(이름이나 id 앞부분, 이때는 이미지를 따지지 않습니다).
199
+ 2. **경로**: `--to` 와 import 의 `<파일>` 은 이 컴퓨터의 경로이고 지금 폴더를 기준으로 풉니다(Windows 의 드라이브 문자,
200
+ 역슬래시, 공백 그대로). 엔진에는 이 명령이 컨테이너 안에 만든 임시 폴더(`/data/exports/aiakiv-cli-<시각>-<무작위>`,
201
+ `/data/imports/aiakiv-cli-<시각>-<무작위>`)의 경로만 넘깁니다. `docker cp` 에는 호스트 쪽을 늘 절대 경로로 줍니다.
202
+ 3. **export**: 임시 폴더에 `aiakiv-admin export <팀> --to <임시 폴더> --yes --json [--domain]` 을 돌리고(새로 만든 빈
203
+ 폴더라 엔진에는 `--yes` 를 줍니다), 결과를 `docker cp` 로 꺼냅니다. `--to` 가 이미 있는 폴더이거나 끝이 `/`(Windows 는
204
+ `\` 도)면 그 안에 엔진이 지은 이름(`<팀>_<도메인>_<시각>.mweft.tar.gz`)으로 둡니다. 이미 있는 파일은 `--yes` 가 있어야
205
+ 덮어씁니다(이 검사는 Docker 를 부르기 전에 합니다). 팀의 도메인이 여럿이면 엔진이 묶음(아카이브 여럿과
206
+ `org_manifest.json`)을 내고, 그 내용을 `--to` 폴더에 꺼냅니다(`--to` 가 파일이면 종료 코드 2).
207
+ 4. **import**: 호스트 파일(또는 묶음 폴더)을 임시 폴더로 `docker cp` 하고 `aiakiv-admin import <컨테이너 경로> --team <팀>
208
+ [--as] [--domain] [--dry-run] [--resume] [--yes] --json` 을 부릅니다. 묻지 않습니다. `--yes` 가 없으면 엔진이 계획만 내고
209
+ 종료 코드 2 로 끝납니다(같은 명령에 `--yes` 를 붙여 다시). `--resume` 은 끊긴 재임베딩을 이어서 하며 같은 호스트 파일을
210
+ 다시 옮겨 줍니다(엔진은 파일 이름이 아니라 내용 지문으로 가져오기 기록을 찾습니다).
211
+ 5. **임시 폴더 지우기**: 끝나면 컨테이너 안 임시 폴더를 지웁니다(`docker exec -u 0 ... rm -rf`, `docker cp` 로 넣은 파일이
212
+ root 소유라서). 엔진은 `--to` 를 생략한 내보내기를 데이터 볼륨에 남겨 두지만, 이 명령의 임시 폴더는 옮기려고 만든
213
+ 것이라 남기면 데이터 볼륨만 찹니다(벡터가 실려 큽니다). 예외로 export 가 호스트로 꺼내지 못하면 파일을 남기고 손으로
214
+ 꺼내는 `docker cp` 명령을 알립니다. 명령을 중간에 끊으면(Ctrl+C) 임시 폴더가 남을 수 있습니다.
215
+ 6. **출력**: 표준 출력에는 JSON 만 냅니다(`--json` 은 붙여도 같습니다). export 는 한 문서
216
+ `{"ok":true,"command":"local export","kind":"archive|bundle","path":<호스트 경로>,"container":{"name","path","removed"},"engine":<엔진의 결과 그대로>}`.
217
+ import 는 이 명령의 줄 `{"step":"cli.copy","done":1,"total":1,"container":…,"path":<컨테이너 경로>}` 뒤에 엔진의 줄(진행
218
+ `{"step","done","total"}`, 마지막 줄이 결과)을 받는 대로 바꾸지 않고 냅니다. 엔진의 실패 JSON(이유, 남은 수, record id)도
219
+ 그대로입니다. 이 명령 자신의 실패는 `{"ok":false,"error":…,"message":…,"fix":…}` 한 문서이고, 같은 문장을 표준 오류에도
220
+ 냅니다. 엔진의 로그와 JSON 이 아닌 줄은 표준 오류로 냅니다.
221
+ 7. **벡터**: 이 명령은 파일 내용을 읽거나 바꾸지 않습니다. 벡터를 싣고 버리고 다시 만드는 일은 엔진이 합니다(호스티드에서
222
+ 내보낸 파일은 벡터가 없어 가져올 때 엔진이 다시 임베딩합니다).
223
+
224
+ 종료 코드: 엔진의 것을 그대로 돌려줍니다(0 성공, 2 입력 오류나 `--yes` 없이 계획만, 3 거절, 4 엔진의 DB 연결 실패,
225
+ 5 가져오기 검증 실패, 1 예기치 못한 오류). 이 명령의 것은 enterprise precheck 와 같은 번호로 2 인자 오류, 파일 없음, 이미
226
+ 있는 파일, 엔진이 여럿, 4 Docker 를 쓸 수 없음(실행 파일 없음, 데몬에 닿지 못함)이나 엔진 컨테이너가 떠 있지 않음,
227
+ 1 `docker cp` 나 `docker exec` 실패, 엔진이 결과 없이 끝남입니다. 오류의 종류는 JSON 의 `error` 로 가릅니다.
228
+
229
+ ## enterprise precheck: 유료 기업판 설치 전 점검
230
+
231
+ 유료 기업판을 깔기 전에 회사의 신원 서버(OIDC), 데이터베이스, 파일 저장소, 메일 서버에 실제로 연결해 보고 보고서를
232
+ 냅니다. 점검 본체는 유료 기업판 엔진 이미지 안의 `aiakiv-admin precheck` 이고, 이 명령은 Docker 로 그것을 돌립니다.
233
+ 설계는 K2G `docs/212_blue_print-173-enterprise_precheck_login.md` §3, 점검의 동작은 K2G
234
+ `packaging/enterprise/precheck/README.md` §2 에 있습니다. 라이선스 없이 돕니다.
235
+
236
+ ```
237
+ npx aiakiv enterprise precheck --config ./aiakiv-enterprise.toml
238
+ ```
239
+
240
+ 1. **설정 파일**: `--config`(기본 `./aiakiv-enterprise.toml`). 없으면 종료 코드 2 와 함께 예시 파일
241
+ `aiakiv-enterprise.example.toml` 을 복사해 고치라고 안내합니다. 설정 파일이 있는 폴더가 작업 폴더이고 컨테이너의
242
+ `/config` 로 붙습니다(설정의 `ca_bundle = "/config/corp-ca.pem"` 은 그 폴더의 `corp-ca.pem` 입니다). 같은 폴더의
243
+ `.env` 가 있으면 compose 의 `env_file` 로 넘깁니다. 비밀 값은 설정에서 `env:이름` 으로 가리킵니다. 이 명령은 `.env`
244
+ 를 열어 읽지 않습니다.
245
+ 2. **Docker**: docker 실행 파일, 데몬, compose v2, 리눅스 컨테이너 모드를 봅니다. 안 되면 종료 코드 4 와 설치나 켜는 법.
246
+ 3. **이미지**(이 순서):
247
+ - `--image <참조>`: 이 컴퓨터에 이미 있는 이미지(`docker image inspect`). 없으면 종료 코드 5.
248
+ - `--image-tar <파일>`: 내용 지문(sha256)을 계산해 같은 폴더의 `<파일>.sha256`(있으면, `<지문>` 이나
249
+ `<지문> <파일 이름>` 형식)과 비교합니다. 다르면 종료 코드 5. 맞으면 `docker load` 하고 그 출력의 이미지 이름을 씁니다.
250
+ - 둘 다 없으면 이 CLI 판에 고정한 `aiakiv/engine-enterprise:<판>`(지금 `0.1.0`, `src/version.ts` 의
251
+ `ENTERPRISE_ENGINE_VERSION`)이 있으면 그것을 씁니다. 없으면 `https://www.aiakiv.com/download/enterprise/<판>/manifest.json`
252
+ 을 받아 Docker 의 아키텍처(amd64, arm64)에 맞는 tar 를 `~/.aiakiv/cache/` 에 내려받고 지문을 확인한 뒤 `docker load`
253
+ 합니다. 목록에 닿지 못하면 종료 코드 5 와 "홈페이지(https://www.aiakiv.com/enterprise)에서 점검용 이미지를 내려받아
254
+ --image-tar 로 넣으세요" 를 냅니다.
255
+ 4. **compose 파일**: `<작업 폴더>/.aiakiv-precheck/compose.yml` 을 돌릴 때마다 다시 씁니다. K2G `precheck-compose.yml` 의
256
+ `precheck` 서비스와 같은 내용(entrypoint `aiakiv-admin precheck`, healthcheck 끔, 포트 `127.0.0.1:7777:7777`)이고,
257
+ 이미지는 3에서 정한 참조로 고정하며 받으러 나가지 않습니다(`pull_policy: never`). 볼륨은 Windows 드라이브 문자와
258
+ 헷갈리지 않게 긴 형식(`type: bind`, `source`, `target: /config`)으로 적고, Windows 경로의 역슬래시는 `/` 로 바꿉니다.
259
+ 동봉 Keycloak 서비스는 넣지 않습니다.
260
+ 5. **실행**: `docker compose -p aiakiv-precheck -f <그 파일> run --rm --service-ports -T precheck --config /config/<설정 파일>
261
+ --json` 에 `--device`, `--no-login`, `--only`, `--skip`, `--redact-claims` 를 그대로 붙입니다.
262
+ 6. **로그인**: 점검이 로그인 대기 줄을 내면 로그인 주소(기기 코드 방식이면 주소와 코드)를 눈에 띄게 보이고 브라우저를
263
+ 엽니다. `--device` 나 `--no-browser` 면 열지 않고 주소만 보입니다. 비밀번호는 다루지 않습니다. 점검은 로그인을 600초
264
+ 기다립니다(점검 본체의 기본값).
265
+ 7. **보고서**: 점검의 마지막 줄(보고서 JSON)을 받아 이 CLI 가 `<작업 폴더>/<--out>/` 에 `report.json` 과 `report.md` 를
266
+ 씁니다(`--out` 기본 `report`, 작업 폴더 안의 상대 경로만, 밖이면 종료 코드 2). 컨테이너에는 `--out` 을 넘기지 않습니다.
267
+ Linux 에서 점검 컨테이너(사용자 10001)가 붙인 폴더에 쓰지 못하는 문제를 피하려는 것이고, 두 파일의 이름과 형식은 점검
268
+ 본체의 `--out` 과 같습니다. `report.md` 의 말은 이 CLI 의 환경 변수 `AIAKIV_LANGUAGE`, 설정 파일 맨 위의 `language`,
269
+ 기본 `en` 순서로 정합니다.
270
+ 8. **화면**: 기본은 점검마다 한 줄(상태, id, 걸린 시간, 내용), 끝에 결과, 고칠 것(실패와 주의의 `fix`), 견적과 발급 조건
271
+ (`quote.eligible`), 보고서 파일 위치입니다. JSON 이 아닌 줄은 stderr 로 그대로 냅니다. `--json` 이면 이 명령의 줄
272
+ (`{"step":"cli.docker",…}`, `{"step":"cli.image","status":…,"image":…,"source":"flag|tar|local|download"}`) 뒤에 점검의
273
+ 줄을 바꾸지 않고 내고, 마지막 줄이 점검의 보고서입니다. JSON 이 아닌 줄은 버리고, 로그인 안내는 stderr 로 냅니다.
274
+
275
+ 종료 코드: 점검의 것(0 실패 없음, 3 실패 있음, 2 설정 오류)을 그대로 돌려줍니다. 이 명령의 것은 2 인자 오류(설정 파일
276
+ 없음, `--out` 이 작업 폴더 밖, `--image` 와 `--image-tar` 를 함께 줌), 4 Docker 를 쓸 수 없음(실행 파일 없음, 데몬에 닿지
277
+ 못함, compose v2 없음, Windows 컨테이너 모드), 5 이미지를 구하지 못함, 1 점검이 보고서 없이 끝남(포트 7777 사용 중 등)이나
278
+ 보고서 파일을 쓰지 못함입니다.
279
+
175
280
  ## 키 평문을 내지 않는 원칙
176
281
 
177
282
  - 받은 키 평문은 **그 클라이언트의 설정 파일에만** 씁니다(bind 는 그 폴더의 설정 파일, setup 은 Gemini CLI 와
@@ -197,6 +302,7 @@ CLI 는 폐기된 개수와 키 id 를 알립니다. 그래서 쓸 수 있는
197
302
  | `AIAKIV_MCP_URL` | `https://mcp.aiakiv.com/mcp` | MCP 주소. 끝의 `/mcp` 는 빼고 줘도 됩니다. |
198
303
  | `AIAKIV_CONTROL_URL` | `https://app.aiakiv.com` | 웹 콘솔 주소(인계 화면과 키 교환). |
199
304
  | `AIAKIV_SKILLS_TARBALL_URL` | `https://codeload.github.com/AiAkiv/aiakiv-plugins/tar.gz/refs/heads/main` | 폴더 스킬과 전역 스킬(setup)을 받는 tar.gz 주소(http 나 https). 시험은 로컬 서버로 바꿉니다. |
305
+ | `AIAKIV_ENTERPRISE_MANIFEST_URL` | `https://www.aiakiv.com/download/enterprise/<판>/manifest.json` | `enterprise precheck` 가 이미지를 내려받을 때 읽는 목록 파일 주소(http 나 https). 시험은 로컬 서버로 바꿉니다. |
200
306
  | `CODEX_HOME` | `~/.codex` | Codex 설정 위치(플러그인 캐시, 스킬 확인에 씁니다). |
201
307
 
202
308
  ## 개발
package/dist/bind.js CHANGED
@@ -18,7 +18,7 @@ import { dirname, join, resolve } from "node:path";
18
18
  import { CLAUDE_FOLDER_SETTINGS, FOLDER_CONFIG } from "./clients/types.js";
19
19
  import { addToJsonArray, jsonServers, readJsonObject, upsertJsonServer } from "./config/json.js";
20
20
  import { upsertCodexServer } from "./config/toml.js";
21
- import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
21
+ import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, oldKeysKeptNotice, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
22
22
  import { installFolderSkills } from "./skills.js";
23
23
  import { folderServerName, folderServerNameWithoutId, projectBinding } from "./snippet.js";
24
24
  import { canonicalMcpUrl, controlBase, mcpBase } from "./urls.js";
@@ -244,6 +244,9 @@ async function bindKey(ctx, client, project, dir, opts) {
244
244
  const sup = supersededNotice(res.superseded);
245
245
  if (sup)
246
246
  ctx.out(sup);
247
+ const kept = oldKeysKeptNotice(res.old_keys_kept);
248
+ if (kept)
249
+ ctx.out(kept);
247
250
  ctx.out(`${replaced ? "바꿨습니다" : "더했습니다"}: ${file}`);
248
251
  ctx.out(` 서버 이름 ${name}`);
249
252
  ctx.out(` 주소 ${mcpUrl}`);
package/dist/console.js CHANGED
@@ -14,7 +14,7 @@ import { join, resolve } from "node:path";
14
14
  import { ensureGitIgnored, resolveDir } from "./bind.js";
15
15
  import { callConsole, classify, EXIT, failureLines, findConsoleKey, isObj, keyLabel, NO_KEY_MESSAGE, } from "./consoleApi.js";
16
16
  import { CONSOLE_KEY_DIR, CONSOLE_KEY_FILE, expiryText, writeConsoleKeyFile } from "./consoleKeyFile.js";
17
- import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
17
+ import { exchangeCode, handoffUrl, HANDOFF_TIMEOUT_MS, newState, oldKeysKeptNotice, scopeList, startLoopback, supersededNotice, } from "./handoff.js";
18
18
  import { renderDescribe, renderResult } from "./render.js";
19
19
  import { controlBase } from "./urls.js";
20
20
  /** 별칭은 이 넷뿐이다(§5.3). 나머지는 이름 그대로 보낸다. */
@@ -204,6 +204,9 @@ async function consoleKey(ctx, dir, opts) {
204
204
  const sup = supersededNotice(res.superseded, "account");
205
205
  if (sup)
206
206
  ctx.out(sup);
207
+ const kept = oldKeysKeptNotice(res.old_keys_kept);
208
+ if (kept)
209
+ ctx.out(kept);
207
210
  ctx.out(`썼습니다: ${file}`);
208
211
  let ignore;
209
212
  try {
package/dist/ctx.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // 시험은 가짜(가짜 실행기, 가짜 브라우저, 임시 홈 폴더)를 넣어 같은 코드를 돈다.
3
3
  import { homedir } from "node:os";
4
4
  import { openUrl } from "./browser.js";
5
- import { run, which } from "./proc.js";
5
+ import { run, runLines, which } from "./proc.js";
6
6
  async function askLine(question) {
7
7
  const { createInterface } = await import("node:readline/promises");
8
8
  // 질문은 stderr 로 — `--json` 의 stdout 을 더럽히지 않는다.
@@ -28,6 +28,7 @@ export function realCtx() {
28
28
  err: (line = "") => process.stderr.write(`${line}\n`),
29
29
  which: (name) => which(name, env, platform),
30
30
  run,
31
+ runLines,
31
32
  openUrl: (url) => openUrl(url, platform),
32
33
  fetch: globalThis.fetch.bind(globalThis),
33
34
  };