comitia 0.3.0 → 0.4.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/index.js CHANGED
@@ -98,11 +98,11 @@ async function rpc(method, params, { token: override } = {}) {
98
98
  // `login` 은 **아직 저장하지 않은** 토큰으로 확인해야 한다 — 그래서 넘겨받는 길을 둔다.
99
99
  const token = override || saved;
100
100
  if (!token) {
101
- die('아직 연결 안 됐어요.\n `comitia login` 을 하면 브라우저에서 「허용」 한 번으로 연결됩니다.');
101
+ die('Not connected yet.\n Run `comitia login` and approve once in the browser.');
102
102
  }
103
103
 
104
104
  if (token && !TOKEN_RE.test(token)) {
105
- die('토큰에 이상한 글자가 섞여 있어요. 따옴표나 줄바꿈이 딸려 오지 않았는지 확인해 주세요.');
105
+ die('That token has unexpected characters in it. Check for stray quotes or line breaks.');
106
106
  }
107
107
 
108
108
  let res;
@@ -111,27 +111,27 @@ async function rpc(method, params, { token: override } = {}) {
111
111
  method: 'POST',
112
112
  headers: {
113
113
  'Content-Type': 'application/json',
114
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
114
+ ...(token ? { Authorization: `Bearer ${token}`, 'X-Comitia-Token': token } : {}),
115
115
  },
116
116
  body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
117
117
  });
118
118
  } catch (e) {
119
- die(`서버에 닿지 못했어요 (${api}).\n ${e.message}\n 회사망·VPN·보안 프로그램이 막는 경우가 많아요 — 다른 망(휴대폰 핫스팟)에서 한 번 해 보세요.`);
119
+ die(`Couldn't reach the server (${api}).\n ${e.message}\n A corporate network, VPN or security software is often the cause — try another network (your phone's hotspot).`);
120
120
  }
121
121
 
122
- if (res.status === 401) die('연결이 끊겼어요(토큰이 끊겼거나 틀림). `comitia login` 으로 다시 연결해 주세요.');
122
+ if (res.status === 401) die('The connection was lost (the token was revoked or is wrong). Reconnect with `comitia login`.');
123
123
  if (res.status === 403) {
124
124
  // 읽기 전용 토큰으로 쓰기를 시도한 경우. 서버 문구를 그대로 전한다.
125
125
  let detail = '';
126
126
  try {
127
127
  detail = (await res.json())?.detail || '';
128
128
  } catch { /* 본문이 JSON 이 아닐 수 있다 */ }
129
- die(detail || '이 토큰으로는 할 수 없는 일이에요. 쓰기 토큰이 필요합니다.');
129
+ die(detail || "This token can't do that. You need a write token.");
130
130
  }
131
- if (!res.ok) die(`서버가 ${res.status} 로 답했어요.`);
131
+ if (!res.ok) die(`The server replied ${res.status}.`);
132
132
 
133
133
  const body = await res.json();
134
- if (body.error) die(`요청이 거절됐어요: ${body.error.message || body.error.code}`);
134
+ if (body.error) die(`Request rejected: ${body.error.message || body.error.code}`);
135
135
  return body.result || {};
136
136
  }
137
137
 
@@ -144,7 +144,7 @@ async function callTool(name, args, opts) {
144
144
  .join('\n');
145
145
  // 🔑 **도구가 「안 됐다」고 답한 것도 종료 코드로 옮긴다.** 셸에서 `&&` 로 이어
146
146
  // 쓰는 사람이 성공/실패를 알아야 한다 — 글만 보고는 스크립트가 못 가린다.
147
- if (result.isError) die(text || '요청을 처리하지 못했어요.');
147
+ if (result.isError) die(text || 'Could not complete the request.');
148
148
  return text;
149
149
  }
150
150
 
@@ -184,7 +184,7 @@ function openBrowser(url) {
184
184
  const DONE_HTML = (msg) => `<!doctype html><meta charset="utf-8"><title>Comitia</title>
185
185
  <body style="font-family:-apple-system,system-ui,sans-serif;background:#f6f6f2;display:grid;place-items:center;height:100vh;margin:0">
186
186
  <div style="text-align:center;color:#34322e"><div style="font-size:18px;font-weight:600">${msg}</div>
187
- <div style="margin-top:8px;font-size:13px;opacity:.55">이 창은 닫아도 돼요.</div></div></body>`;
187
+ <div style="margin-top:8px;font-size:13px;opacity:.55">You can close this window.</div></div></body>`;
188
188
 
189
189
  /** 브라우저에서 허용받아 토큰을 저장한다. 워크스페이스를 매었으면 그것도 정해 둔다. */
190
190
  async function browserLogin({ paste = false } = {}) {
@@ -199,9 +199,9 @@ async function browserLogin({ paste = false } = {}) {
199
199
  if (paste) {
200
200
  // 브라우저가 다른 기기에 있다(원격 서버·SSH). 여기로 돌아올 길이 없으니 코드를 옮겨 적는다.
201
201
  q.set('mode', 'paste');
202
- console.log(`이 주소를 브라우저에서 열고 「허용」을 누르세요:\n\n ${web}/cli-login?${q}\n`);
202
+ console.log(`Open this in your browser and click Approve:\n\n ${web}/cli-login?${q}\n`);
203
203
  const rl = createInterface({ input: process.stdin, output: process.stdout });
204
- code = (await rl.question('화면에 뜬 연결 코드를 붙여 넣으세요: ')).trim();
204
+ code = (await rl.question('Paste the connection code shown on screen: ')).trim();
205
205
  rl.close();
206
206
  } else {
207
207
  code = await new Promise((resolve, reject) => {
@@ -215,7 +215,7 @@ async function browserLogin({ paste = false } = {}) {
215
215
  // ⚠️ `state` 가 다르면 이 창에서 시작한 요청이 아니다 — 받지 않는다.
216
216
  const got = u.searchParams.get('state') === state ? u.searchParams.get('code') : null;
217
217
  res.writeHead(got ? 200 : 400, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
218
- res.end(DONE_HTML(got ? '연결됐어요. 터미널로 돌아가세요.' : '요청이 맞지 않아요. 터미널에서 comitia login 을 다시 해 주세요.'));
218
+ res.end(DONE_HTML(got ? 'Connected. Head back to your terminal.' : 'That request did not match. Run comitia login again in your terminal.'));
219
219
  if (!got) return;
220
220
  clearTimeout(timer);
221
221
  server.close();
@@ -223,21 +223,21 @@ async function browserLogin({ paste = false } = {}) {
223
223
  });
224
224
  const timer = setTimeout(() => {
225
225
  server.close();
226
- reject(new Error('5분 안에 허용하지 않아 멈췄어요. `comitia login` 을 다시 해 주세요.'));
226
+ reject(new Error('Stopped — not approved within 5 minutes. Run `comitia login` again.'));
227
227
  }, 5 * 60 * 1000);
228
228
  server.on('error', reject);
229
229
  server.listen(0, '127.0.0.1', () => {
230
230
  q.set('port', String(server.address().port));
231
231
  const url = `${web}/cli-login?${q}`;
232
232
  console.log(openBrowser(url)
233
- ? '브라우저에서 Comitia 를 열었어요. 「허용」을 누르면 여기로 돌아옵니다.'
234
- : '브라우저를 열지 못했어요.');
235
- console.log(`\n안 열리면 이 주소를 직접 여세요:\n ${url}\n`);
236
- console.log('(브라우저가 다른 기기에 있으면 Ctrl+C 로 멈추고 `comitia login --paste`)\n');
233
+ ? "Opened Comitia in your browser. Click Approve and you'll come back here."
234
+ : 'Could not open a browser.');
235
+ console.log(`\nIf it didn't open, go to this address yourself:\n ${url}\n`);
236
+ console.log('(If your browser is on another device, press Ctrl+C and run `comitia login --paste`)\n');
237
237
  });
238
238
  });
239
239
  }
240
- if (!code) die('연결 코드가 비었어요.');
240
+ if (!code) die('The connection code is empty.');
241
241
 
242
242
  let res;
243
243
  try {
@@ -247,16 +247,16 @@ async function browserLogin({ paste = false } = {}) {
247
247
  body: JSON.stringify({ code, code_verifier: verifier }),
248
248
  });
249
249
  } catch (e) {
250
- die(`서버에 닿지 못했어요 (${api}).\n ${e.message}\n 회사망·VPN·보안 프로그램이 막는 경우가 많아요 — 다른 망(휴대폰 핫스팟)에서 한 번 해 보세요.`);
250
+ die(`Couldn't reach the server (${api}).\n ${e.message}\n A corporate network, VPN or security software is often the cause — try another network (your phone's hotspot).`);
251
251
  }
252
252
  const body = await res.json().catch(() => ({}));
253
- if (!res.ok || !body.token) die(body.detail || `서버가 ${res.status} 로 답했어요.`);
253
+ if (!res.ok || !body.token) die(body.detail || `The server replied ${res.status}.`);
254
254
 
255
255
  // 워크스페이스에 매인 토큰이면 그걸 기본으로 — `comitia use` 를 따로 안 해도 된다.
256
256
  saveConf({ ...loadConf(), token: body.token, ...(body.company_id ? { companyId: body.company_id } : {}) });
257
257
  console.log(
258
- `연결했어요 — ${body.company_name || '내가 볼 수 있는 워크스페이스 전부'} · ` +
259
- `${body.scope === 'write' ? '자료·정리까지' : '읽기만'}`,
258
+ `Connected — ${body.company_name || 'every workspace you can see'} · ` +
259
+ `${body.scope === 'write' ? 'sources and organizing' : 'read only'}`,
260
260
  );
261
261
  return body;
262
262
  }
@@ -265,7 +265,7 @@ async function browserLogin({ paste = false } = {}) {
265
265
  function needCompany(flags) {
266
266
  const id = flags.c || settings().companyId;
267
267
  if (!id) {
268
- die('워크스페이스를 안 정했어요.\n `comitia companies` 로 목록을 보고 `comitia use <id>` 하세요.');
268
+ die('No workspace selected.\n Run `comitia companies` to list them, then `comitia use <id>`.');
269
269
  }
270
270
  return id;
271
271
  }
@@ -278,7 +278,7 @@ function needCompany(flags) {
278
278
 
279
279
  // 🌍 **도구마다 「모든 폴더에 적용되는」 규칙 파일 자리가 따로 있다.**
280
280
  // 여기 한 번 심으면 폴더마다 `init` 을 돌릴 필요가 없다.
281
- // 🧩 **Claude Code 에는 스킬로도 심는다** — `/comitia:read` · `/comitia:ingest` · `/comitia:slack` · `/comitia:sync`.
281
+ // 🧩 **Claude Code 에는 스킬로도 심는다** — `/comitia:read` · `/comitia:ingest` · `/comitia:problem` · `/comitia:slack` · `/comitia:sync`.
282
282
  // 규칙 파일 블록은 **모든 대화에 항상** 실리지만, 스킬은 한 줄 설명(~90토큰)만 늘 들고
283
283
  // 본문은 **부를 때만** 읽힌다. 그래서 절차를 길고 정확하게 적을 수 있다.
284
284
  // `~/.claude/skills/<이름>/` 에 두면 마켓플레이스 없이 다음 세션부터 `comitia@skills-dir` 로 뜬다.
@@ -312,19 +312,19 @@ function installPlugin() {
312
312
  }
313
313
  if (existsSync(dest)) {
314
314
  // 같은 이름의 **남의 것**은 덮지 않는다.
315
- if (pluginName(dest) !== 'comitia') return { dest, removed, state: '건너뜀 — 같은 이름의 다른 폴더가 있어요' };
315
+ if (pluginName(dest) !== 'comitia') return { dest, removed, state: 'skipped - a different folder with the same name exists' };
316
316
  rmSync(dest, { recursive: true, force: true }); // 옛 판의 지운 스킬이 남지 않게
317
317
  }
318
318
  mkdirSync(join(home, 'skills'), { recursive: true });
319
319
  cpSync(PLUGIN_SRC, dest, { recursive: true });
320
- return { dest, removed, state: '설치' };
320
+ return { dest, removed, state: 'installed' };
321
321
  }
322
322
 
323
323
  function reportPlugin(r = installPlugin()) {
324
324
  if (!r) return;
325
- for (const old of r.removed || []) console.log(`~/.claude/skills/${old} — 옛 이름이라 지움 (/${old}:* → /comitia:*)`);
325
+ for (const old of r.removed || []) console.log(`~/.claude/skills/${old} - removed (old name: /${old}:* → /comitia:*)`);
326
326
  console.log(`~/.claude/skills/comitia — ${r.state}`);
327
- if (r.state === '설치') console.log(' Claude Code 스킬 /comitia:read · /comitia:ingest · /comitia:slack · /comitia:sync (다음 세션부터, 지금은 /reload-plugins)');
327
+ if (r.state === 'installed') console.log(' Claude Code skills /comitia:read · /comitia:ingest · /comitia:problem · /comitia:slack · /comitia:sync (from the next session, or run /reload-plugins now)');
328
328
  }
329
329
 
330
330
  const GLOBAL_RULES = [
@@ -343,28 +343,38 @@ const OLD_MARKS = [
343
343
  ];
344
344
 
345
345
  const BLOCK = `${MARK_START}
346
- ## 회사 맥락 (comitia)
347
346
 
348
- 슬랙·회의록·위키를 \`comitia\` 명령으로 꺼낸다. **사람에게 복사해 달라고 하지 않는다.**
347
+ ## Company context (comitia)
349
348
 
350
- | 정리 전 원본 | | 정리된 위키 | |
349
+ Pull Slack, meetings and the wiki with the \`comitia\` command. **Never ask the person to paste it in.**
350
+
351
+ | Raw, not yet organized | | The organized wiki | |
351
352
  |---|---|---|---|
352
- | \`comitia recent\` | 최근 들어온 것 | \`comitia search <낱말>\` | 본문 검색 |
353
- | \`comitia find <낱말>\` | 원본에서 찾기 | \`comitia read <경로>\` | 문서 본문 |
354
- | \`comitia raw <id>\` | 원본 본문 | \`comitia list [접두어]\` | 문서 목록 |
355
- | \`comitia forget <id> <낱말>\` | 원본에서 낱말 지우기(남을 이유 없는 제3자 이름 등) | | |
353
+ | \`comitia recent\` | what came in lately | \`comitia search <word>\` | full-text search |
354
+ | \`comitia find <word>\` | search the raw items | \`comitia read <path>\` | a page's body |
355
+ | \`comitia raw <id>\` | a raw item's body | \`comitia list [prefix]\` | list pages |
356
+ | \`comitia forget <id> <word>\` | scrub a word from a raw item (a third-party name with no reason to be there, etc.) | | |
357
+
358
+ **When asked to organize, do it yourself** — don't hand it to Comitia's own AI.
359
+
360
+ 🔑 **Before writing anything to the wiki, read \`comitia rules\` first and follow it.**
361
+ The axes, the page format, the places you must not touch and the sequence are all there.
362
+ **It differs per company and the person may have edited it, so never rely on your memory of this block.**
356
363
 
357
- **정리를 부탁받으면 내가 한다** — Comitia 의 AI 에게 맡기지 않는다.
364
+ ⚠️ **Posting to Slack (\`comitia slack\`) is something the whole team sees** — show the channel and the text and get approval before posting.
365
+ **Editing and deleting (\`--edit\` · \`--delete\`) too** — only the bot's own messages, and always show what you're changing or deleting and get approval. Deleting cannot be undone.
358
366
 
359
- 🔑 **위키에 무엇을 쓰기 전에 \`comitia rules\` 를 먼저 읽고 그대로 따른다.**
360
- 축 구성·문서 형식·건드리지 않을 곳·정리 순서가 거기 있다. **회사마다 다르고
361
- 사용자가 고쳐 두었을 수 있으므로, 이 블록의 기억에 의존하지 않는다.**
367
+ In Claude Code, the skills carry this sequence.
362
368
 
363
- ⚠️ **슬랙에 올리는 것(\`comitia slack\`)은 팀 전체가 보는 일이다** — 채널과 문구를 먼저 보여 주고 허락받은 뒤에만 올린다.
364
- **고치기·지우기(\`--edit\` · \`--delete\`)도 마찬가지다** — 봇이 올린 글만 되고, 무엇을 고치거나 지우는지 보여 주고 허락받는다. 지우기는 되돌릴 수 없다.
369
+ | | |
370
+ |---|---|
371
+ | \`/comitia:read\` | find company context and answer from it |
372
+ | \`/comitia:ingest\` | push originals in and organize them into the wiki |
373
+ | \`/comitia:problem\` | surface the things you only learn by changing something, onto the lab board |
374
+ | \`/comitia:slack\` | post to Slack (with approval) |
375
+ | \`/comitia:sync\` | pull connected sources in right now |
365
376
 
366
- Claude Code 에서는 스킬 \`/comitia:read\` · \`/comitia:ingest\` · \`/comitia:slack\` · \`/comitia:sync\` 이 순서를 담고 있다.
367
- 자세한 명령은 \`comitia --help\`.
377
+ For the full command list, run \`comitia --help\`.
368
378
  ${MARK_END}`;
369
379
 
370
380
  /** 규칙 파일에 블록을 심는다. **두 번 해도 하나만 남는다.** */
@@ -398,13 +408,13 @@ function writeRules(file) {
398
408
  at = j;
399
409
  });
400
410
  after += before.slice(at);
401
- if (after === before) return { file, state: '그대로' };
411
+ if (after === before) return { file, state: 'unchanged' };
402
412
  writeFileSync(file, after);
403
- return { file, state: '갱신' };
413
+ return { file, state: 'updated' };
404
414
  }
405
415
  after = before.trimEnd() + (before.trim() ? '\n\n' : '') + BLOCK + '\n';
406
416
  writeFileSync(file, after);
407
- return { file, state: had ? '추가' : '새로 만듦' };
417
+ return { file, state: had ? 'appended' : 'created' };
408
418
  }
409
419
 
410
420
  // ── 인자 파싱 ────────────────────────────────────────────
@@ -430,52 +440,52 @@ function parseArgs(argv) {
430
440
  return { flags, rest };
431
441
  }
432
442
 
433
- const HELP = `comitia — 회사 온톨로지를 내가 쓰는 AI에서 꺼내 쓴다
434
-
435
- comitia init 한 번에 붙이기 — 브라우저 로그인 + 워크스페이스 + AI 규칙파일
436
- comitia init -g 규칙을 **모든 폴더**에 (~/.claude 등). 폴더마다 안 해도 됨
437
- comitia login 브라우저에서 「허용」 한 번으로 연결 (토큰을 볼 일이 없다)
438
- comitia login --paste 브라우저가 다른 기기에 있을 때 — 화면의 코드를 붙여 넣는다
439
- comitia login <토큰> 발급받은 토큰을 직접 넣기
440
- comitia logout 저장한 토큰 지우기
441
- comitia whoami 지금 어떻게 붙어 있나
442
-
443
- comitia companies 워크스페이스 목록
444
- comitia use <id 또는 이름> 기본 워크스페이스 정하기
445
-
446
- ── 정리된 문서 ──
447
- comitia list [접두어] 문서 목록 (예: comitia list ledger/)
448
- comitia search <낱말> 본문 검색
449
- comitia read <경로...> 문서 본문
450
- comitia graph 문서가 서로 어떻게 이어져 있는지
451
-
452
- ── 정리 전 원본 ── 슬랙·메일이 들어온 그대로
453
- comitia recent [개수] 최근 들어온 원본
454
- comitia find <낱말> 원본에서 찾기
455
- comitia raw <id...> 원본 본문 (id 는 recent·find 가 준 것)
456
- comitia forget <id> <낱말...> 원본에서 낱말 지우기 — 다시 수집돼도 안 돌아옴 (쓰기 토큰)
457
- comitia sync [slack|tiro|...] 지금 당겨오기 — 매시 자동 수집을 기다리지 않고 (쓰기 토큰)
458
- comitia slack <채널> <파일|-> 연결된 봇으로 슬랙에 올리기 (쓰기 토큰 · 봇에 chat:write 필요)
459
- comitia slack <채널> --delete <ts> 봇이 올린 그 글 지우기 (되돌릴 수 없음)
460
- --thread <ts> 그 글의 스레드에 답글로 (부모 ts)
461
- --thread-of "<검색어>" 부모 글을 본문으로 찾아 그 스레드에 (최근 100건 중 최신)
462
- --edit <ts> 새 글 대신 봇이 올린 그 글을 통째로 바꾸기 (봇 글만)
463
- --edit-of "<검색어>" 고칠 봇 글을 본문으로 찾아서 (최근 100건 중 최신)
464
-
465
- ── 정리를 내 AI 가 한다 ── (쓰기 토큰 필요)
466
- comitia rules 이 워크스페이스의 위키 규칙 — **쓰기 전에 먼저**
467
- comitia skills Claude Code 스킬 설치·갱신 (/comitia:read · /comitia:ingest · /comitia:slack · /comitia:sync)
468
- comitia push <제목> <파일|-> 원본 넣기
469
- comitia build 원문·장부·표 만들기 (코드만, 0원)
470
- comitia pending 요약이 필요한 원본
471
- comitia write <경로> <파일|-> 요약을 위키에 남기기 (예: topics/물류.md)
472
- comitia organize Comitia 의 AI 에게 정리를 맡긴다 (직접 안 쓸 때)
473
-
474
- -c, --company <id> 이번만 다른 워크스페이스로
475
- -g, --global init 을 모든 폴더에 적용
476
- --paste login·init 을 코드 붙여넣기로
477
-
478
- 환경 변수: COMITIA_API · COMITIA_TOKEN · COMITIA_COMPANY (설정 파일보다 우선. 옛 이름 Z2O_* 도 읽는다)`;
443
+ const HELP = `comitia — pull your company's ontology into the AI you already use
444
+
445
+ comitia init set it all up — browser login + workspace + AI rules file
446
+ comitia init -g put the rules in **every folder** (~/.claude etc). No need to repeat per folder
447
+ comitia login connect by approving once in the browser (you never see a token)
448
+ comitia login --paste when your browser is on another device — paste the code from the screen
449
+ comitia login <token> enter a token you were issued
450
+ comitia logout delete the stored token
451
+ comitia whoami how you're connected right now
452
+
453
+ comitia companies list workspaces
454
+ comitia use <id or name> set the default workspace
455
+
456
+ ── Organized pages ──
457
+ comitia list [prefix] list pages (e.g. comitia list ledger/)
458
+ comitia search <word> full-text search
459
+ comitia read <path...> a page's body
460
+ comitia graph how pages link to each other
461
+
462
+ ── Raw, not yet organized ── Slack and email exactly as they arrived
463
+ comitia recent [n] raw items that came in lately
464
+ comitia find <word> search the raw items
465
+ comitia raw <id...> a raw item's body (ids come from recent / find)
466
+ comitia forget <id> <word...> scrub a word from a raw item — it won't come back on re-collection (write token)
467
+ comitia sync [slack|tiro|...] pull right now, without waiting for the hourly collection (write token)
468
+ comitia slack <channel> <file|-> post to Slack via the connected bot (write token · bot needs chat:write)
469
+ comitia slack <channel> --delete <ts> delete that bot message (cannot be undone)
470
+ --thread <ts> reply in that message's thread (parent ts)
471
+ --thread-of "<query>" find the parent by its text and reply there (latest of the last 100)
472
+ --edit <ts> replace that bot message wholesale instead of posting a new one (bot messages only)
473
+ --edit-of "<query>" find the bot message to edit by its text (latest of the last 100)
474
+
475
+ ── Your own AI does the organizing ── (write token required)
476
+ comitia rules this workspace's wiki rules — **read before writing**
477
+ comitia skills install / update the Claude Code skills (/comitia:read · /comitia:ingest · /comitia:problem · /comitia:slack · /comitia:sync)
478
+ comitia push <title> <file|-> push an original in
479
+ comitia build build the originals, ledger and tables (code only, costs nothing)
480
+ comitia pending originals that still need a summary
481
+ comitia write <path> <file|-> leave a summary in the wiki (e.g. topics/logistics.md)
482
+ comitia organize hand the organizing to Comitia's AI (when you're not doing it yourself)
483
+
484
+ -c, --company <id> use a different workspace just this once
485
+ -g, --global apply init to every folder
486
+ --paste do login / init by pasting a code
487
+
488
+ Environment variables: COMITIA_API · COMITIA_TOKEN · COMITIA_COMPANY (they win over the config file. The old names Z2O_* are also read)`;
479
489
 
480
490
  // ── 명령 ────────────────────────────────────────────────
481
491
 
@@ -486,21 +496,21 @@ const COMMANDS = {
486
496
  await browserLogin({ paste: flags.paste });
487
497
  if (!settings().companyId) {
488
498
  console.log('\n' + (await callTool('list_companies', {})));
489
- console.log('\n`comitia use <id 또는 이름>` 으로 기본 워크스페이스를 정해 두세요.');
499
+ console.log('\nSet a default workspace with `comitia use <id or name>`.');
490
500
  }
491
501
  return;
492
502
  }
493
503
  if (!TOKEN_RE.test(token)) {
494
- die('토큰 모양이 아니에요. `z2o_live_` 로 시작하는 한 줄이어야 합니다.');
504
+ die('That is not a token. It should be a single line starting with `z2o_live_`.');
495
505
  }
496
506
  // 🔑 **저장하기 전에 한 번 써 본다.** 틀린 토큰을 저장해 두면 나중에 엉뚱한
497
507
  // 명령에서 실패하고, 사람은 그게 토큰 탓인 줄 모른다. 확인에 실패하면
498
508
  // `callTool` 이 여기서 끝내므로 **잘못된 토큰이 파일에 남지 않는다.**
499
509
  const text = await callTool('list_companies', {}, { token });
500
510
  saveConf({ ...loadConf(), token });
501
- console.log('연결했어요.\n');
511
+ console.log('Connected.\n');
502
512
  console.log(text);
503
- if (!settings().companyId) console.log('\n`comitia use <id>` 로 기본 워크스페이스를 정해 두세요.');
513
+ if (!settings().companyId) console.log('\nSet a default workspace with `comitia use <id>`.');
504
514
  },
505
515
 
506
516
  /** 한 번에 다 붙인다 — 설치 뒤 이것만 하면 된다.
@@ -519,7 +529,7 @@ const COMMANDS = {
519
529
  await browserLogin({ paste: flags.paste });
520
530
  console.log('');
521
531
  } else {
522
- console.log('이미 연결돼 있어요.\n');
532
+ console.log('Already connected.\n');
523
533
  }
524
534
 
525
535
  // ② 워크스페이스 — 하나뿐이면 묻지 않고 정한다.
@@ -527,20 +537,20 @@ const COMMANDS = {
527
537
  if (!settings().companyId) {
528
538
  const rows = (await callTool('list_companies', {}))
529
539
  .split('\n')
530
- .map((l) => l.match(/^- (\S+) — (.+?)(?: \(초대받음.*\))?$/))
540
+ .map((l) => l.match(/^- (\S+) — (.+?)(?: \((?:초대받음|invited).*\))?$/))
531
541
  .filter(Boolean);
532
542
  if (rows.length === 1) {
533
543
  saveConf({ ...loadConf(), companyId: rows[0][1] });
534
- console.log(`워크스페이스: ${rows[0][2]}`);
544
+ console.log(`Workspace: ${rows[0][2]}`);
535
545
  } else if (rows.length > 1) {
536
546
  // ⚠️ **여럿이면 고르지 않는다.** 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
537
547
  needPick = true;
538
- console.log('워크스페이스가 여럿이에요. 하나 고르세요:\n');
548
+ console.log('You have several workspaces. Pick one:\n');
539
549
  for (const r of rows) console.log(` ${r[1]} — ${r[2]}`);
540
550
  console.log('');
541
551
  } else {
542
552
  needPick = true;
543
- console.log('쓸 수 있는 워크스페이스가 없어요. z2o.kr 에서 만들거나 초대받으세요.\n');
553
+ console.log('No workspace available. Create one at z2o.kr or get invited to one.\n');
544
554
  }
545
555
  }
546
556
 
@@ -555,13 +565,13 @@ const COMMANDS = {
555
565
  const r = writeRules(join(dir, f));
556
566
  console.log(`~/${d}/${f} — ${r.state}`);
557
567
  }
558
- console.log(' (모든 폴더에서 통합니다)');
568
+ console.log(' (works in every folder)');
559
569
  reportPlugin();
560
- console.log('\n이제 AI 에게 그냥 물어보세요:');
561
- console.log(' "최근 슬랙에서 나온 얘기 정리해줘"');
570
+ console.log('\nNow just ask your AI:');
571
+ console.log(' "catch me up on recent Slack"');
562
572
  if (needPick) {
563
- console.log('\n🔴 아직 한 걸음 남았어요:');
564
- console.log(' comitia use <id 또는 이름>');
573
+ console.log('\n🔴 One step left:');
574
+ console.log(' comitia use <id or name>');
565
575
  }
566
576
  return;
567
577
  }
@@ -578,19 +588,19 @@ const COMMANDS = {
578
588
  console.log(`${r.file} — ${r.state}`);
579
589
  }
580
590
  console.log(
581
- ' (다른 도구를 쓰면 그 도구의 규칙 파일을 만들어 두고 `comitia init` 을 다시 돌리세요)',
591
+ ' (Using another tool? Create its rules file first, then run `comitia init` again)',
582
592
  );
583
593
  reportPlugin();
584
594
 
585
595
  // ⚠️ **다 된 것처럼 끝내지 않는다.** 워크스페이스를 못 정했으면 아직 아무것도 안 된다 —
586
596
  // 여기서 "이제 물어보세요" 라고 하면 사람은 됐다고 믿고 넘어간다.
587
597
  if (needPick) {
588
- console.log('\n🔴 아직 한 걸음 남았어요:');
589
- console.log(' comitia use <id 또는 이름> (예: comitia use 린디)');
598
+ console.log('\n🔴 One step left:');
599
+ console.log(' comitia use <id or name> (e.g. comitia use lindy)');
590
600
  } else {
591
- console.log('\n이제 AI 에게 그냥 물어보세요:');
592
- console.log(' "최근 슬랙에서 나온 얘기 정리해줘"');
593
- console.log(' "지난주 회의 내용 찾아서 할 일 뽑아줘"');
601
+ console.log('\nNow just ask your AI:');
602
+ console.log(' "catch me up on recent Slack"');
603
+ console.log(' "find last week\'s meeting and pull out the action items"');
594
604
  }
595
605
  void flags;
596
606
  },
@@ -598,7 +608,7 @@ const COMMANDS = {
598
608
  // 스킬만 깔거나 새 판으로 바꾼다 — 토큰이 없어도 된다. `npm i -g comitia` 로 올린 뒤에 한 번.
599
609
  async skills() {
600
610
  const r = installPlugin();
601
- if (!r) die('Claude Code 가 안 보여요(~/.claude 없음). Claude Code 를 한 번 실행한 뒤 다시 해 주세요.');
611
+ if (!r) die("Can't find Claude Code (no ~/.claude). Run Claude Code once, then try again.");
602
612
  reportPlugin(r);
603
613
  },
604
614
 
@@ -606,14 +616,14 @@ const COMMANDS = {
606
616
  const conf = loadConf();
607
617
  delete conf.token;
608
618
  saveConf(conf);
609
- console.log('저장한 토큰을 지웠어요. (서버 쪽에서 끊으려면 프로필 → 「연결 토큰」에서 끊으세요.)');
619
+ console.log('Removed the stored token. (To revoke it server-side, go to your profile → Connection tokens.)');
610
620
  },
611
621
 
612
622
  async whoami() {
613
623
  const { api, token, companyId } = settings();
614
- console.log(`서버 ${api}`);
615
- console.log(`토큰 ${token ? token.slice(0, 17) + '…' : '(없음)'}`);
616
- console.log(`워크스페이스 ${companyId || '(안 정함)'}`);
624
+ console.log(`Server ${api}`);
625
+ console.log(`Token ${token ? token.slice(0, 17) + '…' : '(none)'}`);
626
+ console.log(`Workspace ${companyId || '(not set)'}`);
617
627
  if (!token) return;
618
628
  console.log('');
619
629
  console.log(await callTool('list_companies', {}));
@@ -624,12 +634,12 @@ const COMMANDS = {
624
634
  },
625
635
 
626
636
  async use([q]) {
627
- if (!q) die('워크스페이스 id 나 이름 일부를 적어 주세요.');
637
+ if (!q) die('Give a workspace id or part of its name.');
628
638
  const text = await callTool('list_companies', {});
629
639
  // 목록 줄은 `- <id> — <이름>` 모양이다.
630
640
  const rows = text
631
641
  .split('\n')
632
- .map((l) => l.match(/^- (\S+) — (.+?)(?: \(초대받음.*\))?$/))
642
+ .map((l) => l.match(/^- (\S+) — (.+?)(?: \((?:초대받음|invited).*\))?$/))
633
643
  .filter(Boolean)
634
644
  .map((m) => ({ id: m[1], name: m[2] }));
635
645
  const hit = rows.filter((r) => r.id === q || r.name.includes(q));
@@ -637,16 +647,16 @@ const COMMANDS = {
637
647
  // 🔑 **매인 토큰이면 목록에 하나만 나온다.** 그걸 모르면 "왜 내 회사가 없지" 가 된다.
638
648
  const why =
639
649
  rows.length === 1
640
- ? '\n\n이 열쇠는 위 워크스페이스에만 매여 있어요. 다른 곳도 쓰려면 「전부」로 새로 발급하세요.'
650
+ ? '\n\nThis key is tied to the workspace above. To use others, issue a new key scoped to all of them.'
641
651
  : '';
642
- die(`「${q}」 에 해당하는 워크스페이스가 없어요.\n\n${text}${why}`);
652
+ die(`No workspace matches "${q}".\n\n${text}${why}`);
643
653
  }
644
654
  // ⚠️ 여럿이면 고르지 않는다 — 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
645
655
  if (hit.length > 1) {
646
- die(`「${q}」 가 여럿에 걸려요. id 로 정확히 골라 주세요.\n` + hit.map((r) => `- ${r.id} — ${r.name}`).join('\n'));
656
+ die(`"${q}" matches several. Pick one by id.\n` + hit.map((r) => `- ${r.id} — ${r.name}`).join('\n'));
647
657
  }
648
658
  saveConf({ ...loadConf(), companyId: hit[0].id });
649
- console.log(`기본 워크스페이스를 「${hit[0].name}」 로 정했어요.`);
659
+ console.log(`Default workspace set to "${hit[0].name}".`);
650
660
  },
651
661
 
652
662
  async list(args, flags) {
@@ -655,12 +665,12 @@ const COMMANDS = {
655
665
 
656
666
  async search(args, flags) {
657
667
  const q = args.join(' ').trim();
658
- if (!q) die('찾을 낱말을 적어 주세요.');
668
+ if (!q) die('Give a word to search for.');
659
669
  console.log(await callTool('search_pages', { company_id: needCompany(flags), query: q }));
660
670
  },
661
671
 
662
672
  async read(args, flags) {
663
- if (args.length === 0) die('읽을 문서 경로를 적어 주세요. (`comitia list` 로 확인)');
673
+ if (args.length === 0) die('Give the page path to read. (`comitia list` shows them)');
664
674
  console.log(await callTool('read_pages', { company_id: needCompany(flags), paths: args }));
665
675
  },
666
676
 
@@ -686,28 +696,28 @@ const COMMANDS = {
686
696
 
687
697
  async find(args, flags) {
688
698
  const q = args.join(' ').trim();
689
- if (!q) die('찾을 낱말을 적어 주세요.');
699
+ if (!q) die('Give a word to search for.');
690
700
  console.log(await callTool('search_raw', { company_id: needCompany(flags), query: q }));
691
701
  },
692
702
 
693
703
  async raw(args, flags) {
694
- if (args.length === 0) die('읽을 원본 id 를 적어 주세요. (`comitia recent` 로 확인)');
704
+ if (args.length === 0) die('Give the raw id to read. (`comitia recent` shows them)');
695
705
  console.log(await callTool('read_raw', { company_id: needCompany(flags), ids: args }));
696
706
  },
697
707
 
698
708
  /** 원본에서 낱말을 지운다. 통째로 지우면 다음 수집에 되살아나지만 이건 안 돌아온다. */
699
709
  async forget(args, flags) {
700
710
  const [id, ...terms] = args;
701
- if (!id || terms.length === 0) die('원본 id 와 지울 낱말을 적어 주세요.\n z2o forget af1c6e9f "홍길동"');
711
+ if (!id || terms.length === 0) die('Give a raw id and the word to remove.\n comitia forget af1c6e9f "Jane Doe"');
702
712
  console.log(await callTool('forget_raw', { company_id: needCompany(flags), id, terms }));
703
713
  },
704
714
 
705
715
  async push(args, flags) {
706
716
  const [title, path] = args;
707
- if (!title || !path) die('제목과 파일을 적어 주세요.\n comitia push "회의록" note.md\n cat x.md | comitia push "제목" -');
717
+ if (!title || !path) die('Give a title and a file.\n comitia push "Meeting notes" note.md\n cat x.md | comitia push "Title" -');
708
718
  // `-` 면 표준입력. 파이프로 이어 쓰는 게 이 명령의 주된 쓰임새다.
709
719
  const content = path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
710
- if (!content.trim()) die('본문이 비어 있어요.');
720
+ if (!content.trim()) die('The body is empty.');
711
721
  console.log(await callTool('ingest_text', { company_id: needCompany(flags), title, content }));
712
722
  },
713
723
 
@@ -730,10 +740,10 @@ const COMMANDS = {
730
740
  async write(args, flags) {
731
741
  const [path, file] = args;
732
742
  if (!path || !file) {
733
- die('경로와 파일을 적어 주세요.\n comitia write topics/물류.md 요약.md\n ... | comitia write topics/물류.md -');
743
+ die('Give a path and a file.\n comitia write topics/logistics.md summary.md\n ... | comitia write topics/logistics.md -');
734
744
  }
735
745
  const content = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8');
736
- if (!content.trim()) die('본문이 비어 있어요.');
746
+ if (!content.trim()) die('The body is empty.');
737
747
  console.log(
738
748
  await callTool('write_pages', {
739
749
  company_id: needCompany(flags),
@@ -768,22 +778,22 @@ const COMMANDS = {
768
778
  async slack([channel, file], flags) {
769
779
  // 🗑️ 지우기는 올릴 글이 없다 — 채널과 ts(또는 검색어)만 받는다. 되돌릴 수 없어 스킬이 먼저 허락받는다.
770
780
  if (flags.delete || flags.deleteOf) {
771
- if (!channel) die('지울 글이 있는 채널을 적어 주세요.\n comitia slack project_0to1 --delete 1757769600.123456\n comitia slack project_0to1 --delete-of "z2o → Comitia 이름 변경"');
772
- if (flags.delete && !/^\d{10}\.\d{6}$/.test(flags.delete)) die(`--delete 는 슬랙 ts 모양이어야 해요 (예: 1757769600.123456). 검색어로 찾으려면 --delete-of "<글의 일부>"`);
773
- if (flags.edit || flags.editOf || flags.thread || flags.threadOf) die('--delete 는 --edit·--thread 와 같이 못 써요.');
781
+ if (!channel) die('Give the channel the message is in.\n comitia slack project_0to1 --delete 1757769600.123456\n comitia slack project_0to1 --delete-of "renamed z2o to Comitia"');
782
+ if (flags.delete && !/^\d{10}\.\d{6}$/.test(flags.delete)) die(`--delete needs a Slack ts (e.g. 1757769600.123456). To find it by text, use --delete-of "<part of the message>"`);
783
+ if (flags.edit || flags.editOf || flags.thread || flags.threadOf) die('--delete cannot be combined with --edit or --thread.');
774
784
  const args = { company_id: needCompany(flags), channel };
775
785
  if (flags.delete) args.ts = flags.delete;
776
786
  else args.of = flags.deleteOf;
777
787
  console.log(await callTool('delete_slack', args));
778
788
  return;
779
789
  }
780
- if (!channel || !file) die('채널과 파일을 적어 주세요.\n comitia slack project_0to1 글.txt\n ... | comitia slack project_0to1 -\n comitia slack weekly-meeting 글.txt --thread-of "0913 위클리" # 그 글의 스레드에 답글로');
790
+ if (!channel || !file) die('Give a channel and a file.\n comitia slack project_0to1 message.txt\n ... | comitia slack project_0to1 -\n comitia slack weekly-meeting message.txt --thread-of "0913 weekly" # reply in that message\'s thread');
781
791
  const text = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8');
782
- if (!text.trim()) die('올릴 글이 비었어요.');
792
+ if (!text.trim()) die('The message is empty.');
783
793
  const tsShape = /^\d{10}\.\d{6}$/;
784
- if (flags.thread && !tsShape.test(flags.thread)) die(`--thread 는 슬랙 ts 모양이어야 해요 (예: 1757769600.123456). 검색어로 찾으려면 --thread-of "<글의 일부>"`);
785
- if (flags.edit && !tsShape.test(flags.edit)) die(`--edit 는 슬랙 ts 모양이어야 해요 (예: 1757769600.123456). 검색어로 찾으려면 --edit-of "<글의 일부>"`);
786
- if ((flags.edit || flags.editOf) && (flags.thread || flags.threadOf)) die('--edit 와 --thread 는 같이 못 써요. 고치기는 새 글이 아니에요.');
794
+ if (flags.thread && !tsShape.test(flags.thread)) die(`--thread needs a Slack ts (e.g. 1757769600.123456). To find it by text, use --thread-of "<part of the message>"`);
795
+ if (flags.edit && !tsShape.test(flags.edit)) die(`--edit needs a Slack ts (e.g. 1757769600.123456). To find it by text, use --edit-of "<part of the message>"`);
796
+ if ((flags.edit || flags.editOf) && (flags.thread || flags.threadOf)) die('--edit and --thread cannot be combined. An edit is not a new message.');
787
797
  const args = { company_id: needCompany(flags), channel, text };
788
798
  if (flags.edit) args.edit_ts = flags.edit;
789
799
  else if (flags.editOf) args.edit_of = flags.editOf;
@@ -795,7 +805,7 @@ const COMMANDS = {
795
805
  /** 지금 당겨오기. 자동 수집(매시 정각)을 기다리지 않고 방금 것을 가져온다. */
796
806
  async sync([source], flags) {
797
807
  const ok = ['slack', 'tiro', 'notion', 'calendar'];
798
- if (source && !ok.includes(source)) die(`고를 수 있는 것: ${ok.join(' · ')} (비우면 전부)`);
808
+ if (source && !ok.includes(source)) die(`Choose one of: ${ok.join(' · ')} (leave empty for all)`);
799
809
  console.log(await callTool('sync_sources', { company_id: needCompany(flags), ...(source ? { source } : {}) }));
800
810
  },
801
811
  };
@@ -817,6 +827,6 @@ if (!cmd || flags.help || cmd === 'help') {
817
827
  }
818
828
 
819
829
  const run = COMMANDS[cmd];
820
- if (!run) die(`모르는 명령이에요: ${cmd}\n\n${HELP}`);
830
+ if (!run) die(`Unknown command: ${cmd}\n\n${HELP}`);
821
831
 
822
- run(args, flags).catch((e) => die(`문제가 생겼어요: ${e.message}`));
832
+ run(args, flags).catch((e) => die(`Something went wrong: ${e.message}`));