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/README.md +87 -63
- package/index.js +154 -144
- 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 +65 -54
- 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,28 +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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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 —
|
|
434
|
-
|
|
435
|
-
comitia init
|
|
436
|
-
comitia init -g
|
|
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 [
|
|
448
|
-
comitia search
|
|
449
|
-
comitia read
|
|
450
|
-
comitia graph
|
|
451
|
-
|
|
452
|
-
──
|
|
453
|
-
comitia recent [
|
|
454
|
-
comitia find
|
|
455
|
-
comitia raw <id...>
|
|
456
|
-
comitia forget <id>
|
|
457
|
-
comitia sync [slack|tiro|...]
|
|
458
|
-
comitia slack
|
|
459
|
-
comitia slack
|
|
460
|
-
--thread <ts>
|
|
461
|
-
--thread-of "
|
|
462
|
-
--edit <ts>
|
|
463
|
-
--edit-of "
|
|
464
|
-
|
|
465
|
-
──
|
|
466
|
-
comitia rules
|
|
467
|
-
comitia skills Claude Code
|
|
468
|
-
comitia push
|
|
469
|
-
comitia build
|
|
470
|
-
comitia pending
|
|
471
|
-
comitia write
|
|
472
|
-
comitia organize
|
|
473
|
-
|
|
474
|
-
-c, --company <id>
|
|
475
|
-
-g, --global init
|
|
476
|
-
--paste login
|
|
477
|
-
|
|
478
|
-
|
|
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('\
|
|
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('
|
|
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('
|
|
511
|
+
console.log('Connected.\n');
|
|
502
512
|
console.log(text);
|
|
503
|
-
if (!settings().companyId) console.log('\
|
|
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('
|
|
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(
|
|
544
|
+
console.log(`Workspace: ${rows[0][2]}`);
|
|
535
545
|
} else if (rows.length > 1) {
|
|
536
546
|
// ⚠️ **여럿이면 고르지 않는다.** 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
|
|
537
547
|
needPick = true;
|
|
538
|
-
console.log('
|
|
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('
|
|
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('\
|
|
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
|
-
' (
|
|
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
|
|
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('\
|
|
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
|
|
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(
|
|
615
|
-
console.log(
|
|
616
|
-
console.log(
|
|
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('
|
|
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\
|
|
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(
|
|
652
|
+
die(`No workspace matches "${q}".\n\n${text}${why}`);
|
|
643
653
|
}
|
|
644
654
|
// ⚠️ 여럿이면 고르지 않는다 — 엉뚱한 회사에 자료를 넣게 되는 쪽이 훨씬 나쁘다.
|
|
645
655
|
if (hit.length > 1) {
|
|
646
|
-
die(
|
|
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(
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
772
|
-
if (flags.delete && !/^\d{10}\.\d{6}$/.test(flags.delete)) die(`--delete
|
|
773
|
-
if (flags.edit || flags.editOf || flags.thread || flags.threadOf) die('--delete
|
|
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('
|
|
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
|
|
785
|
-
if (flags.edit && !tsShape.test(flags.edit)) die(`--edit
|
|
786
|
-
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.');
|
|
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(
|
|
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(
|
|
830
|
+
if (!run) die(`Unknown command: ${cmd}\n\n${HELP}`);
|
|
821
831
|
|
|
822
|
-
run(args, flags).catch((e) => die(
|
|
832
|
+
run(args, flags).catch((e) => die(`Something went wrong: ${e.message}`));
|