symbiot 0.1.1 → 0.2.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 +16 -4
- package/index.mjs +153 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ bring yourself.
|
|
|
11
11
|
symbiot your last 7 days, written up (same as: symbiot week)
|
|
12
12
|
symbiot standup yesterday + today, for standup
|
|
13
13
|
symbiot todo what's still on your plate
|
|
14
|
+
symbiot login connect it to Claude (once)
|
|
15
|
+
symbiot whoami show how it's connected
|
|
14
16
|
symbiot help
|
|
15
17
|
```
|
|
16
18
|
|
|
@@ -20,12 +22,19 @@ symbiot help
|
|
|
20
22
|
npm install -g symbiot # or run without installing: npx symbiot week
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
## Connect it (once)
|
|
26
|
+
|
|
27
|
+
Symbiot writes with **Claude**, so it needs an Anthropic API key. The easiest way:
|
|
24
28
|
|
|
25
29
|
```bash
|
|
26
|
-
|
|
30
|
+
symbiot login # paste a key once — saved locally to ~/.config/symbiot
|
|
27
31
|
```
|
|
28
32
|
|
|
33
|
+
Get a key at <https://console.anthropic.com/settings/keys>. Prefer an env var? Set
|
|
34
|
+
`ANTHROPIC_API_KEY=sk-ant-...` instead and skip `login`. If you already use the
|
|
35
|
+
Anthropic CLI, `ant auth login` works too — Symbiot picks up any of these
|
|
36
|
+
automatically, and `symbiot whoami` shows which one it's using.
|
|
37
|
+
|
|
29
38
|
## Use
|
|
30
39
|
|
|
31
40
|
```bash
|
|
@@ -52,8 +61,11 @@ Point it somewhere specific with `--dir ~/work`.
|
|
|
52
61
|
|
|
53
62
|
## Config
|
|
54
63
|
|
|
55
|
-
- `SYMBIOT_MODEL` — the model to use (default `claude-opus-5`
|
|
56
|
-
|
|
64
|
+
- `SYMBIOT_MODEL` — the model to use (default `claude-opus-5-5`; `claude-haiku-4-5`
|
|
65
|
+
is a cheaper option for this kind of summary).
|
|
66
|
+
- `ANTHROPIC_API_KEY` — your key. Or run `symbiot login` once, or use an
|
|
67
|
+
`ant auth login` profile. `symbiot whoami` shows which credential is active;
|
|
68
|
+
`symbiot logout` forgets a saved key.
|
|
57
69
|
|
|
58
70
|
---
|
|
59
71
|
|
package/index.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// symbiot — your week, written from your real work.
|
|
3
3
|
//
|
|
4
4
|
// Reads your local git activity (no accounts, no OAuth, no integrations) and
|
|
5
|
-
// writes the update you'd actually send.
|
|
6
|
-
//
|
|
5
|
+
// writes the update you'd actually send. It writes with Claude, so it needs an
|
|
6
|
+
// Anthropic API key — set one up once with `symbiot login`.
|
|
7
7
|
//
|
|
8
8
|
// symbiot week your week, written up (default)
|
|
9
9
|
// symbiot standup yesterday + today, for standup
|
|
10
10
|
// symbiot todo what's still on your plate
|
|
11
|
+
// symbiot login connect it to Claude (once)
|
|
12
|
+
// symbiot whoami show how it's connected
|
|
11
13
|
// symbiot help
|
|
12
14
|
//
|
|
13
15
|
// Flags: --dir <path> where to look (default: your home folder)
|
|
@@ -17,10 +19,15 @@
|
|
|
17
19
|
import Anthropic from "@anthropic-ai/sdk";
|
|
18
20
|
import { execSync } from "node:child_process";
|
|
19
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
24
|
+
import { createInterface } from "node:readline";
|
|
20
25
|
|
|
21
|
-
const MODEL = process.env.SYMBIOT_MODEL || "claude-opus-5";
|
|
26
|
+
const MODEL = process.env.SYMBIOT_MODEL || "claude-opus-5-5";
|
|
22
27
|
const MAX_REPOS = 14;
|
|
23
28
|
const MAX_COMMITS = 140;
|
|
29
|
+
const CONFIG_DIR = join(homedir(), ".config", "symbiot");
|
|
30
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
24
31
|
|
|
25
32
|
// ---- tiny arg parse --------------------------------------------------------
|
|
26
33
|
const argv = process.argv.slice(2);
|
|
@@ -49,6 +56,60 @@ function spinner(label) {
|
|
|
49
56
|
return () => { clearInterval(t); process.stderr.write("\r\x1b[K"); };
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
// ---- auth ------------------------------------------------------------------
|
|
60
|
+
// Symbiot writes with Claude. Credentials can come from (first wins):
|
|
61
|
+
// 1. ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN in the environment
|
|
62
|
+
// 2. a key saved by `symbiot login` (~/.config/symbiot/config.json)
|
|
63
|
+
// 3. an Anthropic CLI profile on disk (`ant auth login`), which the SDK reads
|
|
64
|
+
function loadConfig() {
|
|
65
|
+
try { return JSON.parse(readFileSync(CONFIG_PATH, "utf8")); } catch { return {}; }
|
|
66
|
+
}
|
|
67
|
+
function saveConfig(cfg) {
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
70
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
|
|
71
|
+
try { chmodSync(CONFIG_PATH, 0o600); } catch {}
|
|
72
|
+
return true;
|
|
73
|
+
} catch { return false; }
|
|
74
|
+
}
|
|
75
|
+
function antProfileExists() {
|
|
76
|
+
try { return existsSync(join(homedir(), ".config", "anthropic")); } catch { return false; }
|
|
77
|
+
}
|
|
78
|
+
// Returns { opts, source } for building the client, or null if nothing is set up.
|
|
79
|
+
function resolveAuth() {
|
|
80
|
+
if (process.env.ANTHROPIC_API_KEY) return { opts: {}, source: "ANTHROPIC_API_KEY (environment)" };
|
|
81
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) return { opts: {}, source: "ANTHROPIC_AUTH_TOKEN (environment)" };
|
|
82
|
+
const cfg = loadConfig();
|
|
83
|
+
if (cfg.apiKey) return { opts: { apiKey: cfg.apiKey }, source: "your saved login (~/.config/symbiot)" };
|
|
84
|
+
if (antProfileExists()) return { opts: {}, source: "your Anthropic CLI login (ant auth login)" };
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function getClient() {
|
|
88
|
+
const auth = resolveAuth();
|
|
89
|
+
if (!auth) return null;
|
|
90
|
+
try { return { client: new Anthropic(auth.opts), source: auth.source }; }
|
|
91
|
+
catch { return null; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- prompt (with masked secret input) ------------------------------------
|
|
95
|
+
function ask(question, { secret = false } = {}) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
98
|
+
if (secret && !PLAIN) {
|
|
99
|
+
rl._writeToOutput = (str) => {
|
|
100
|
+
if (str.includes(question)) rl.output.write(question);
|
|
101
|
+
else if (str.includes("\n")) rl.output.write("\n");
|
|
102
|
+
else rl.output.write("*");
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
rl.question(question, (ans) => {
|
|
106
|
+
rl.close();
|
|
107
|
+
if (secret) process.stdout.write("\n");
|
|
108
|
+
resolve((ans || "").trim());
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
52
113
|
// ---- git ------------------------------------------------------------------
|
|
53
114
|
function sh(cmd) {
|
|
54
115
|
try { return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024 }); }
|
|
@@ -120,29 +181,32 @@ function openWork(repos) {
|
|
|
120
181
|
|
|
121
182
|
// ---- claude ---------------------------------------------------------------
|
|
122
183
|
async function write(system, prompt) {
|
|
123
|
-
|
|
124
|
-
|
|
184
|
+
const conn = getClient();
|
|
185
|
+
if (!conn) { console.log(AUTH_HELP); return null; }
|
|
125
186
|
const stop = spinner("thinking…");
|
|
126
187
|
try {
|
|
127
|
-
const res = await client.messages.create({
|
|
188
|
+
const res = await conn.client.messages.create({
|
|
128
189
|
model: MODEL, max_tokens: 1600,
|
|
190
|
+
output_config: { effort: "low" }, // a summary doesn't need deep reasoning — keeps it fast + cheap
|
|
129
191
|
system, messages: [{ role: "user", content: prompt }],
|
|
130
192
|
});
|
|
131
193
|
if (res.stop_reason === "refusal") return "(the model declined this one — odd for a work summary; try again)";
|
|
132
194
|
return res.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
133
195
|
} catch (err) {
|
|
134
|
-
if (err instanceof Anthropic.AuthenticationError || /api key|ANTHROPIC_API_KEY|credential/i.test(err?.message || "")) {
|
|
135
|
-
|
|
196
|
+
if (err instanceof Anthropic.AuthenticationError || /api key|ANTHROPIC_API_KEY|authentication|credential|401/i.test(err?.message || "")) {
|
|
197
|
+
console.log(c.y("Your Claude key was rejected. ") + "Re-connect with: " + c.b("symbiot login --force"));
|
|
198
|
+
return null;
|
|
136
199
|
}
|
|
137
200
|
return `Couldn't reach the model: ${err?.message || err}`;
|
|
138
201
|
} finally { stop(); }
|
|
139
202
|
}
|
|
140
203
|
|
|
141
|
-
const AUTH_HELP =
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
|
|
204
|
+
const AUTH_HELP =
|
|
205
|
+
c.y("Symbiot writes your updates with Claude, so it needs an Anthropic API key.\n") +
|
|
206
|
+
" Fastest: " + c.b("symbiot login") + c.d(" paste a key once; saved to ~/.config/symbiot") + "\n" +
|
|
207
|
+
" Or set: export ANTHROPIC_API_KEY=sk-ant-...\n" +
|
|
208
|
+
" Get a key: https://console.anthropic.com/settings/keys\n" +
|
|
209
|
+
c.d(" (If you use the Anthropic CLI, `ant auth login` works too.)");
|
|
146
210
|
|
|
147
211
|
// ---- render ---------------------------------------------------------------
|
|
148
212
|
function renderCommits(list) {
|
|
@@ -158,6 +222,7 @@ const SINCE_WEEK = Number(flag("since", "7"));
|
|
|
158
222
|
const BASE = flag("dir", homedir());
|
|
159
223
|
|
|
160
224
|
async function cmdWeek(days, label) {
|
|
225
|
+
if (!resolveAuth()) { console.log(AUTH_HELP); return; }
|
|
161
226
|
const who = me();
|
|
162
227
|
const repos = findRepos(BASE, days);
|
|
163
228
|
if (!repos.length) {
|
|
@@ -186,11 +251,12 @@ async function cmdWeek(days, label) {
|
|
|
186
251
|
`Write the ${label === "standup" ? "standup" : "update"}.`;
|
|
187
252
|
|
|
188
253
|
header("Symbiot", `${cs.length} commits across ${new Set(cs.map((x) => x.repo)).size} repos · ${label}`);
|
|
189
|
-
|
|
190
|
-
console.log("");
|
|
254
|
+
const out = await write(system, prompt);
|
|
255
|
+
if (out) console.log(out + "\n");
|
|
191
256
|
}
|
|
192
257
|
|
|
193
258
|
async function cmdTodo() {
|
|
259
|
+
if (!resolveAuth()) { console.log(AUTH_HELP); return; }
|
|
194
260
|
const repos = findRepos(BASE, 60);
|
|
195
261
|
const open = openWork(repos);
|
|
196
262
|
if (!open.length) { console.log(c.y("Nothing outstanding found (no TODOs or uncommitted work).")); return; }
|
|
@@ -199,8 +265,67 @@ async function cmdTodo() {
|
|
|
199
265
|
`Group by project, lead with what looks most in-flight (uncommitted work) then the to-dos. ` +
|
|
200
266
|
`Be concise and concrete. No preamble.`;
|
|
201
267
|
header("Symbiot", `${open.length} open items · todo`);
|
|
202
|
-
|
|
203
|
-
console.log("");
|
|
268
|
+
const out = await write(system, `Open work:\n${open.map((o) => `- ${o}`).join("\n")}\n\nWhat's still on my plate?`);
|
|
269
|
+
if (out) console.log(out + "\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function cmdLogin() {
|
|
273
|
+
const existing = resolveAuth();
|
|
274
|
+
const provided = flag("key", null);
|
|
275
|
+
if (existing && !provided && !has("force")) {
|
|
276
|
+
console.log(c.g("✓ ") + `Already connected — Symbiot is using ${existing.source}.`);
|
|
277
|
+
console.log(c.d(" Replace it with `symbiot login --force`, or just run `symbiot week`."));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log("\n" + c.b("Connect Symbiot to Claude") + "\n");
|
|
281
|
+
console.log("Symbiot writes your updates with Claude (Anthropic). It needs an API key,");
|
|
282
|
+
console.log("stored locally on this machine and used only to write your updates.\n");
|
|
283
|
+
console.log(c.d(" Get a key at: https://console.anthropic.com/settings/keys") + "\n");
|
|
284
|
+
|
|
285
|
+
let key = provided;
|
|
286
|
+
if (!key) key = await ask("Paste your Anthropic API key (sk-ant-…): ", { secret: true });
|
|
287
|
+
key = (key || "").trim();
|
|
288
|
+
if (!key) { console.log(c.y("No key entered — nothing saved.")); return; }
|
|
289
|
+
if (!/^sk-ant-/.test(key)) console.log(c.d("(that doesn't look like an sk-ant- key, but I'll try it)"));
|
|
290
|
+
|
|
291
|
+
const stop = spinner("checking the key…");
|
|
292
|
+
let ok = false, why = "";
|
|
293
|
+
try { await new Anthropic({ apiKey: key }).models.list(); ok = true; }
|
|
294
|
+
catch (e) {
|
|
295
|
+
if (e instanceof Anthropic.AuthenticationError || /401|invalid|authentication/i.test(e?.message || ""))
|
|
296
|
+
why = "Anthropic rejected it — the key looks invalid.";
|
|
297
|
+
else why = `couldn't reach Anthropic — ${e?.message || e}`;
|
|
298
|
+
}
|
|
299
|
+
finally { stop(); }
|
|
300
|
+
|
|
301
|
+
if (!ok) {
|
|
302
|
+
console.log(c.y("That key didn't work: ") + why);
|
|
303
|
+
console.log(c.d("Double-check it and run `symbiot login` again."));
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (saveConfig({ ...loadConfig(), apiKey: key })) {
|
|
308
|
+
console.log(c.g("✓ ") + "Connected. You're set — try: " + c.b("symbiot week"));
|
|
309
|
+
console.log(c.d(` Key saved in ${CONFIG_PATH} (readable only by you).`));
|
|
310
|
+
} else {
|
|
311
|
+
console.log(c.y("Couldn't write the config file. Set it in your shell instead:"));
|
|
312
|
+
console.log(" export ANTHROPIC_API_KEY=" + key);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function cmdLogout() {
|
|
317
|
+
const cfg = loadConfig();
|
|
318
|
+
if (!cfg.apiKey) { console.log("No saved key to remove."); return; }
|
|
319
|
+
delete cfg.apiKey;
|
|
320
|
+
saveConfig(cfg);
|
|
321
|
+
console.log(c.g("✓ ") + `Removed the saved key from ${CONFIG_PATH}.`);
|
|
322
|
+
if (process.env.ANTHROPIC_API_KEY) console.log(c.d("Note: ANTHROPIC_API_KEY is still set in your environment."));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function cmdWhoami() {
|
|
326
|
+
const a = resolveAuth();
|
|
327
|
+
if (a) console.log(c.g("✓ ") + `Symbiot is connected — using ${a.source}. Model: ${MODEL}`);
|
|
328
|
+
else { console.log(c.y("Not connected yet.\n")); console.log(AUTH_HELP); }
|
|
204
329
|
}
|
|
205
330
|
|
|
206
331
|
const HELP = `${c.b("symbiot")} — your week, written from your real work.
|
|
@@ -209,15 +334,21 @@ ${c.b("Usage")}
|
|
|
209
334
|
symbiot ${c.d("(or)")} symbiot week write up your last ${SINCE_WEEK} days
|
|
210
335
|
symbiot standup yesterday + today, for standup
|
|
211
336
|
symbiot todo what's still on your plate
|
|
337
|
+
symbiot login connect it to Claude (once)
|
|
338
|
+
symbiot whoami show how it's connected
|
|
339
|
+
symbiot logout forget the saved key
|
|
212
340
|
symbiot help
|
|
213
341
|
|
|
214
342
|
${c.b("Options")}
|
|
215
343
|
--dir <path> where your repos are (default: ${homedir()})
|
|
216
344
|
--since <days> window for 'week' (default 7)
|
|
345
|
+
--all everyone's commits, not just yours
|
|
217
346
|
--plain no colour/spinner (good for piping)
|
|
218
347
|
|
|
219
|
-
${c.b("Setup")}
|
|
220
|
-
|
|
348
|
+
${c.b("Setup")} it writes with Claude, so it needs one Anthropic key:
|
|
349
|
+
symbiot login ${c.d("paste a key once (recommended)")}
|
|
350
|
+
export ANTHROPIC_API_KEY=sk-ant-... ${c.d("or set it yourself")}
|
|
351
|
+
${c.d("Change the model with SYMBIOT_MODEL (default " + MODEL + "; e.g. claude-haiku-4-5 is cheaper).")}
|
|
221
352
|
|
|
222
353
|
${c.d("Reads only your local git. No accounts, no OAuth, no data leaves except the")}
|
|
223
354
|
${c.d("commit summaries sent to the model to write your update.")}`;
|
|
@@ -225,6 +356,9 @@ ${c.d("commit summaries sent to the model to write your update.")}`;
|
|
|
225
356
|
// ---- main -----------------------------------------------------------------
|
|
226
357
|
(async () => {
|
|
227
358
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") { console.log(HELP); return; }
|
|
359
|
+
if (cmd === "login" || cmd === "auth") return cmdLogin();
|
|
360
|
+
if (cmd === "logout") return cmdLogout();
|
|
361
|
+
if (cmd === "whoami" || cmd === "status") return cmdWhoami();
|
|
228
362
|
if (cmd === "week") return cmdWeek(SINCE_WEEK, "week");
|
|
229
363
|
if (cmd === "standup") return cmdWeek(2, "standup");
|
|
230
364
|
if (cmd === "todo") return cmdTodo();
|