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/README.md +87 -63
- package/index.js +164 -139
- package/package.json +8 -3
- package/plugin/.claude-plugin/plugin.json +8 -2
- package/plugin/skills/ingest/SKILL.md +51 -43
- package/plugin/skills/problem/SKILL.md +311 -0
- package/plugin/skills/read/SKILL.md +43 -37
- package/plugin/skills/slack/SKILL.md +68 -45
- package/plugin/skills/sync/SKILL.md +24 -18
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('
|
|
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(
|
|
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('
|
|
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(
|
|
131
|
+
if (!res.ok) die(`The server replied ${res.status}.`);
|
|
132
132
|
|
|
133
133
|
const body = await res.json();
|
|
134
|
-
if (body.error) die(
|
|
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"
|
|
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(
|
|
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('
|
|
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 ? '
|
|
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('
|
|
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
|
-
?
|
|
234
|
-
: '
|
|
235
|
-
console.log(`\
|
|
236
|
-
console.log('(
|
|
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(
|
|
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 ||
|
|
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
|
-
|
|
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('
|
|
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}
|
|
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 === '
|
|
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
|
-
|
|
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\` |
|
|
353
|
-
| \`comitia find
|
|
354
|
-
| \`comitia raw <id>\` |
|
|
355
|
-
| \`comitia forget <id>
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
축 구성·문서 형식·건드리지 않을 곳·정리 순서가 거기 있다. **회사마다 다르고
|
|
361
|
-
사용자가 고쳐 두었을 수 있으므로, 이 블록의 기억에 의존하지 않는다.**
|
|
367
|
+
In Claude Code, the skills carry this sequence.
|
|
362
368
|
|
|
363
|
-
|
|
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
|
-
|
|
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 —
|
|
431
|
-
|
|
432
|
-
comitia init
|
|
433
|
-
comitia init -g
|
|
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 [
|
|
445
|
-
comitia search
|
|
446
|
-
comitia read
|
|
447
|
-
comitia graph
|
|
448
|
-
|
|
449
|
-
──
|
|
450
|
-
comitia recent [
|
|
451
|
-
comitia find
|
|
452
|
-
comitia raw <id...>
|
|
453
|
-
comitia forget <id>
|
|
454
|
-
comitia sync [slack|tiro|...]
|
|
455
|
-
comitia slack
|
|
456
|
-
|
|
457
|
-
--thread
|
|
458
|
-
--
|
|
459
|
-
--edit
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
comitia
|
|
464
|
-
comitia
|
|
465
|
-
comitia
|
|
466
|
-
comitia
|
|
467
|
-
comitia
|
|
468
|
-
comitia
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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('\
|
|
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('
|
|
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('
|
|
511
|
+
console.log('Connected.\n');
|
|
498
512
|
console.log(text);
|
|
499
|
-
if (!settings().companyId) console.log('\
|
|
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('
|
|
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(
|
|
544
|
+
console.log(`Workspace: ${rows[0][2]}`);
|
|
531
545
|
} else if (rows.length > 1) {
|
|
532
546
|
// ⚠️ **여럿이면 고르지 않는다.** 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
|
|
533
547
|
needPick = true;
|
|
534
|
-
console.log('
|
|
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('
|
|
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('\
|
|
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
|
-
' (
|
|
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
|
|
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('\
|
|
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
|
|
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(
|
|
611
|
-
console.log(
|
|
612
|
-
console.log(
|
|
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('
|
|
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\
|
|
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(
|
|
652
|
+
die(`No workspace matches "${q}".\n\n${text}${why}`);
|
|
639
653
|
}
|
|
640
654
|
// ⚠️ 여럿이면 고르지 않는다 — 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
|
|
641
655
|
if (hit.length > 1) {
|
|
642
|
-
die(
|
|
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(
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
|
770
|
-
if (flags.edit && !tsShape.test(flags.edit)) die(`--edit
|
|
771
|
-
if ((flags.edit || flags.editOf) && (flags.thread || flags.threadOf)) die('--edit
|
|
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(
|
|
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(
|
|
830
|
+
if (!run) die(`Unknown command: ${cmd}\n\n${HELP}`);
|
|
806
831
|
|
|
807
|
-
run(args, flags).catch((e) => die(
|
|
832
|
+
run(args, flags).catch((e) => die(`Something went wrong: ${e.message}`));
|