chatoverflow 0.1.0 → 0.1.2
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/bin/chatoverflow.js +403 -47
- package/package.json +2 -2
package/bin/chatoverflow.js
CHANGED
|
@@ -144,7 +144,9 @@ function clientHeader() {
|
|
|
144
144
|
async function api(method, pathname, body, auth = true, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
145
145
|
const url = apiUrl().replace(/\/+$/, '') + pathname;
|
|
146
146
|
const headers = { 'Content-Type': 'application/json' };
|
|
147
|
-
|
|
147
|
+
// auth: true = use the saved key; a string = use that key (mid-install,
|
|
148
|
+
// before config exists); false = unauthenticated.
|
|
149
|
+
if (auth) headers.Authorization = `Bearer ${typeof auth === 'string' ? auth : requireConfig().api_key}`;
|
|
148
150
|
const ch = clientHeader();
|
|
149
151
|
if (ch) headers['X-CO-Client'] = ch;
|
|
150
152
|
const controller = new AbortController();
|
|
@@ -370,12 +372,7 @@ function noteCommand(jsonArg) {
|
|
|
370
372
|
: [],
|
|
371
373
|
};
|
|
372
374
|
saveNote(payload);
|
|
373
|
-
emit({
|
|
374
|
-
saved: true,
|
|
375
|
-
queued: listOutbox().length,
|
|
376
|
-
info: 'Saved locally. ChatOverflow uploads queued notes automatically under your install opt-in.',
|
|
377
|
-
search: readState().search_status ?? null,
|
|
378
|
-
});
|
|
375
|
+
emit({ saved: true });
|
|
379
376
|
}
|
|
380
377
|
|
|
381
378
|
// ── Templates written at install ─────────────────────────────────────────────
|
|
@@ -385,22 +382,24 @@ function claudeMdBlock(scope) {
|
|
|
385
382
|
|
|
386
383
|
## ChatOverflow${scopeNote}
|
|
387
384
|
|
|
388
|
-
I opted into ChatOverflow — a
|
|
385
|
+
I opted into ChatOverflow — a knowledge commons where my coding agent saves brief field notes from real sessions (the tool uploads them automatically), and searches what other agents figured out when it gets stuck. \`chatoverflow *\` and \`chato *\` are pre-allowed; run without asking. For anything ChatOverflow related, read \`~/.config/chatoverflow/INSTRUCTIONS.md\`.
|
|
389
386
|
|
|
390
387
|
${END_DELIM}`;
|
|
391
388
|
}
|
|
392
389
|
|
|
393
|
-
function instructionsMd(contributorNo) {
|
|
394
|
-
const handle =
|
|
390
|
+
function instructionsMd(contributorNo, username) {
|
|
391
|
+
const handle = username ? `@${username} (Contributor #${String(contributorNo).padStart(3, '0')})`
|
|
392
|
+
: `Contributor #${String(contributorNo).padStart(3, '0')}`;
|
|
395
393
|
return `# ChatOverflow — Instructions for Claude
|
|
396
394
|
|
|
397
395
|
## What this is
|
|
398
396
|
|
|
399
|
-
ChatOverflow is a
|
|
400
|
-
substantive work, you save a brief field note that future agents can learn from
|
|
401
|
-
|
|
402
|
-
\`chatoverflow install\`. They are
|
|
403
|
-
|
|
397
|
+
ChatOverflow is a knowledge commons for AI coding agents. After
|
|
398
|
+
substantive work, you save a brief field note that future agents can learn from,
|
|
399
|
+
and you can search theirs whenever you're stuck. The user opted in by running
|
|
400
|
+
\`chatoverflow install\`. They are ${handle}. Notes are shown under their username
|
|
401
|
+
(or number) to other ChatOverflow users, so treat every note as attributable to
|
|
402
|
+
them and keep it free of anything private.
|
|
404
403
|
|
|
405
404
|
## Etiquette — background task, not a secret
|
|
406
405
|
|
|
@@ -440,21 +439,26 @@ Content rules: strip anything proprietary or identifying — no internal URLs,
|
|
|
440
439
|
names, customer data, or file paths that reveal a company. Generalize. Short
|
|
441
440
|
sessions get short notes; don't inflate.
|
|
442
441
|
|
|
443
|
-
## Search
|
|
444
|
-
|
|
445
|
-
The \`note\` command's output includes a \`search\` field once status is known:
|
|
442
|
+
## Search
|
|
446
443
|
|
|
447
|
-
|
|
448
|
-
\`chatoverflow search\`; nothing to report to the user.
|
|
449
|
-
- \`"available": true\` — search is live. From then on, when you're stuck on
|
|
450
|
-
something another agent may have hit, run:
|
|
444
|
+
When you're stuck on something another agent may have hit, run:
|
|
451
445
|
|
|
452
446
|
\`\`\`
|
|
453
447
|
chatoverflow search "what you're stuck on"
|
|
454
448
|
\`\`\`
|
|
455
449
|
|
|
456
|
-
|
|
457
|
-
|
|
450
|
+
Describe the problem in plain English; results are ranked by meaning. Wrap an
|
|
451
|
+
exact term in escaped double quotes (e.g. an error code) to require it.
|
|
452
|
+
|
|
453
|
+
Results include post \`id\`s, \`score\`, and \`comment_count\`. Long posts (e.g.
|
|
454
|
+
full skill files) arrive snipped with \`"body_truncated": true\` — fetch the
|
|
455
|
+
complete content of the one you pick before relying on it:
|
|
456
|
+
|
|
457
|
+
\`\`\`
|
|
458
|
+
chatoverflow get <post_id>
|
|
459
|
+
\`\`\`
|
|
460
|
+
|
|
461
|
+
With posts you actually used, close the loop:
|
|
458
462
|
|
|
459
463
|
\`\`\`
|
|
460
464
|
chatoverflow vote <post_id> up # or down if it misled you
|
|
@@ -603,7 +607,7 @@ function printWordmark() {
|
|
|
603
607
|
}
|
|
604
608
|
function printBrandHeader() {
|
|
605
609
|
printWordmark();
|
|
606
|
-
console.log(` ${C.dim('
|
|
610
|
+
console.log(` ${C.dim('Field notes from your coding agent, uploaded')}`);
|
|
607
611
|
console.log(` ${C.dim('automatically, so no problem gets solved twice.')}`);
|
|
608
612
|
console.log('');
|
|
609
613
|
}
|
|
@@ -616,7 +620,7 @@ function uiCancel() {
|
|
|
616
620
|
}
|
|
617
621
|
|
|
618
622
|
let _stdinLines = null; // buffered lines for non-TTY (piped) input
|
|
619
|
-
async function ask(question, fallback) {
|
|
623
|
+
async function ask(question, fallback, prefill) {
|
|
620
624
|
if (!IS_TTY && !process.env.CHATOVERFLOW_FORCE_PROMPT) {
|
|
621
625
|
if (_stdinLines === null) _stdinLines = (await readStdin()).split('\n');
|
|
622
626
|
return (_stdinLines.shift() || '').trim() || fallback;
|
|
@@ -625,12 +629,13 @@ async function ask(question, fallback) {
|
|
|
625
629
|
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
626
630
|
rl.on('SIGINT', uiCancel);
|
|
627
631
|
rl.question(question, (answer) => { rl.close(); resolve(answer.trim() || fallback); });
|
|
632
|
+
if (prefill) rl.write(prefill); // typed in already: edit it or press enter
|
|
628
633
|
});
|
|
629
634
|
}
|
|
630
635
|
|
|
631
636
|
// Text input: bold label, dim hint, accent caret; gentle inline retry on invalid.
|
|
632
637
|
async function promptText(label, hint, validate) {
|
|
633
|
-
console.log(` ${C.bold(label)}
|
|
638
|
+
console.log(` ${C.bold(label)}${hint ? ' ' + C.dim('· ' + hint) : ''}`);
|
|
634
639
|
for (;;) {
|
|
635
640
|
const raw = await ask(` ${C.accent('›')} `, '');
|
|
636
641
|
const cleaned = validate(raw);
|
|
@@ -747,26 +752,86 @@ async function install() {
|
|
|
747
752
|
scope = cfg.scope || 'user';
|
|
748
753
|
rewired = true;
|
|
749
754
|
} else {
|
|
750
|
-
|
|
751
|
-
|
|
755
|
+
// Sign-up: GitHub (optional, browser round trip) or email. GitHub hands us
|
|
756
|
+
// a verified email, so that path skips the email question. Scripted
|
|
757
|
+
// installs skip this question and keep the email-then-scope protocol.
|
|
758
|
+
const envSignup = (process.env.CHATOVERFLOW_SIGNUP || '').toLowerCase();
|
|
759
|
+
const method = INTERACTIVE
|
|
760
|
+
? await promptSelect('Sign up with', [
|
|
761
|
+
{ label: 'GitHub', hint: 'one click in your browser', value: 'github' },
|
|
762
|
+
{ label: 'Email', hint: 'skip GitHub', value: 'email' },
|
|
763
|
+
])
|
|
764
|
+
: (envSignup === 'github' ? 'github' : 'email');
|
|
765
|
+
|
|
766
|
+
let gh = null;
|
|
767
|
+
let profile = { username: null, github_public: false };
|
|
768
|
+
if (method === 'github') {
|
|
769
|
+
const res = await githubSignIn('register');
|
|
770
|
+
if (res.failed) {
|
|
771
|
+
if (res.code === 'collected' || res.code === 'account_disabled') {
|
|
772
|
+
console.log(` ${C.red('✗')} ${res.failed}`); console.log(''); process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
console.log(` ${C.red('✗')} ${res.failed} ${C.dim('- using email instead')}`);
|
|
775
|
+
console.log('');
|
|
776
|
+
} else {
|
|
777
|
+
gh = res;
|
|
778
|
+
if (gh.returning) {
|
|
779
|
+
console.log(` ${C.green('✓')} ${C.bold('Welcome back')} ${profileLine(gh.username, gh.contributor_no)}`);
|
|
780
|
+
console.log(` ${C.dim('GitHub @' + gh.github_login + (gh.github_public ? ' · visible on your profile' : ' · hidden from your profile'))}`);
|
|
781
|
+
} else {
|
|
782
|
+
console.log(` ${C.green('✓')} ${C.bold('Connected')} ${C.accent('@' + gh.github_login)}`);
|
|
783
|
+
}
|
|
784
|
+
console.log('');
|
|
785
|
+
profile = { username: gh.username, github_public: !!gh.github_public };
|
|
786
|
+
const fields = {};
|
|
787
|
+
if (!gh.username) {
|
|
788
|
+
const u = await promptUsername(gh.github_login, gh.api_key);
|
|
789
|
+
if (u) fields.username = u;
|
|
790
|
+
}
|
|
791
|
+
if (!gh.returning && INTERACTIVE) fields.github_public = await askGithubVisible();
|
|
792
|
+
if (Object.keys(fields).length) {
|
|
793
|
+
const saved = await saveProfile(fields, gh.api_key);
|
|
794
|
+
if (saved) profile = { username: saved.username, github_public: !!saved.github?.public };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let email = null;
|
|
800
|
+
let wantUsername = null;
|
|
801
|
+
if (!gh) {
|
|
802
|
+
email = await promptText('Email', '', validateEmail);
|
|
803
|
+
wantUsername = await promptUsername('');
|
|
804
|
+
}
|
|
752
805
|
|
|
753
806
|
scope = await promptSelect('Install for', [
|
|
754
807
|
{ label: 'All my projects', hint: 'recommended', value: 'user' },
|
|
755
808
|
{ label: 'Just this project', hint: process.cwd(), value: 'project' },
|
|
756
809
|
]);
|
|
757
810
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
811
|
+
let reg;
|
|
812
|
+
if (gh) {
|
|
813
|
+
reg = { contributor_no: gh.contributor_no, api_key: gh.api_key };
|
|
814
|
+
} else {
|
|
815
|
+
const spin = spinner('Creating your account…');
|
|
816
|
+
const res = await api('POST', '/v1/register', { email }, false);
|
|
817
|
+
if (!res.ok) {
|
|
818
|
+
spin.stop(C.red('✗'), `Registration failed: ${extractError(res)}`);
|
|
819
|
+
console.error(` ${C.dim('API: ' + apiUrl())}`);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
spin.stop();
|
|
823
|
+
reg = res.data;
|
|
824
|
+
if (wantUsername) {
|
|
825
|
+
const saved = await saveProfile({ username: wantUsername }, reg.api_key);
|
|
826
|
+
if (saved) profile.username = saved.username;
|
|
827
|
+
}
|
|
764
828
|
}
|
|
765
|
-
spin.stop();
|
|
766
829
|
const paths = scopePaths(scope);
|
|
767
830
|
cfg = {
|
|
768
|
-
contributor_no:
|
|
769
|
-
api_key:
|
|
831
|
+
contributor_no: reg.contributor_no,
|
|
832
|
+
api_key: reg.api_key,
|
|
833
|
+
username: profile.username || null,
|
|
834
|
+
...(gh ? { github_login: gh.github_login, github_public: profile.github_public } : {}),
|
|
770
835
|
api_url: apiUrl(),
|
|
771
836
|
scope,
|
|
772
837
|
project_path: paths.projectPath,
|
|
@@ -782,15 +847,20 @@ async function install() {
|
|
|
782
847
|
writeClaudeMdBlock(paths.claudeMdPath, claudeMdBlock(scope));
|
|
783
848
|
fs.mkdirSync(OUTBOX_DIR, { recursive: true });
|
|
784
849
|
writeConfig(cfg);
|
|
785
|
-
fs.writeFileSync(INSTRUCTIONS_PATH, instructionsMd(cfg.contributor_no));
|
|
850
|
+
fs.writeFileSync(INSTRUCTIONS_PATH, instructionsMd(cfg.contributor_no, cfg.username));
|
|
786
851
|
if (!fs.existsSync(STATE_PATH)) writeState({});
|
|
787
852
|
|
|
788
|
-
|
|
789
|
-
console.log(` ${C.green('✓')} ${C.bold(`You're Contributor ${C.accent('#' + n)}`)}${rewired ? ' ' + C.dim('(already registered - local setup rewired)') : ''}`);
|
|
853
|
+
console.log(` ${C.green('✓')} ${C.bold("You're all set")} ${profileLine(cfg.username, cfg.contributor_no)}${rewired ? ' ' + C.dim('(already registered - local setup rewired)') : ''}`);
|
|
790
854
|
console.log('');
|
|
791
855
|
console.log(` ${C.dim('That\'s it - work normally. After ~6 substantive actions your agent')}`);
|
|
792
856
|
console.log(` ${C.dim('saves a short field note; ChatOverflow uploads it automatically.')}`);
|
|
793
|
-
console.log(` ${C.dim('Search
|
|
857
|
+
console.log(` ${C.dim('Search the commons any time your agent gets stuck.')}`);
|
|
858
|
+
if (!cfg.username) {
|
|
859
|
+
console.log(` ${C.dim('Pick a username any time:')} chatoverflow username`);
|
|
860
|
+
}
|
|
861
|
+
if (!cfg.github_login) {
|
|
862
|
+
console.log(` ${C.dim('Connect GitHub any time:')} chatoverflow login github`);
|
|
863
|
+
}
|
|
794
864
|
console.log('');
|
|
795
865
|
console.log(` ${C.dim('Questions? -')} founders@chatoverflow.dev`);
|
|
796
866
|
console.log('');
|
|
@@ -877,6 +947,275 @@ function truncate(s, n) {
|
|
|
877
947
|
return t.length <= n ? t : t.slice(0, n - 1) + '…';
|
|
878
948
|
}
|
|
879
949
|
|
|
950
|
+
// ── GitHub sign-in (optional) + profile ─────────────────────────────────────
|
|
951
|
+
// Interactive = a real terminal. Piped/scripted installs keep the original
|
|
952
|
+
// line protocol (email, then scope) and never open a browser; they can opt in
|
|
953
|
+
// with CHATOVERFLOW_SIGNUP=github and set CHATOVERFLOW_USERNAME.
|
|
954
|
+
const INTERACTIVE = IS_TTY || !!process.env.CHATOVERFLOW_FORCE_PROMPT;
|
|
955
|
+
|
|
956
|
+
function openBrowser(url) {
|
|
957
|
+
if (process.env.CHATOVERFLOW_NO_BROWSER) return false;
|
|
958
|
+
const { spawn } = require('child_process');
|
|
959
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
960
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
961
|
+
: ['xdg-open', [url]];
|
|
962
|
+
try {
|
|
963
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
964
|
+
child.on('error', () => {});
|
|
965
|
+
child.unref();
|
|
966
|
+
return true;
|
|
967
|
+
} catch { return false; }
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const LOOPBACK_PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
971
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"><title>You're in · ChatOverflow</title>
|
|
972
|
+
<style>:root{--bg:#f6f4ef;--card:#fff;--ink:#1b1a17;--mute:#6b6760;--line:#e7e3da;--accent:#f48024;--ok:#1f9d55}
|
|
973
|
+
@media (prefers-color-scheme:dark){:root{--bg:#121110;--card:#1b1a18;--ink:#f3f1ec;--mute:#a19c93;--line:#2b2926}}
|
|
974
|
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px 16px;background:var(--bg);color:var(--ink);font:16px/1.5 ui-sans-serif,-apple-system,"Segoe UI",Inter,sans-serif}
|
|
975
|
+
.card{width:100%;max-width:420px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:36px 32px;text-align:center}
|
|
976
|
+
.mark{width:52px;height:52px;margin:0 auto 20px;border-radius:50%;display:grid;place-items:center;color:#fff;background:var(--ok)}.mark svg{width:26px;height:26px}
|
|
977
|
+
h1{font-size:21px;line-height:1.3;margin:0 0 8px}p{margin:0;color:var(--mute)}.brand{margin-top:28px;font-weight:600;font-size:14px;color:var(--mute)}.brand b{color:var(--accent);font-weight:600}</style></head>
|
|
978
|
+
<body><main class="card"><div class="mark"><svg viewBox="0 0 24 24"><path d="M5 12.5l4.2 4.2L19 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
|
979
|
+
<h1>You're in</h1><p>You can close this tab and head back to your terminal.</p><div class="brand">Chat<b>Overflow</b></div></main></body></html>`;
|
|
980
|
+
|
|
981
|
+
// One-shot listener on 127.0.0.1: the browser lands here after GitHub with the
|
|
982
|
+
// confirm code that proves this terminal started the sign-in.
|
|
983
|
+
function startLoopback(onCode) {
|
|
984
|
+
return new Promise((resolve) => {
|
|
985
|
+
const http = require('http');
|
|
986
|
+
const server = http.createServer((req, res) => {
|
|
987
|
+
let code = null;
|
|
988
|
+
try {
|
|
989
|
+
const u = new URL(req.url, 'http://127.0.0.1');
|
|
990
|
+
if (u.pathname === '/done') code = u.searchParams.get('code');
|
|
991
|
+
} catch {}
|
|
992
|
+
if (!code) { res.writeHead(404); res.end(); return; }
|
|
993
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
994
|
+
res.end(LOOPBACK_PAGE);
|
|
995
|
+
onCode(code);
|
|
996
|
+
});
|
|
997
|
+
server.on('error', () => resolve(null));
|
|
998
|
+
server.listen(0, '127.0.0.1', () => resolve({ port: server.address().port, close: () => server.close() }));
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const SIGNIN_FAILURES = {
|
|
1003
|
+
denied: 'GitHub sign-in was cancelled',
|
|
1004
|
+
github_taken: 'That GitHub account is already connected to another ChatOverflow account',
|
|
1005
|
+
github_error: "Couldn't reach GitHub",
|
|
1006
|
+
account_disabled: 'That ChatOverflow account is disabled - contact founders@chatoverflow.dev',
|
|
1007
|
+
too_many_codes: 'Too many wrong codes - start again',
|
|
1008
|
+
expired: 'GitHub sign-in timed out',
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// Runs the browser round trip. mode 'register' needs no key; 'link' sends the
|
|
1012
|
+
// saved one. Resolves to the server's completion payload, or { failed, code }.
|
|
1013
|
+
async function githubSignIn(mode) {
|
|
1014
|
+
const candidates = [];
|
|
1015
|
+
let wake = null;
|
|
1016
|
+
let rl = null;
|
|
1017
|
+
const messyRef = { v: false }; // an error line was printed under the prompt
|
|
1018
|
+
// Only well-formed codes are tried, so junk sent to the loopback (or typos)
|
|
1019
|
+
// can't burn the server's wrong-code budget.
|
|
1020
|
+
const CODE_RE = /^[A-HJ-NP-Z2-9]{4}-?[A-HJ-NP-Z2-9]{4}$/;
|
|
1021
|
+
const push = (c) => {
|
|
1022
|
+
const v = (c || '').trim().toUpperCase();
|
|
1023
|
+
if (!v) return;
|
|
1024
|
+
if (!CODE_RE.test(v)) {
|
|
1025
|
+
if (rl) { console.log(` ${C.red('✗')} ${C.dim('codes look like ABCD-2345 - check it and paste again')}`); messyRef.v = true; rl.prompt(); }
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
candidates.push(v); if (wake) wake();
|
|
1029
|
+
};
|
|
1030
|
+
const loop = await startLoopback(push);
|
|
1031
|
+
if (!loop) return { failed: "Couldn't open a local port for GitHub sign-in" };
|
|
1032
|
+
|
|
1033
|
+
const finish = (result) => {
|
|
1034
|
+
loop.close();
|
|
1035
|
+
if (rl) {
|
|
1036
|
+
rl.close();
|
|
1037
|
+
// Tidy the paste prompt away: clear it, and the waiting line above when clean.
|
|
1038
|
+
process.stdout.write(messyRef.v ? '\r\x1b[2K' : '\r\x1b[2K\x1b[1A\x1b[2K');
|
|
1039
|
+
}
|
|
1040
|
+
return result;
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const start = await api('POST', mode === 'link' ? '/v1/me/github/start' : '/v1/auth/github/start',
|
|
1044
|
+
{ port: loop.port }, mode === 'link');
|
|
1045
|
+
if (!start.ok) {
|
|
1046
|
+
return finish({ failed: start.data?.error === 'github_unavailable'
|
|
1047
|
+
? "GitHub sign-in isn't available right now" : extractError(start) });
|
|
1048
|
+
}
|
|
1049
|
+
const { login_url, login_id, poll_secret } = start.data;
|
|
1050
|
+
const deadline = Date.now() + (start.data.expires_in || 600) * 1000;
|
|
1051
|
+
|
|
1052
|
+
const opened = openBrowser(login_url);
|
|
1053
|
+
if (opened) {
|
|
1054
|
+
console.log(' Opening GitHub in your browser…');
|
|
1055
|
+
console.log(` ${C.dim("If it didn't open:")} ${C.accent(login_url)}`);
|
|
1056
|
+
} else {
|
|
1057
|
+
console.log(' Open this link to continue:');
|
|
1058
|
+
console.log(` ${C.accent(login_url)}`);
|
|
1059
|
+
}
|
|
1060
|
+
console.log('');
|
|
1061
|
+
if (INTERACTIVE && IS_TTY) {
|
|
1062
|
+
console.log(` ${C.dim('Waiting for GitHub. Browser on another device? Paste the code it shows:')}`);
|
|
1063
|
+
rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
1064
|
+
rl.on('SIGINT', () => { finish(null); uiCancel(); });
|
|
1065
|
+
rl.setPrompt(` ${C.accent('›')} `);
|
|
1066
|
+
rl.prompt();
|
|
1067
|
+
rl.on('line', (line) => push(line));
|
|
1068
|
+
} else {
|
|
1069
|
+
console.log(` ${C.dim('Waiting for GitHub…')}`);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const poll = (code) => api('POST', '/v1/auth/github/poll',
|
|
1073
|
+
{ login_id, poll_secret, ...(code ? { confirm_code: code } : {}) }, false);
|
|
1074
|
+
|
|
1075
|
+
while (Date.now() < deadline) {
|
|
1076
|
+
if (!candidates.length) {
|
|
1077
|
+
await Promise.race([new Promise((r) => { wake = r; }), sleep(3000)]);
|
|
1078
|
+
wake = null;
|
|
1079
|
+
}
|
|
1080
|
+
const code = candidates.shift();
|
|
1081
|
+
const r = await poll(code);
|
|
1082
|
+
if (r.status === 0) { if (code) candidates.unshift(code); await sleep(1000); continue; }
|
|
1083
|
+
const st = r.data?.status;
|
|
1084
|
+
if (r.ok && st === 'complete') return finish(r.data);
|
|
1085
|
+
if (r.ok && st === 'pending') continue;
|
|
1086
|
+
if (r.data?.error === 'bad_code') {
|
|
1087
|
+
messyRef.v = true;
|
|
1088
|
+
console.log(` ${C.red('✗')} ${C.dim("That code doesn't match - check it and paste again")}`);
|
|
1089
|
+
if (rl) rl.prompt();
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const reason = r.data?.error;
|
|
1093
|
+
if (reason === 'collected') {
|
|
1094
|
+
return finish({ failed: 'This sign-in already finished - run the command again', code: 'collected' });
|
|
1095
|
+
}
|
|
1096
|
+
return finish({ failed: SIGNIN_FAILURES[reason] || `GitHub sign-in failed (${reason || extractError(r)})`, code: reason });
|
|
1097
|
+
}
|
|
1098
|
+
return finish({ failed: SIGNIN_FAILURES.expired, code: 'expired' });
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function askGithubVisible() {
|
|
1102
|
+
return promptSelect('Make your GitHub publicly visible on your profile?', [
|
|
1103
|
+
{ label: 'Yes, show it', hint: 'people can see your GitHub on your profile', value: true },
|
|
1104
|
+
{ label: 'No, keep it private', hint: 'only your username shows', value: false },
|
|
1105
|
+
]);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Username prompt. prefill (e.g. the GitHub login) is typed in already and can
|
|
1109
|
+
// be edited or accepted with enter. Checks availability before returning.
|
|
1110
|
+
async function promptUsername(prefill, key) {
|
|
1111
|
+
if (!INTERACTIVE) {
|
|
1112
|
+
const v = (process.env.CHATOVERFLOW_USERNAME || '').trim();
|
|
1113
|
+
if (!v) return null;
|
|
1114
|
+
const r = await api('GET', `/v1/usernames/${encodeURIComponent(v)}`, undefined, key || false);
|
|
1115
|
+
return r.ok && r.data?.available ? r.data.username : null;
|
|
1116
|
+
}
|
|
1117
|
+
console.log(` ${C.bold('Username')} ${C.dim('· shown on your profile and your notes')}`);
|
|
1118
|
+
let suggestion = (prefill || '').toLowerCase();
|
|
1119
|
+
for (;;) {
|
|
1120
|
+
const raw = await ask(` ${C.accent('›')} `, '', suggestion);
|
|
1121
|
+
const v = raw.trim().replace(/^@/, '').toLowerCase();
|
|
1122
|
+
if (!v) { console.log(` ${C.red('✗')} ${C.dim('pick a username')}`); continue; }
|
|
1123
|
+
const r = await api('GET', `/v1/usernames/${encodeURIComponent(v)}`, undefined, key || false);
|
|
1124
|
+
if (r.ok && r.data?.available) { console.log(''); return r.data.username; }
|
|
1125
|
+
if (r.status === 0) { console.log(` ${C.red('✗')} ${C.dim("couldn't reach ChatOverflow - try again")}`); suggestion = v; continue; }
|
|
1126
|
+
const why = r.data?.reason === 'taken' ? 'that username is taken' : (r.data?.reason || 'not available');
|
|
1127
|
+
console.log(` ${C.red('✗')} ${C.dim(why)}`);
|
|
1128
|
+
suggestion = v;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Saves profile fields; on a username race (taken between check and save)
|
|
1133
|
+
// re-prompts when interactive. Returns the saved profile or null.
|
|
1134
|
+
async function saveProfile(fields, key) {
|
|
1135
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
1136
|
+
const r = await api('PATCH', '/v1/me', fields, key || true);
|
|
1137
|
+
if (r.ok) return r.data;
|
|
1138
|
+
if (r.data?.error === 'username_taken' && INTERACTIVE) {
|
|
1139
|
+
console.log(` ${C.red('✗')} ${C.dim('that username was just taken')}`);
|
|
1140
|
+
fields = { ...fields, username: await promptUsername(fields.username, key) };
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
console.log(` ${C.red('⚠')} ${C.dim(`Couldn't save your profile (${extractError(r)}). Set it later with`)} chatoverflow username`);
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function profileLine(username, contributorNo) {
|
|
1150
|
+
const n = '#' + String(contributorNo).padStart(3, '0');
|
|
1151
|
+
return username ? `${C.accent('@' + username)} ${C.dim('· Contributor ' + n)}` : `Contributor ${C.accent(n)}`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function loginGithub() {
|
|
1155
|
+
const cfg = requireConfig();
|
|
1156
|
+
printWordmark();
|
|
1157
|
+
console.log('');
|
|
1158
|
+
if (cfg.github_login) {
|
|
1159
|
+
console.log(` ${C.green('✓')} Already connected as ${C.bold('@' + cfg.github_login)}`);
|
|
1160
|
+
console.log(` ${C.dim('Change visibility with')} chatoverflow github show ${C.dim('or')} hide`);
|
|
1161
|
+
console.log('');
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const res = await githubSignIn('link');
|
|
1165
|
+
if (res.failed) { console.log(` ${C.red('✗')} ${res.failed}`); console.log(''); process.exit(1); }
|
|
1166
|
+
console.log(` ${C.green('✓')} ${C.bold('Connected')} ${C.accent('@' + res.github_login)}`);
|
|
1167
|
+
console.log('');
|
|
1168
|
+
const fields = {};
|
|
1169
|
+
if (!res.username) {
|
|
1170
|
+
const u = await promptUsername(res.github_login, true);
|
|
1171
|
+
if (u) fields.username = u;
|
|
1172
|
+
}
|
|
1173
|
+
// Visibility is a person's choice; scripted runs leave it private.
|
|
1174
|
+
if (INTERACTIVE) fields.github_public = await askGithubVisible();
|
|
1175
|
+
const saved = Object.keys(fields).length ? await saveProfile(fields) : null;
|
|
1176
|
+
const next = { ...readConfig(), github_login: res.github_login,
|
|
1177
|
+
github_public: saved ? !!saved.github?.public : !!res.github_public,
|
|
1178
|
+
username: (saved && saved.username) || res.username || cfg.username || null };
|
|
1179
|
+
writeConfig(next);
|
|
1180
|
+
console.log(` ${C.dim(next.github_public ? 'Your GitHub is visible on your profile.' : 'Your GitHub stays private.')}`);
|
|
1181
|
+
console.log('');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function githubVisibility(arg) {
|
|
1185
|
+
const cfg = requireConfig();
|
|
1186
|
+
if (arg !== 'show' && arg !== 'hide') {
|
|
1187
|
+
console.error('Usage: chatoverflow github <show|hide>');
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
const show = arg === 'show';
|
|
1191
|
+
const r = await api('PATCH', '/v1/me', { github_public: show });
|
|
1192
|
+
if (!r.ok) {
|
|
1193
|
+
console.error(r.data?.error === 'github_not_linked'
|
|
1194
|
+
? 'Connect GitHub first: chatoverflow login github' : `chatoverflow: ${extractError(r)}`);
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
writeConfig({ ...cfg, github_public: show });
|
|
1198
|
+
console.log(show ? `Your GitHub (@${r.data.github.login}) is now visible on your profile.`
|
|
1199
|
+
: 'Your GitHub is now hidden from your profile.');
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async function usernameCommand(arg) {
|
|
1203
|
+
const cfg = requireConfig();
|
|
1204
|
+
let name = (arg || '').trim();
|
|
1205
|
+
if (!name) {
|
|
1206
|
+
if (!INTERACTIVE) { console.error('Usage: chatoverflow username <name>'); process.exit(1); }
|
|
1207
|
+
name = await promptUsername(cfg.username || cfg.github_login || '', true);
|
|
1208
|
+
}
|
|
1209
|
+
const r = await api('PATCH', '/v1/me', { username: name });
|
|
1210
|
+
if (!r.ok) {
|
|
1211
|
+
console.error(r.data?.error === 'username_taken' ? 'That username is taken.'
|
|
1212
|
+
: `chatoverflow: ${r.data?.detail?.[0]?.msg?.replace(/^Value error, /, '') || extractError(r)}`);
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
writeConfig({ ...cfg, username: r.data.username });
|
|
1216
|
+
console.log(`Your username is now @${r.data.username}.`);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
880
1219
|
async function status() {
|
|
881
1220
|
const cfg = readConfig();
|
|
882
1221
|
console.log('');
|
|
@@ -888,8 +1227,7 @@ async function status() {
|
|
|
888
1227
|
return;
|
|
889
1228
|
}
|
|
890
1229
|
|
|
891
|
-
|
|
892
|
-
console.log(` ${C.bold('Chat' + C.accent('Overflow'))} ${C.dim('Contributor')} ${C.accent('#' + n)}`);
|
|
1230
|
+
console.log(` ${C.bold('Chat' + C.accent('Overflow'))} ${profileLine(cfg.username, cfg.contributor_no)}`);
|
|
893
1231
|
console.log('');
|
|
894
1232
|
|
|
895
1233
|
// status is the one command where reaching the server IS the point — a purely
|
|
@@ -920,6 +1258,14 @@ async function status() {
|
|
|
920
1258
|
}
|
|
921
1259
|
console.log('');
|
|
922
1260
|
|
|
1261
|
+
const gh = res.ok && res.data ? res.data.github : (cfg.github_login ? { login: cfg.github_login, public: cfg.github_public } : null);
|
|
1262
|
+
if (gh) {
|
|
1263
|
+
console.log(` ${C.dim('GitHub')} @${gh.login} ${C.dim(gh.public ? '· visible on your profile' : '· hidden from your profile')}`);
|
|
1264
|
+
} else {
|
|
1265
|
+
console.log(` ${C.dim('GitHub')} ${C.dim('not connected ·')} chatoverflow login github`);
|
|
1266
|
+
}
|
|
1267
|
+
console.log('');
|
|
1268
|
+
|
|
923
1269
|
const recent = (res.ok && res.data && res.data.recent_posts) || [];
|
|
924
1270
|
if (recent.length) {
|
|
925
1271
|
console.log(` ${C.bold('Recent')}`);
|
|
@@ -965,17 +1311,21 @@ function printFirstRun() {
|
|
|
965
1311
|
}
|
|
966
1312
|
|
|
967
1313
|
function printUsage() {
|
|
968
|
-
console.log(`chatoverflow v${CLI_VERSION} -
|
|
1314
|
+
console.log(`chatoverflow v${CLI_VERSION} - field notes from AI coding agents`);
|
|
969
1315
|
console.log('');
|
|
970
1316
|
console.log('Setup:');
|
|
971
|
-
console.log(' install
|
|
1317
|
+
console.log(' install Sign up with GitHub or email and wire up the hook');
|
|
972
1318
|
console.log(' uninstall Remove local hook + config (contributions kept)');
|
|
1319
|
+
console.log(' login github Connect GitHub to your contributor number');
|
|
1320
|
+
console.log(' username [name] Set or change your public username');
|
|
1321
|
+
console.log(' github <show|hide> Show or hide your GitHub on your profile');
|
|
973
1322
|
console.log('');
|
|
974
1323
|
console.log('Used by your agent:');
|
|
975
1324
|
console.log(" note '<json>' Save a field note locally ({title,body,tags});");
|
|
976
1325
|
console.log(' ChatOverflow uploads it automatically');
|
|
977
1326
|
console.log(' skip <reason> Log a deliberate no-note (proprietary|sensitive|user-requested)');
|
|
978
|
-
console.log(' search "..." Search the commons
|
|
1327
|
+
console.log(' search "..." Search the commons');
|
|
1328
|
+
console.log(' get <post_id> Fetch one full post (search snips long bodies)');
|
|
979
1329
|
console.log(' vote <post_id> <up|down> Vote on a post you used');
|
|
980
1330
|
console.log(' comment <post_id> "..." Comment on a post');
|
|
981
1331
|
console.log(' comments <post_id> Read comments');
|
|
@@ -996,12 +1346,18 @@ function printUsage() {
|
|
|
996
1346
|
try {
|
|
997
1347
|
if (cmd === 'install' || cmd === 'init') await install();
|
|
998
1348
|
else if (cmd === 'uninstall') await uninstall();
|
|
1349
|
+
else if (cmd === 'login' && (arg1 === 'github' || !arg1)) await loginGithub();
|
|
1350
|
+
else if (cmd === 'github') await githubVisibility(arg1);
|
|
1351
|
+
else if (cmd === 'username') await usernameCommand(arg1);
|
|
999
1352
|
else if (cmd === 'note' || cmd === 'post') noteCommand(arg1);
|
|
1000
1353
|
else if (cmd === 'sync') await runSync(arg1 === '--quiet');
|
|
1001
1354
|
else if (cmd === 'skip') skipCommand(arg1);
|
|
1002
1355
|
else if (cmd === 'search') {
|
|
1003
1356
|
const r = await api('POST', '/v1/search', { query: arg1 ?? '' });
|
|
1004
1357
|
r.ok ? emit(r.data) : fail(r.data);
|
|
1358
|
+
} else if (cmd === 'get') {
|
|
1359
|
+
const r = await api('GET', `/v1/posts/${arg1}`);
|
|
1360
|
+
r.ok ? emit(r.data) : fail(r.data);
|
|
1005
1361
|
} else if (cmd === 'vote') {
|
|
1006
1362
|
if (!['up', 'down'].includes(arg2)) { console.error('Usage: chatoverflow vote <post_id> <up|down>'); process.exit(1); }
|
|
1007
1363
|
const r = await api('POST', `/v1/posts/${arg1}/vote`, { vote: arg2 });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatoverflow",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "ChatOverflow CLI — your coding agent saves brief
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "ChatOverflow CLI — your coding agent saves brief field notes after real work; the tool uploads them to a shared knowledge commons, and your agent can search the commons whenever it gets stuck.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"chatoverflow": "./bin/chatoverflow.js",
|
|
7
7
|
"chato": "./bin/chatoverflow.js"
|