comitia 0.2.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,27 +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\`)은 팀 전체가 보는 일이다** — 채널과 문구를 먼저 보여 주고 허락받은 뒤에만 올린다.
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 |
364
376
 
365
- Claude Code 에서는 스킬 \`/comitia:read\` · \`/comitia:ingest\` · \`/comitia:slack\` · \`/comitia:sync\` 이 순서를 담고 있다.
366
- 자세한 명령은 \`comitia --help\`.
377
+ For the full command list, run \`comitia --help\`.
367
378
  ${MARK_END}`;
368
379
 
369
380
  /** 규칙 파일에 블록을 심는다. **두 번 해도 하나만 남는다.** */
@@ -397,13 +408,13 @@ function writeRules(file) {
397
408
  at = j;
398
409
  });
399
410
  after += before.slice(at);
400
- if (after === before) return { file, state: '그대로' };
411
+ if (after === before) return { file, state: 'unchanged' };
401
412
  writeFileSync(file, after);
402
- return { file, state: '갱신' };
413
+ return { file, state: 'updated' };
403
414
  }
404
415
  after = before.trimEnd() + (before.trim() ? '\n\n' : '') + BLOCK + '\n';
405
416
  writeFileSync(file, after);
406
- return { file, state: had ? '추가' : '새로 만듦' };
417
+ return { file, state: had ? 'appended' : 'created' };
407
418
  }
408
419
 
409
420
  // ── 인자 파싱 ────────────────────────────────────────────
@@ -419,6 +430,8 @@ function parseArgs(argv) {
419
430
  else if (a === '--thread-of') flags.threadOf = argv[++i];
420
431
  else if (a === '--edit') flags.edit = argv[++i];
421
432
  else if (a === '--edit-of') flags.editOf = argv[++i];
433
+ else if (a === '--delete') flags.delete = argv[++i];
434
+ else if (a === '--delete-of') flags.deleteOf = argv[++i];
422
435
  else if (a === '-g' || a === '--global') flags.global = true;
423
436
  else if (a === '--paste') flags.paste = true;
424
437
  else if (a === '-h' || a === '--help') flags.help = true;
@@ -427,51 +440,52 @@ function parseArgs(argv) {
427
440
  return { flags, rest };
428
441
  }
429
442
 
430
- const HELP = `comitia — 회사 온톨로지를 내가 쓰는 AI에서 꺼내 쓴다
431
-
432
- comitia init 한 번에 붙이기 — 브라우저 로그인 + 워크스페이스 + AI 규칙파일
433
- comitia init -g 규칙을 **모든 폴더**에 (~/.claude 등). 폴더마다 안 해도 됨
434
- comitia login 브라우저에서 「허용」 한 번으로 연결 (토큰을 볼 일이 없다)
435
- comitia login --paste 브라우저가 다른 기기에 있을 때 — 화면의 코드를 붙여 넣는다
436
- comitia login <토큰> 발급받은 토큰을 직접 넣기
437
- comitia logout 저장한 토큰 지우기
438
- comitia whoami 지금 어떻게 붙어 있나
439
-
440
- comitia companies 워크스페이스 목록
441
- comitia use <id 또는 이름> 기본 워크스페이스 정하기
442
-
443
- ── 정리된 문서 ──
444
- comitia list [접두어] 문서 목록 (예: comitia list ledger/)
445
- comitia search <낱말> 본문 검색
446
- comitia read <경로...> 문서 본문
447
- comitia graph 문서가 서로 어떻게 이어져 있는지
448
-
449
- ── 정리 전 원본 ── 슬랙·메일이 들어온 그대로
450
- comitia recent [개수] 최근 들어온 원본
451
- comitia find <낱말> 원본에서 찾기
452
- comitia raw <id...> 원본 본문 (id 는 recent·find 가 준 것)
453
- comitia forget <id> <낱말...> 원본에서 낱말 지우기 — 다시 수집돼도 안 돌아옴 (쓰기 토큰)
454
- comitia sync [slack|tiro|...] 지금 당겨오기 — 매시 자동 수집을 기다리지 않고 (쓰기 토큰)
455
- comitia slack <채널> <파일|-> 연결된 봇으로 슬랙에 올리기 (쓰기 토큰 · 봇에 chat:write 필요)
456
- --thread <ts> 그 글의 스레드에 답글로 (부모 ts)
457
- --thread-of "<검색어>" 부모 글을 본문으로 찾아 그 스레드에 (최근 100건 중 최신)
458
- --edit <ts> 새 글 대신 봇이 올린 그 글을 통째로 바꾸기 (봇 글만)
459
- --edit-of "<검색어>" 고칠 봇 글을 본문으로 찾아서 (최근 100건 중 최신)
460
-
461
- ── 정리를 내 AI 가 한다 ── (쓰기 토큰 필요)
462
- comitia rules 이 워크스페이스의 위키 규칙 — **쓰기 전에 먼저**
463
- comitia skills Claude Code 스킬 설치·갱신 (/comitia:read · /comitia:ingest · /comitia:slack · /comitia:sync)
464
- comitia push <제목> <파일|-> 원본 넣기
465
- comitia build 원문·장부·표 만들기 (코드만, 0원)
466
- comitia pending 요약이 필요한 원본
467
- comitia write <경로> <파일|-> 요약을 위키에 남기기 (예: topics/물류.md)
468
- comitia organize Comitia 의 AI 에게 정리를 맡긴다 (직접 안 쓸 때)
469
-
470
- -c, --company <id> 이번만 다른 워크스페이스로
471
- -g, --global init 을 모든 폴더에 적용
472
- --paste login·init 을 코드 붙여넣기로
473
-
474
- 환경 변수: 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)`;
475
489
 
476
490
  // ── 명령 ────────────────────────────────────────────────
477
491
 
@@ -482,21 +496,21 @@ const COMMANDS = {
482
496
  await browserLogin({ paste: flags.paste });
483
497
  if (!settings().companyId) {
484
498
  console.log('\n' + (await callTool('list_companies', {})));
485
- console.log('\n`comitia use <id 또는 이름>` 으로 기본 워크스페이스를 정해 두세요.');
499
+ console.log('\nSet a default workspace with `comitia use <id or name>`.');
486
500
  }
487
501
  return;
488
502
  }
489
503
  if (!TOKEN_RE.test(token)) {
490
- die('토큰 모양이 아니에요. `z2o_live_` 로 시작하는 한 줄이어야 합니다.');
504
+ die('That is not a token. It should be a single line starting with `z2o_live_`.');
491
505
  }
492
506
  // 🔑 **저장하기 전에 한 번 써 본다.** 틀린 토큰을 저장해 두면 나중에 엉뚱한
493
507
  // 명령에서 실패하고, 사람은 그게 토큰 탓인 줄 모른다. 확인에 실패하면
494
508
  // `callTool` 이 여기서 끝내므로 **잘못된 토큰이 파일에 남지 않는다.**
495
509
  const text = await callTool('list_companies', {}, { token });
496
510
  saveConf({ ...loadConf(), token });
497
- console.log('연결했어요.\n');
511
+ console.log('Connected.\n');
498
512
  console.log(text);
499
- if (!settings().companyId) console.log('\n`comitia use <id>` 로 기본 워크스페이스를 정해 두세요.');
513
+ if (!settings().companyId) console.log('\nSet a default workspace with `comitia use <id>`.');
500
514
  },
501
515
 
502
516
  /** 한 번에 다 붙인다 — 설치 뒤 이것만 하면 된다.
@@ -515,7 +529,7 @@ const COMMANDS = {
515
529
  await browserLogin({ paste: flags.paste });
516
530
  console.log('');
517
531
  } else {
518
- console.log('이미 연결돼 있어요.\n');
532
+ console.log('Already connected.\n');
519
533
  }
520
534
 
521
535
  // ② 워크스페이스 — 하나뿐이면 묻지 않고 정한다.
@@ -523,20 +537,20 @@ const COMMANDS = {
523
537
  if (!settings().companyId) {
524
538
  const rows = (await callTool('list_companies', {}))
525
539
  .split('\n')
526
- .map((l) => l.match(/^- (\S+) — (.+?)(?: \(초대받음.*\))?$/))
540
+ .map((l) => l.match(/^- (\S+) — (.+?)(?: \((?:초대받음|invited).*\))?$/))
527
541
  .filter(Boolean);
528
542
  if (rows.length === 1) {
529
543
  saveConf({ ...loadConf(), companyId: rows[0][1] });
530
- console.log(`워크스페이스: ${rows[0][2]}`);
544
+ console.log(`Workspace: ${rows[0][2]}`);
531
545
  } else if (rows.length > 1) {
532
546
  // ⚠️ **여럿이면 고르지 않는다.** 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
533
547
  needPick = true;
534
- console.log('워크스페이스가 여럿이에요. 하나 고르세요:\n');
548
+ console.log('You have several workspaces. Pick one:\n');
535
549
  for (const r of rows) console.log(` ${r[1]} — ${r[2]}`);
536
550
  console.log('');
537
551
  } else {
538
552
  needPick = true;
539
- console.log('쓸 수 있는 워크스페이스가 없어요. z2o.kr 에서 만들거나 초대받으세요.\n');
553
+ console.log('No workspace available. Create one at z2o.kr or get invited to one.\n');
540
554
  }
541
555
  }
542
556
 
@@ -551,13 +565,13 @@ const COMMANDS = {
551
565
  const r = writeRules(join(dir, f));
552
566
  console.log(`~/${d}/${f} — ${r.state}`);
553
567
  }
554
- console.log(' (모든 폴더에서 통합니다)');
568
+ console.log(' (works in every folder)');
555
569
  reportPlugin();
556
- console.log('\n이제 AI 에게 그냥 물어보세요:');
557
- console.log(' "최근 슬랙에서 나온 얘기 정리해줘"');
570
+ console.log('\nNow just ask your AI:');
571
+ console.log(' "catch me up on recent Slack"');
558
572
  if (needPick) {
559
- console.log('\n🔴 아직 한 걸음 남았어요:');
560
- console.log(' comitia use <id 또는 이름>');
573
+ console.log('\n🔴 One step left:');
574
+ console.log(' comitia use <id or name>');
561
575
  }
562
576
  return;
563
577
  }
@@ -574,19 +588,19 @@ const COMMANDS = {
574
588
  console.log(`${r.file} — ${r.state}`);
575
589
  }
576
590
  console.log(
577
- ' (다른 도구를 쓰면 그 도구의 규칙 파일을 만들어 두고 `comitia init` 을 다시 돌리세요)',
591
+ ' (Using another tool? Create its rules file first, then run `comitia init` again)',
578
592
  );
579
593
  reportPlugin();
580
594
 
581
595
  // ⚠️ **다 된 것처럼 끝내지 않는다.** 워크스페이스를 못 정했으면 아직 아무것도 안 된다 —
582
596
  // 여기서 "이제 물어보세요" 라고 하면 사람은 됐다고 믿고 넘어간다.
583
597
  if (needPick) {
584
- console.log('\n🔴 아직 한 걸음 남았어요:');
585
- 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)');
586
600
  } else {
587
- console.log('\n이제 AI 에게 그냥 물어보세요:');
588
- console.log(' "최근 슬랙에서 나온 얘기 정리해줘"');
589
- 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"');
590
604
  }
591
605
  void flags;
592
606
  },
@@ -594,7 +608,7 @@ const COMMANDS = {
594
608
  // 스킬만 깔거나 새 판으로 바꾼다 — 토큰이 없어도 된다. `npm i -g comitia` 로 올린 뒤에 한 번.
595
609
  async skills() {
596
610
  const r = installPlugin();
597
- 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.");
598
612
  reportPlugin(r);
599
613
  },
600
614
 
@@ -602,14 +616,14 @@ const COMMANDS = {
602
616
  const conf = loadConf();
603
617
  delete conf.token;
604
618
  saveConf(conf);
605
- console.log('저장한 토큰을 지웠어요. (서버 쪽에서 끊으려면 프로필 → 「연결 토큰」에서 끊으세요.)');
619
+ console.log('Removed the stored token. (To revoke it server-side, go to your profile → Connection tokens.)');
606
620
  },
607
621
 
608
622
  async whoami() {
609
623
  const { api, token, companyId } = settings();
610
- console.log(`서버 ${api}`);
611
- console.log(`토큰 ${token ? token.slice(0, 17) + '…' : '(없음)'}`);
612
- 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)'}`);
613
627
  if (!token) return;
614
628
  console.log('');
615
629
  console.log(await callTool('list_companies', {}));
@@ -620,12 +634,12 @@ const COMMANDS = {
620
634
  },
621
635
 
622
636
  async use([q]) {
623
- if (!q) die('워크스페이스 id 나 이름 일부를 적어 주세요.');
637
+ if (!q) die('Give a workspace id or part of its name.');
624
638
  const text = await callTool('list_companies', {});
625
639
  // 목록 줄은 `- <id> — <이름>` 모양이다.
626
640
  const rows = text
627
641
  .split('\n')
628
- .map((l) => l.match(/^- (\S+) — (.+?)(?: \(초대받음.*\))?$/))
642
+ .map((l) => l.match(/^- (\S+) — (.+?)(?: \((?:초대받음|invited).*\))?$/))
629
643
  .filter(Boolean)
630
644
  .map((m) => ({ id: m[1], name: m[2] }));
631
645
  const hit = rows.filter((r) => r.id === q || r.name.includes(q));
@@ -633,16 +647,16 @@ const COMMANDS = {
633
647
  // 🔑 **매인 토큰이면 목록에 하나만 나온다.** 그걸 모르면 "왜 내 회사가 없지" 가 된다.
634
648
  const why =
635
649
  rows.length === 1
636
- ? '\n\n이 열쇠는 위 워크스페이스에만 매여 있어요. 다른 곳도 쓰려면 「전부」로 새로 발급하세요.'
650
+ ? '\n\nThis key is tied to the workspace above. To use others, issue a new key scoped to all of them.'
637
651
  : '';
638
- die(`「${q}」 에 해당하는 워크스페이스가 없어요.\n\n${text}${why}`);
652
+ die(`No workspace matches "${q}".\n\n${text}${why}`);
639
653
  }
640
654
  // ⚠️ 여럿이면 고르지 않는다 — 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
641
655
  if (hit.length > 1) {
642
- 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'));
643
657
  }
644
658
  saveConf({ ...loadConf(), companyId: hit[0].id });
645
- console.log(`기본 워크스페이스를 「${hit[0].name}」 로 정했어요.`);
659
+ console.log(`Default workspace set to "${hit[0].name}".`);
646
660
  },
647
661
 
648
662
  async list(args, flags) {
@@ -651,12 +665,12 @@ const COMMANDS = {
651
665
 
652
666
  async search(args, flags) {
653
667
  const q = args.join(' ').trim();
654
- if (!q) die('찾을 낱말을 적어 주세요.');
668
+ if (!q) die('Give a word to search for.');
655
669
  console.log(await callTool('search_pages', { company_id: needCompany(flags), query: q }));
656
670
  },
657
671
 
658
672
  async read(args, flags) {
659
- if (args.length === 0) die('읽을 문서 경로를 적어 주세요. (`comitia list` 로 확인)');
673
+ if (args.length === 0) die('Give the page path to read. (`comitia list` shows them)');
660
674
  console.log(await callTool('read_pages', { company_id: needCompany(flags), paths: args }));
661
675
  },
662
676
 
@@ -682,28 +696,28 @@ const COMMANDS = {
682
696
 
683
697
  async find(args, flags) {
684
698
  const q = args.join(' ').trim();
685
- if (!q) die('찾을 낱말을 적어 주세요.');
699
+ if (!q) die('Give a word to search for.');
686
700
  console.log(await callTool('search_raw', { company_id: needCompany(flags), query: q }));
687
701
  },
688
702
 
689
703
  async raw(args, flags) {
690
- if (args.length === 0) die('읽을 원본 id 를 적어 주세요. (`comitia recent` 로 확인)');
704
+ if (args.length === 0) die('Give the raw id to read. (`comitia recent` shows them)');
691
705
  console.log(await callTool('read_raw', { company_id: needCompany(flags), ids: args }));
692
706
  },
693
707
 
694
708
  /** 원본에서 낱말을 지운다. 통째로 지우면 다음 수집에 되살아나지만 이건 안 돌아온다. */
695
709
  async forget(args, flags) {
696
710
  const [id, ...terms] = args;
697
- 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"');
698
712
  console.log(await callTool('forget_raw', { company_id: needCompany(flags), id, terms }));
699
713
  },
700
714
 
701
715
  async push(args, flags) {
702
716
  const [title, path] = args;
703
- 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" -');
704
718
  // `-` 면 표준입력. 파이프로 이어 쓰는 게 이 명령의 주된 쓰임새다.
705
719
  const content = path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
706
- if (!content.trim()) die('본문이 비어 있어요.');
720
+ if (!content.trim()) die('The body is empty.');
707
721
  console.log(await callTool('ingest_text', { company_id: needCompany(flags), title, content }));
708
722
  },
709
723
 
@@ -726,10 +740,10 @@ const COMMANDS = {
726
740
  async write(args, flags) {
727
741
  const [path, file] = args;
728
742
  if (!path || !file) {
729
- 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 -');
730
744
  }
731
745
  const content = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8');
732
- if (!content.trim()) die('본문이 비어 있어요.');
746
+ if (!content.trim()) die('The body is empty.');
733
747
  console.log(
734
748
  await callTool('write_pages', {
735
749
  company_id: needCompany(flags),
@@ -762,13 +776,24 @@ const COMMANDS = {
762
776
 
763
777
  /** 슬랙에 올린다 — 연결된 봇으로. ⚠️ 팀에게 보이는 일이라 AI 는 올리기 전에 사람에게 허락받는다. */
764
778
  async slack([channel, file], flags) {
765
- if (!channel || !file) die('채널과 파일을 적어 주세요.\n comitia slack project_0to1 글.txt\n ... | comitia slack project_0to1 -\n comitia slack weekly-meeting 글.txt --thread-of "0913 위클리" # 그 글의 스레드에 답글로');
779
+ // 🗑️ 지우기는 올릴 글이 없다 — 채널과 ts(또는 검색어)만 받는다. 되돌릴 수 없어 스킬이 먼저 허락받는다.
780
+ if (flags.delete || flags.deleteOf) {
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.');
784
+ const args = { company_id: needCompany(flags), channel };
785
+ if (flags.delete) args.ts = flags.delete;
786
+ else args.of = flags.deleteOf;
787
+ console.log(await callTool('delete_slack', args));
788
+ return;
789
+ }
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');
766
791
  const text = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8');
767
- if (!text.trim()) die('올릴 글이 비었어요.');
792
+ if (!text.trim()) die('The message is empty.');
768
793
  const tsShape = /^\d{10}\.\d{6}$/;
769
- if (flags.thread && !tsShape.test(flags.thread)) die(`--thread 는 슬랙 ts 모양이어야 해요 (예: 1757769600.123456). 검색어로 찾으려면 --thread-of "<글의 일부>"`);
770
- if (flags.edit && !tsShape.test(flags.edit)) die(`--edit 는 슬랙 ts 모양이어야 해요 (예: 1757769600.123456). 검색어로 찾으려면 --edit-of "<글의 일부>"`);
771
- 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.');
772
797
  const args = { company_id: needCompany(flags), channel, text };
773
798
  if (flags.edit) args.edit_ts = flags.edit;
774
799
  else if (flags.editOf) args.edit_of = flags.editOf;
@@ -780,7 +805,7 @@ const COMMANDS = {
780
805
  /** 지금 당겨오기. 자동 수집(매시 정각)을 기다리지 않고 방금 것을 가져온다. */
781
806
  async sync([source], flags) {
782
807
  const ok = ['slack', 'tiro', 'notion', 'calendar'];
783
- if (source && !ok.includes(source)) die(`고를 수 있는 것: ${ok.join(' · ')} (비우면 전부)`);
808
+ if (source && !ok.includes(source)) die(`Choose one of: ${ok.join(' · ')} (leave empty for all)`);
784
809
  console.log(await callTool('sync_sources', { company_id: needCompany(flags), ...(source ? { source } : {}) }));
785
810
  },
786
811
  };
@@ -802,6 +827,6 @@ if (!cmd || flags.help || cmd === 'help') {
802
827
  }
803
828
 
804
829
  const run = COMMANDS[cmd];
805
- if (!run) die(`모르는 명령이에요: ${cmd}\n\n${HELP}`);
830
+ if (!run) die(`Unknown command: ${cmd}\n\n${HELP}`);
806
831
 
807
- run(args, flags).catch((e) => die(`문제가 생겼어요: ${e.message}`));
832
+ run(args, flags).catch((e) => die(`Something went wrong: ${e.message}`));