@workscribe/cli 1.10.0 → 1.11.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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -26,6 +26,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/index.ts
27
27
  var import_commander = require("commander");
28
28
 
29
+ // src/utils/node-version.ts
30
+ function assertNodeVersion(nodeVersion) {
31
+ const major = Number(nodeVersion.split(".")[0]);
32
+ if (major < 22) {
33
+ process.stderr.write(
34
+ `workscribe requires Node.js 22 or later (you have v${nodeVersion}).
35
+ Upgrade at https://nodejs.org or via your version manager:
36
+ nvm install 22 && nvm use 22
37
+ `
38
+ );
39
+ process.exit(1);
40
+ }
41
+ }
42
+
29
43
  // src/capture/index.ts
30
44
  var import_fs9 = __toESM(require("fs"));
31
45
 
@@ -307,7 +321,10 @@ var BUILTIN_PATTERNS = [
307
321
  // Values after long-form secret flags (single-char flags like -t/-p/-k are too ambiguous)
308
322
  /(?<=--(password|passwd|secret|token|key|api[-_]?key|auth[-_]?token)\s+)\S+/gi,
309
323
  // export VAR=VALUE where VAR looks sensitive
310
- /(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi
324
+ /(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
325
+ // Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
326
+ // Lookbehind matches only the variable-name= prefix, so only the value is replaced
327
+ /(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi
311
328
  ];
312
329
  function redact(command, extraPatterns = []) {
313
330
  let result = command;
@@ -1059,8 +1076,11 @@ function captureCommand(opts) {
1059
1076
  }
1060
1077
 
1061
1078
  // src/cli/capture.ts
1079
+ var CAPTURE_HANG_TIMEOUT_MS = 5e3;
1062
1080
  function registerCapture(program2) {
1063
1081
  program2.command("_capture").description("Internal: capture a shell command event (called by shell hook)").option("--cmd <command>", "command that was executed").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).option("--git-before <sha>", "git HEAD SHA before this command ran").action((opts) => {
1082
+ const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
1083
+ hangGuard.unref();
1064
1084
  try {
1065
1085
  if (!opts.cmd || !opts.cwd) return;
1066
1086
  captureCommand({
@@ -1119,6 +1139,404 @@ var import_os4 = __toESM(require("os"));
1119
1139
  var import_prompts = __toESM(require("prompts"));
1120
1140
  var import_chalk2 = __toESM(require("chalk"));
1121
1141
 
1142
+ // src/utils/format.ts
1143
+ function formatDuration(minutes) {
1144
+ if (minutes < 60) return `${Math.round(minutes)}m`;
1145
+ const h = Math.floor(minutes / 60);
1146
+ const m = Math.round(minutes % 60);
1147
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
1148
+ }
1149
+ function formatDate(isoString) {
1150
+ return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1151
+ }
1152
+ function todayDate() {
1153
+ return localDateStr(/* @__PURE__ */ new Date());
1154
+ }
1155
+ function parseDate(input) {
1156
+ const d = /* @__PURE__ */ new Date(input + "T00:00:00");
1157
+ if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
1158
+ return input;
1159
+ }
1160
+ function localDateStr(d) {
1161
+ const y = d.getFullYear();
1162
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1163
+ const day = String(d.getDate()).padStart(2, "0");
1164
+ return `${y}-${m}-${day}`;
1165
+ }
1166
+ function startOfWeek(dateStr) {
1167
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
1168
+ const day = d.getDay();
1169
+ const diff = day === 0 ? -6 : 1 - day;
1170
+ d.setDate(d.getDate() + diff);
1171
+ return localDateStr(d);
1172
+ }
1173
+ function eachDayOfRange(start, end) {
1174
+ const days = [];
1175
+ const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
1176
+ const last = /* @__PURE__ */ new Date(end + "T00:00:00");
1177
+ while (cur <= last) {
1178
+ days.push(localDateStr(cur));
1179
+ cur.setDate(cur.getDate() + 1);
1180
+ }
1181
+ return days;
1182
+ }
1183
+ function isoWeekKey(dateStr) {
1184
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
1185
+ const jan4 = new Date(d.getFullYear(), 0, 4);
1186
+ const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
1187
+ return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
1188
+ }
1189
+
1190
+ // src/ai/prompt.ts
1191
+ var SYSTEM_PROMPT = `You write short developer work summaries. Focus on what the developer accomplished and what changed \u2014 not which tools or commands were used.
1192
+
1193
+ Here is an example:
1194
+
1195
+ INPUT:
1196
+ Project: api-service
1197
+ Branch: feature/auth
1198
+ Duration: 2h 00m
1199
+ Events:
1200
+ - versioning {"branch":"feature/auth"}
1201
+ - 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
1202
+ - 8x testing [failed]
1203
+ - testing
1204
+ - 2x files {"tool":"mkdir"}
1205
+ - shipping {"message":"fix: auth token validation"}
1206
+ - shipping {"remote":"origin","branch":"feature/auth"}
1207
+
1208
+ OUTPUT:
1209
+ - Added authentication dependencies (bcrypt, JWT)
1210
+ - Debugged and resolved failing auth tests
1211
+ - Committed and pushed the token validation fix
1212
+
1213
+ RULES \u2014 follow these exactly:
1214
+
1215
+ 1. Interpret repeated events as patterns, not individual steps.
1216
+ - Many testing [failed] followed by a passing test = debugging a failure. Say "resolved failing X tests", not "ran tests multiple times".
1217
+ - Many versioning events = branch housekeeping. Fold into context, do not list each git command.
1218
+
1219
+ 2. Omit utility commands entirely. File size checks, file reads, grep searches, and cat commands are research steps \u2014 they are not outcomes and must not appear in the summary.
1220
+
1221
+ 3. Do not name the AI tool. For ai_session events: focus on what was built or committed. If a commits array is present in metadata, use those commit messages. Never write "via Claude Code", "using Claude Code", or similar \u2014 omit the tool name entirely.
1222
+
1223
+ 4. For other events with a hint field: describe the developer's intent, not the raw command.
1224
+
1225
+ 5. For dependencies events: infer the purpose from package names and describe the domain (bcrypt/jsonwebtoken \u2192 authentication, prisma/pg \u2192 database, jest/vitest \u2192 testing). For unfamiliar packages, name them directly.
1226
+
1227
+ 6. Fold supporting events into the narrative. Building, shipping, versioning, and file operations are context \u2014 only make them a bullet when they are the primary activity (e.g. a standalone build fix or deploy with no accompanying commits).
1228
+
1229
+ 7. Stick to what the events show. Do not infer subject matter from the branch name. Do not add details not derivable from the events.
1230
+
1231
+ Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1232
+ function getSystemPrompt() {
1233
+ const contexts = getActiveProfileAiContexts();
1234
+ if (contexts.length === 0) return SYSTEM_PROMPT;
1235
+ return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
1236
+ }
1237
+ var DEFAULT_EXCLUDED_CATEGORIES = /* @__PURE__ */ new Set(["exploring", "note", "linting"]);
1238
+ function buildExcludedSet(includeCategories = []) {
1239
+ const excluded = new Set(DEFAULT_EXCLUDED_CATEGORIES);
1240
+ for (const cat of includeCategories) excluded.delete(cat);
1241
+ return excluded;
1242
+ }
1243
+ function countExcludedEvents(sessions, excluded) {
1244
+ const counts = /* @__PURE__ */ new Map();
1245
+ for (const session of sessions) {
1246
+ for (const event of session.events) {
1247
+ if (excluded.has(event.category)) {
1248
+ counts.set(event.category, (counts.get(event.category) ?? 0) + 1);
1249
+ }
1250
+ }
1251
+ }
1252
+ return counts;
1253
+ }
1254
+ function buildUserMessage(payload, includeCategories = []) {
1255
+ const excluded = buildExcludedSet(includeCategories);
1256
+ const lines = [`Date: ${payload.date}`, ""];
1257
+ for (const session of payload.sessions) {
1258
+ lines.push(`Project: ${session.project ?? "unknown"}`);
1259
+ if (session.branch) lines.push(`Branch: ${session.branch}`);
1260
+ lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
1261
+ for (const event of session.events) {
1262
+ if (event.category === "note") {
1263
+ lines.push(`NOTE: ${event.metadata.text}`);
1264
+ }
1265
+ }
1266
+ lines.push("Events:");
1267
+ const counts = /* @__PURE__ */ new Map();
1268
+ for (const event of session.events) {
1269
+ if (excluded.has(event.category)) continue;
1270
+ const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1271
+ let key;
1272
+ if (event.hint) {
1273
+ key = `other [${event.hint}]${failed}`;
1274
+ } else {
1275
+ const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
1276
+ key = `${event.category}${meta}${failed}`;
1277
+ }
1278
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1279
+ }
1280
+ for (const [key, count] of counts) {
1281
+ lines.push(` - ${count > 1 ? `${count}x ` : ""}${key}`);
1282
+ }
1283
+ lines.push("");
1284
+ }
1285
+ return lines.join("\n");
1286
+ }
1287
+ var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
1288
+
1289
+ INPUT:
1290
+ Week: 2026-05-18 to 2026-05-22
1291
+
1292
+ Monday 2026-05-18:
1293
+ Project: api-service (feature/auth) \u2014 2h 10m
1294
+ Events: 3x code_commit, 2x test_run, dependency_install
1295
+
1296
+ Tuesday 2026-05-19:
1297
+ Project: api-service (feature/auth) \u2014 1h 30m
1298
+ Events: 2x code_commit, test_run, build
1299
+
1300
+ Wednesday 2026-05-20:
1301
+ Project: dashboard (main) \u2014 3h 00m
1302
+ Events: 4x code_commit, 2x build, test_run
1303
+
1304
+ Thursday 2026-05-21:
1305
+ Project: api-service (feature/auth) \u2014 1h 00m
1306
+ Events: code_commit {"message":"feat: merge auth"}, branch_switch
1307
+
1308
+ Friday 2026-05-22:
1309
+ Project: dashboard (main) \u2014 2h 00m
1310
+ Events: 3x code_commit, build
1311
+
1312
+ OUTPUT:
1313
+ ## Week of 18 May \u2014 9h 40m across 2 projects
1314
+
1315
+ **api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
1316
+ **dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
1317
+
1318
+ Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
1319
+ var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1320
+ function buildWeeklyMessage(payload) {
1321
+ const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
1322
+ for (const { date, sessions } of payload.days) {
1323
+ const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
1324
+ lines.push(`${dayName} ${date}:`);
1325
+ for (const session of sessions) {
1326
+ const project = session.project ?? "unknown";
1327
+ const branch = session.branch ? ` (${session.branch})` : "";
1328
+ lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(session.durationMinutes)}`);
1329
+ for (const event of session.events) {
1330
+ if (event.category === "note") {
1331
+ lines.push(` NOTE: ${event.metadata.text}`);
1332
+ }
1333
+ }
1334
+ const excluded = buildExcludedSet();
1335
+ const counts = /* @__PURE__ */ new Map();
1336
+ for (const event of session.events) {
1337
+ if (excluded.has(event.category)) continue;
1338
+ const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1339
+ let key;
1340
+ if (event.hint) {
1341
+ key = `other [${event.hint}]${failed}`;
1342
+ } else {
1343
+ const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
1344
+ key = `${event.category}${meta}${failed}`;
1345
+ }
1346
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1347
+ }
1348
+ const eventLine = [...counts.entries()].map(([key, count]) => `${count > 1 ? `${count}x ` : ""}${key}`).join(", ");
1349
+ lines.push(` Events: ${eventLine}`);
1350
+ }
1351
+ lines.push("");
1352
+ }
1353
+ return lines.join("\n");
1354
+ }
1355
+
1356
+ // src/ai/fetch.ts
1357
+ var DEFAULT_TIMEOUT_MS = 3e4;
1358
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
1359
+ const controller = new AbortController();
1360
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1361
+ try {
1362
+ return await fetch(url, { ...init, signal: controller.signal });
1363
+ } catch (err) {
1364
+ if (err.name === "AbortError") {
1365
+ throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
1366
+ }
1367
+ throw err;
1368
+ } finally {
1369
+ clearTimeout(timer);
1370
+ }
1371
+ }
1372
+
1373
+ // src/ai/providers/index.ts
1374
+ var GenericProvider = class {
1375
+ constructor(config, model, apiKey, baseUrl, includeCategories = []) {
1376
+ this.config = config;
1377
+ this.model = model;
1378
+ this.apiKey = apiKey;
1379
+ this.baseUrl = baseUrl;
1380
+ this.includeCategories = includeCategories;
1381
+ }
1382
+ config;
1383
+ model;
1384
+ apiKey;
1385
+ baseUrl;
1386
+ includeCategories;
1387
+ async call(system, userMsg, maxTokens) {
1388
+ const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
1389
+ method: "POST",
1390
+ headers: this.config.headers(this.apiKey),
1391
+ body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
1392
+ });
1393
+ if (!response.ok) {
1394
+ throw new Error(this.config.chatError(response.status, await response.text()));
1395
+ }
1396
+ return this.config.parseText(await response.json());
1397
+ }
1398
+ async summarize(payload) {
1399
+ return this.call(getSystemPrompt(), buildUserMessage(payload, this.includeCategories), this.config.dailyTokens);
1400
+ }
1401
+ async summarizeWeek(payload) {
1402
+ return this.call(WEEKLY_SYSTEM_PROMPT, buildWeeklyMessage(payload), this.config.weeklyTokens);
1403
+ }
1404
+ async validate() {
1405
+ const isPost = this.config.validateMethod === "POST";
1406
+ const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
1407
+ method: this.config.validateMethod,
1408
+ headers: isPost ? this.config.headers(this.apiKey) : void 0,
1409
+ body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
1410
+ });
1411
+ if (!response.ok) {
1412
+ throw new Error(this.config.validateError(response.status, await response.text(), this.baseUrl));
1413
+ }
1414
+ }
1415
+ };
1416
+
1417
+ // src/ai/providers/config.ts
1418
+ var PROVIDER_CONFIGS = {
1419
+ anthropic: {
1420
+ defaultModel: "claude-haiku-4-5-20251001",
1421
+ defaultBaseUrl: "https://api.anthropic.com/v1",
1422
+ dailyTokens: 1024,
1423
+ weeklyTokens: 1024,
1424
+ chatUrl: (baseUrl) => `${baseUrl}/messages`,
1425
+ validateUrl: (baseUrl) => `${baseUrl}/messages`,
1426
+ validateMethod: "POST",
1427
+ headers: (apiKey) => ({
1428
+ "Content-Type": "application/json",
1429
+ "x-api-key": apiKey,
1430
+ "anthropic-version": "2023-06-01"
1431
+ }),
1432
+ buildChatBody: (model, system, userMsg, maxTokens) => ({
1433
+ model,
1434
+ max_tokens: maxTokens,
1435
+ system,
1436
+ messages: [{ role: "user", content: userMsg }]
1437
+ }),
1438
+ buildValidateBody: (model) => ({
1439
+ model,
1440
+ max_tokens: 1,
1441
+ messages: [{ role: "user", content: "." }]
1442
+ }),
1443
+ parseText: (json) => {
1444
+ const data = json;
1445
+ return data.content.find((b) => b.type === "text")?.text ?? "";
1446
+ },
1447
+ chatError: (status, body) => `Anthropic API error ${status}: ${body}`,
1448
+ validateError: (status, body) => `Anthropic API error ${status}: ${body}`
1449
+ },
1450
+ openai: {
1451
+ defaultModel: "gpt-4o-mini",
1452
+ defaultBaseUrl: "https://api.openai.com/v1",
1453
+ dailyTokens: 1024,
1454
+ weeklyTokens: 1024,
1455
+ chatUrl: (baseUrl) => `${baseUrl}/chat/completions`,
1456
+ validateUrl: (baseUrl) => `${baseUrl}/chat/completions`,
1457
+ validateMethod: "POST",
1458
+ headers: (apiKey) => ({
1459
+ "Content-Type": "application/json",
1460
+ Authorization: `Bearer ${apiKey}`
1461
+ }),
1462
+ buildChatBody: (model, system, userMsg, maxTokens) => ({
1463
+ model,
1464
+ messages: [
1465
+ { role: "system", content: system },
1466
+ { role: "user", content: userMsg }
1467
+ ],
1468
+ max_tokens: maxTokens
1469
+ }),
1470
+ buildValidateBody: (model) => ({
1471
+ model,
1472
+ messages: [{ role: "user", content: "." }],
1473
+ max_tokens: 1
1474
+ }),
1475
+ parseText: (json) => {
1476
+ const data = json;
1477
+ return data.choices[0]?.message?.content ?? "";
1478
+ },
1479
+ chatError: (status, body) => `OpenAI API error ${status}: ${body}`,
1480
+ validateError: (status, body) => `OpenAI API error ${status}: ${body}`
1481
+ },
1482
+ ollama: {
1483
+ defaultModel: "llama3",
1484
+ defaultBaseUrl: "http://localhost:11434",
1485
+ dailyTokens: 300,
1486
+ weeklyTokens: 400,
1487
+ chatUrl: (baseUrl) => `${baseUrl}/api/chat`,
1488
+ validateUrl: (baseUrl) => `${baseUrl}/api/tags`,
1489
+ validateMethod: "GET",
1490
+ headers: () => ({ "Content-Type": "application/json" }),
1491
+ buildChatBody: (model, system, userMsg, maxTokens) => ({
1492
+ model,
1493
+ stream: false,
1494
+ messages: [
1495
+ { role: "system", content: system },
1496
+ { role: "user", content: userMsg }
1497
+ ],
1498
+ options: { num_predict: maxTokens, temperature: 0.3 }
1499
+ }),
1500
+ parseText: (json) => {
1501
+ return json.message?.content ?? "";
1502
+ },
1503
+ chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
1504
+ validateError: (_status, _body, baseUrl) => `Ollama is not reachable at ${baseUrl}. Is it running? Try: ollama serve`
1505
+ }
1506
+ };
1507
+
1508
+ // src/ai/router.ts
1509
+ function getProvider(config) {
1510
+ const { provider, apiKey = "", model, baseUrl } = config.ai;
1511
+ if (!provider) {
1512
+ throw new Error(
1513
+ "No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
1514
+ );
1515
+ }
1516
+ const configKey = provider === "openai-compatible" ? "openai" : provider;
1517
+ const providerConfig = PROVIDER_CONFIGS[configKey];
1518
+ if (!providerConfig) {
1519
+ throw new Error(
1520
+ `Unknown provider: "${provider}"
1521
+ Supported providers: anthropic, openai, ollama, openai-compatible`
1522
+ );
1523
+ }
1524
+ if (provider !== "ollama" && !apiKey) {
1525
+ throw new Error(`${provider} requires an API key.
1526
+ Run: workscribe config set ai.apiKey <your-key>`);
1527
+ }
1528
+ if (provider === "openai-compatible" && !baseUrl) {
1529
+ throw new Error("openai-compatible requires a base URL.\nRun: workscribe config set ai.baseUrl <url>");
1530
+ }
1531
+ return new GenericProvider(
1532
+ providerConfig,
1533
+ model || providerConfig.defaultModel,
1534
+ apiKey,
1535
+ baseUrl || providerConfig.defaultBaseUrl,
1536
+ config.summary?.includeCategories ?? []
1537
+ );
1538
+ }
1539
+
1122
1540
  // src/capture/shell-hook.ts
1123
1541
  var import_fs10 = __toESM(require("fs"));
1124
1542
  var import_path7 = __toESM(require("path"));
@@ -1245,9 +1663,15 @@ function detectShell() {
1245
1663
  if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os4.default.homedir(), ".bashrc") };
1246
1664
  return null;
1247
1665
  }
1666
+ function step(current, total, label) {
1667
+ log.plain(import_chalk2.default.bold(`
1668
+ [${current}/${total}] ${label}`));
1669
+ }
1248
1670
  function registerInit(program2) {
1249
1671
  program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
1672
+ const TOTAL_STEPS = 5;
1250
1673
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
1674
+ step(1, TOTAL_STEPS, "Creating data directory");
1251
1675
  if (!import_fs12.default.existsSync(WORKSCRIBE_DIR)) {
1252
1676
  import_fs12.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
1253
1677
  log.success(`Created ${WORKSCRIBE_DIR}`);
@@ -1256,12 +1680,15 @@ function registerInit(program2) {
1256
1680
  import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
1257
1681
  } catch {
1258
1682
  }
1683
+ log.info(`${WORKSCRIBE_DIR} already exists`);
1259
1684
  }
1260
1685
  if (!import_fs12.default.existsSync(PROFILES_DIR)) {
1261
1686
  import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1262
1687
  }
1688
+ step(2, TOTAL_STEPS, "Initialising database");
1263
1689
  getDb();
1264
- log.success("Database initialized");
1690
+ log.success("Database ready");
1691
+ step(3, TOTAL_STEPS, "Installing shell hook");
1265
1692
  const shell = detectShell();
1266
1693
  if (!shell) {
1267
1694
  log.warn("Could not detect shell. Supported: zsh, bash");
@@ -1274,6 +1701,12 @@ function registerInit(program2) {
1274
1701
  const hookContent = import_fs12.default.readFileSync(hookFile, "utf-8");
1275
1702
  if (!import_fs12.default.existsSync(shell.rcFile)) import_fs12.default.writeFileSync(shell.rcFile, "");
1276
1703
  if (!isHookInstalled(shell.rcFile)) {
1704
+ log.plain("");
1705
+ log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
1706
+ log.plain(import_chalk2.default.dim(" command you run \u2014 the text, exit code, duration, and working directory."));
1707
+ log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
1708
+ log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
1709
+ log.plain("");
1277
1710
  installHook(shell.rcFile, hookContent);
1278
1711
  log.success(`Shell hook installed in ${shell.rcFile}`);
1279
1712
  } else if (isHookOutdated(shell.rcFile, hookContent)) {
@@ -1294,6 +1727,7 @@ function registerInit(program2) {
1294
1727
  }
1295
1728
  }
1296
1729
  }
1730
+ step(4, TOTAL_STEPS, "Detecting AI coding tools");
1297
1731
  const installedTools = loadAIToolRegistry().filter(
1298
1732
  (entry) => entry.hook !== null && AI_HOOK_INTEGRATIONS[entry.hook]?.isInstalled()
1299
1733
  );
@@ -1348,6 +1782,8 @@ function registerInit(program2) {
1348
1782
  } catch {
1349
1783
  log.info("Could not reach profile registry \u2014 run workscribe profile detect after connecting");
1350
1784
  }
1785
+ step(5, TOTAL_STEPS, "Configuring AI provider");
1786
+ let providerConfigured = !!config.ai.provider;
1351
1787
  if (config.ai.provider) {
1352
1788
  log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
1353
1789
  } else {
@@ -1364,7 +1800,10 @@ function registerInit(program2) {
1364
1800
  { title: "Skip \u2014 configure later", value: "skip" }
1365
1801
  ]
1366
1802
  });
1367
- if (provider && provider !== "skip") {
1803
+ if (!provider || provider === "skip") {
1804
+ providerConfigured = false;
1805
+ log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
1806
+ } else {
1368
1807
  config.ai.provider = provider;
1369
1808
  if (provider === "ollama") {
1370
1809
  log.plain(`
@@ -1389,26 +1828,59 @@ function registerInit(program2) {
1389
1828
  if (baseUrl) config.ai.baseUrl = baseUrl;
1390
1829
  }
1391
1830
  }
1392
- writeConfig(config);
1393
- log.success(`AI provider set to ${import_chalk2.default.cyan(provider)}`);
1394
- } else if (provider === "skip" || !provider) {
1395
- log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
1831
+ log.plain("");
1832
+ log.info(`Validating ${import_chalk2.default.cyan(provider)} connection\u2026`);
1833
+ try {
1834
+ const testProvider = getProvider(config);
1835
+ await testProvider.validate();
1836
+ log.success("API key verified");
1837
+ writeConfig(config);
1838
+ providerConfigured = true;
1839
+ } catch (error) {
1840
+ log.warn(`Validation failed: ${error.message}`);
1841
+ const { retry } = await (0, import_prompts.default)({
1842
+ type: "confirm",
1843
+ name: "retry",
1844
+ message: "Save anyway and configure later?",
1845
+ initial: false
1846
+ });
1847
+ if (retry) {
1848
+ writeConfig(config);
1849
+ providerConfigured = false;
1850
+ log.info("Config saved \u2014 run workscribe config set ai.apiKey <key> to update");
1851
+ } else {
1852
+ log.info("API key not saved \u2014 run workscribe init again to configure");
1853
+ providerConfigured = false;
1854
+ }
1855
+ }
1396
1856
  }
1397
1857
  }
1398
1858
  if (!import_fs12.default.existsSync(CONFIG_PATH)) {
1399
1859
  writeConfig(config);
1400
- log.success("Config file created");
1401
1860
  }
1402
1861
  log.plain("");
1403
1862
  log.plain(import_chalk2.default.bold(" Setup complete!"));
1863
+ log.plain("");
1404
1864
  if (shell) {
1405
- log.plain("");
1406
- log.plain(` Activate capture now: ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
1865
+ log.plain(` Activate capture now:`);
1866
+ log.plain(` ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
1407
1867
  log.plain(` Or restart your terminal.`);
1868
+ log.plain("");
1869
+ }
1870
+ if (providerConfigured) {
1871
+ log.plain(` Then run a few commands and generate your first summary:`);
1872
+ log.plain(` ${import_chalk2.default.cyan("workscribe summary")}`);
1873
+ log.plain("");
1874
+ log.plain(` Check capture is working:`);
1875
+ log.plain(` ${import_chalk2.default.cyan("workscribe status")}`);
1876
+ } else {
1877
+ log.plain(` Before generating summaries, configure an AI provider:`);
1878
+ log.plain(` ${import_chalk2.default.cyan("workscribe config set ai.provider anthropic")}`);
1879
+ log.plain(` ${import_chalk2.default.cyan("workscribe config set ai.apiKey <your-key>")}`);
1880
+ log.plain("");
1881
+ log.plain(` Then run:`);
1882
+ log.plain(` ${import_chalk2.default.cyan("workscribe summary")}`);
1408
1883
  }
1409
- log.plain("");
1410
- log.plain(` ${import_chalk2.default.dim("workscribe summary")} \u2014 generate today's work summary`);
1411
- log.plain(` ${import_chalk2.default.dim("workscribe status")} \u2014 check capture is working`);
1412
1884
  log.plain("");
1413
1885
  });
1414
1886
  }
@@ -1416,56 +1888,6 @@ function registerInit(program2) {
1416
1888
  // src/cli/status.ts
1417
1889
  var import_fs13 = __toESM(require("fs"));
1418
1890
  var import_chalk3 = __toESM(require("chalk"));
1419
-
1420
- // src/utils/format.ts
1421
- function formatDuration(minutes) {
1422
- if (minutes < 60) return `${Math.round(minutes)}m`;
1423
- const h = Math.floor(minutes / 60);
1424
- const m = Math.round(minutes % 60);
1425
- return m > 0 ? `${h}h ${m}m` : `${h}h`;
1426
- }
1427
- function formatDate(isoString) {
1428
- return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1429
- }
1430
- function todayDate() {
1431
- return localDateStr(/* @__PURE__ */ new Date());
1432
- }
1433
- function parseDate(input) {
1434
- const d = /* @__PURE__ */ new Date(input + "T00:00:00");
1435
- if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
1436
- return input;
1437
- }
1438
- function localDateStr(d) {
1439
- const y = d.getFullYear();
1440
- const m = String(d.getMonth() + 1).padStart(2, "0");
1441
- const day = String(d.getDate()).padStart(2, "0");
1442
- return `${y}-${m}-${day}`;
1443
- }
1444
- function startOfWeek(dateStr) {
1445
- const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
1446
- const day = d.getDay();
1447
- const diff = day === 0 ? -6 : 1 - day;
1448
- d.setDate(d.getDate() + diff);
1449
- return localDateStr(d);
1450
- }
1451
- function eachDayOfRange(start, end) {
1452
- const days = [];
1453
- const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
1454
- const last = /* @__PURE__ */ new Date(end + "T00:00:00");
1455
- while (cur <= last) {
1456
- days.push(localDateStr(cur));
1457
- cur.setDate(cur.getDate() + 1);
1458
- }
1459
- return days;
1460
- }
1461
- function isoWeekKey(dateStr) {
1462
- const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
1463
- const jan4 = new Date(d.getFullYear(), 0, 4);
1464
- const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
1465
- return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
1466
- }
1467
-
1468
- // src/cli/status.ts
1469
1891
  function registerStatus(program2) {
1470
1892
  program2.command("status").description("Show capture status and event counts").action(() => {
1471
1893
  const config = readConfig();
@@ -1694,8 +2116,8 @@ function registerSessions(program2) {
1694
2116
  if (opts.date) {
1695
2117
  try {
1696
2118
  date = parseDate(opts.date);
1697
- } catch (e) {
1698
- log.error(e.message);
2119
+ } catch (error) {
2120
+ log.error(error.message);
1699
2121
  process.exit(1);
1700
2122
  }
1701
2123
  }
@@ -1715,6 +2137,16 @@ function registerSessions(program2) {
1715
2137
  log.plain(
1716
2138
  ` ${import_chalk5.default.dim(start)} \u2013 ${import_chalk5.default.dim(end)} ${repo} ${import_chalk5.default.dim(duration)} ${import_chalk5.default.dim(session.eventCount + " events")}`
1717
2139
  );
2140
+ for (const event of session.events) {
2141
+ if (event.category === "note") {
2142
+ let text = "";
2143
+ try {
2144
+ text = JSON.parse(event.metadata ?? "{}").text ?? "";
2145
+ } catch {
2146
+ }
2147
+ if (text) log.plain(` ${import_chalk5.default.yellow("note")} ${import_chalk5.default.dim(text)}`);
2148
+ }
2149
+ }
1718
2150
  }
1719
2151
  log.plain("");
1720
2152
  });
@@ -1726,356 +2158,6 @@ var import_fs14 = __toESM(require("fs"));
1726
2158
  var import_os5 = __toESM(require("os"));
1727
2159
  var import_path10 = __toESM(require("path"));
1728
2160
 
1729
- // src/ai/prompt.ts
1730
- var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
1731
-
1732
- INPUT:
1733
- Project: api-service
1734
- Branch: feature/auth
1735
- Duration: 2h 00m
1736
- Events:
1737
- - versioning {"branch":"feature/auth"}
1738
- - 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
1739
- - 2x testing {"tool":"jest"} [failed]
1740
- - testing {"tool":"jest"}
1741
- - 2x files {"tool":"mkdir"}
1742
- - shipping {"message":"fix: auth token validation"}
1743
- - shipping {"remote":"origin","branch":"feature/auth"}
1744
-
1745
- OUTPUT:
1746
- - Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
1747
- - Resolved failing auth tests
1748
- - Committed and pushed authentication fixes
1749
-
1750
- For dependencies events with a packages field: infer the purpose of packages from their names and describe the domain, not just the packages. Examples: bcrypt/jsonwebtoken \u2192 authentication, prisma/mongoose/pg \u2192 database, jest/vitest/mocha \u2192 testing, axios/got/node-fetch \u2192 HTTP client, stripe/paypal \u2192 payments, socket.io/ws \u2192 real-time. Use your knowledge of common packages. For unfamiliar packages, name them directly without guessing their domain.
1751
-
1752
- Events like shipping (pushes), building, versioning (branch switches, stash), files (file ops, env vars), and infrastructure provide context \u2014 fold them into the surrounding narrative rather than listing them as separate bullets. Only surface them directly when they are the main activity (e.g. a standalone build fix, a deploy, or a significant restructure with no accompanying commits).
1753
-
1754
- For ai_session events: describe what was built during the AI session using the tool name (e.g. "via Claude Code"). If a commits array is present in metadata, use those commit messages to describe what was built. Group all activity from the session into one narrative bullet.
1755
-
1756
- For other events with a hint field: interpret the command snippet and describe the activity in plain language. Do not quote the raw command.
1757
-
1758
- Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1759
- function getSystemPrompt() {
1760
- const contexts = getActiveProfileAiContexts();
1761
- if (contexts.length === 0) return SYSTEM_PROMPT;
1762
- return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
1763
- }
1764
- var EXCLUDED = /* @__PURE__ */ new Set(["exploring"]);
1765
- function buildUserMessage(payload) {
1766
- const lines = [`Date: ${payload.date}`, ""];
1767
- for (const session of payload.sessions) {
1768
- lines.push(`Project: ${session.project ?? "unknown"}`);
1769
- if (session.branch) lines.push(`Branch: ${session.branch}`);
1770
- lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
1771
- lines.push("Events:");
1772
- const counts = /* @__PURE__ */ new Map();
1773
- for (const event of session.events) {
1774
- if (EXCLUDED.has(event.category)) continue;
1775
- const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1776
- let key;
1777
- if (event.hint) {
1778
- key = `other [${event.hint}]${failed}`;
1779
- } else {
1780
- const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
1781
- key = `${event.category}${meta}${failed}`;
1782
- }
1783
- counts.set(key, (counts.get(key) ?? 0) + 1);
1784
- }
1785
- for (const [key, count] of counts) {
1786
- lines.push(` - ${count > 1 ? `${count}x ` : ""}${key}`);
1787
- }
1788
- lines.push("");
1789
- }
1790
- return lines.join("\n");
1791
- }
1792
- var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
1793
-
1794
- INPUT:
1795
- Week: 2026-05-18 to 2026-05-22
1796
-
1797
- Monday 2026-05-18:
1798
- Project: api-service (feature/auth) \u2014 2h 10m
1799
- Events: 3x code_commit, 2x test_run, dependency_install
1800
-
1801
- Tuesday 2026-05-19:
1802
- Project: api-service (feature/auth) \u2014 1h 30m
1803
- Events: 2x code_commit, test_run, build
1804
-
1805
- Wednesday 2026-05-20:
1806
- Project: dashboard (main) \u2014 3h 00m
1807
- Events: 4x code_commit, 2x build, test_run
1808
-
1809
- Thursday 2026-05-21:
1810
- Project: api-service (feature/auth) \u2014 1h 00m
1811
- Events: code_commit {"message":"feat: merge auth"}, branch_switch
1812
-
1813
- Friday 2026-05-22:
1814
- Project: dashboard (main) \u2014 2h 00m
1815
- Events: 3x code_commit, build
1816
-
1817
- OUTPUT:
1818
- ## Week of 18 May \u2014 9h 40m across 2 projects
1819
-
1820
- **api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
1821
- **dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
1822
-
1823
- Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
1824
- var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1825
- function buildWeeklyMessage(payload) {
1826
- const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
1827
- for (const { date, sessions } of payload.days) {
1828
- const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
1829
- lines.push(`${dayName} ${date}:`);
1830
- for (const s of sessions) {
1831
- const project = s.project ?? "unknown";
1832
- const branch = s.branch ? ` (${s.branch})` : "";
1833
- lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
1834
- const counts = /* @__PURE__ */ new Map();
1835
- for (const e of s.events) {
1836
- if (EXCLUDED.has(e.category)) continue;
1837
- const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
1838
- let key;
1839
- if (e.hint) {
1840
- key = `other [${e.hint}]${failed}`;
1841
- } else {
1842
- const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
1843
- key = `${e.category}${meta}${failed}`;
1844
- }
1845
- counts.set(key, (counts.get(key) ?? 0) + 1);
1846
- }
1847
- const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
1848
- lines.push(` Events: ${eventLine}`);
1849
- }
1850
- lines.push("");
1851
- }
1852
- return lines.join("\n");
1853
- }
1854
-
1855
- // src/ai/fetch.ts
1856
- var DEFAULT_TIMEOUT_MS = 3e4;
1857
- async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
1858
- const controller = new AbortController();
1859
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1860
- try {
1861
- return await fetch(url, { ...init, signal: controller.signal });
1862
- } catch (err) {
1863
- if (err.name === "AbortError") {
1864
- throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
1865
- }
1866
- throw err;
1867
- } finally {
1868
- clearTimeout(timer);
1869
- }
1870
- }
1871
-
1872
- // src/ai/providers/anthropic.ts
1873
- var AnthropicProvider = class {
1874
- apiKey;
1875
- model;
1876
- constructor(apiKey, model) {
1877
- this.apiKey = apiKey;
1878
- this.model = model || "claude-haiku-4-5-20251001";
1879
- }
1880
- async summarize(payload) {
1881
- const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
1882
- method: "POST",
1883
- headers: {
1884
- "Content-Type": "application/json",
1885
- "x-api-key": this.apiKey,
1886
- "anthropic-version": "2023-06-01"
1887
- },
1888
- body: JSON.stringify({
1889
- model: this.model,
1890
- max_tokens: 1024,
1891
- system: getSystemPrompt(),
1892
- messages: [{ role: "user", content: buildUserMessage(payload) }]
1893
- })
1894
- });
1895
- if (!response.ok) {
1896
- const body = await response.text();
1897
- throw new Error(`Anthropic API error ${response.status}: ${body}`);
1898
- }
1899
- const data = await response.json();
1900
- return data.content.find((block) => block.type === "text")?.text ?? "";
1901
- }
1902
- async summarizeWeek(payload) {
1903
- const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
1904
- method: "POST",
1905
- headers: {
1906
- "Content-Type": "application/json",
1907
- "x-api-key": this.apiKey,
1908
- "anthropic-version": "2023-06-01"
1909
- },
1910
- body: JSON.stringify({
1911
- model: this.model,
1912
- max_tokens: 1024,
1913
- system: WEEKLY_SYSTEM_PROMPT,
1914
- messages: [{ role: "user", content: buildWeeklyMessage(payload) }]
1915
- })
1916
- });
1917
- if (!response.ok) {
1918
- const body = await response.text();
1919
- throw new Error(`Anthropic API error ${response.status}: ${body}`);
1920
- }
1921
- const data = await response.json();
1922
- return data.content.find((block) => block.type === "text")?.text ?? "";
1923
- }
1924
- };
1925
-
1926
- // src/ai/providers/openai.ts
1927
- var OpenAIProvider = class {
1928
- apiKey;
1929
- model;
1930
- baseUrl;
1931
- constructor(apiKey, model, baseUrl) {
1932
- this.apiKey = apiKey;
1933
- this.model = model || "gpt-4o-mini";
1934
- this.baseUrl = baseUrl || "https://api.openai.com/v1";
1935
- }
1936
- async summarize(payload) {
1937
- const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
1938
- method: "POST",
1939
- headers: {
1940
- "Content-Type": "application/json",
1941
- Authorization: `Bearer ${this.apiKey}`
1942
- },
1943
- body: JSON.stringify({
1944
- model: this.model,
1945
- messages: [
1946
- { role: "system", content: getSystemPrompt() },
1947
- { role: "user", content: buildUserMessage(payload) }
1948
- ],
1949
- max_tokens: 1024
1950
- })
1951
- });
1952
- if (!response.ok) {
1953
- const body = await response.text();
1954
- throw new Error(`OpenAI API error ${response.status}: ${body}`);
1955
- }
1956
- const data = await response.json();
1957
- return data.choices[0]?.message?.content ?? "";
1958
- }
1959
- async summarizeWeek(payload) {
1960
- const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
1961
- method: "POST",
1962
- headers: {
1963
- "Content-Type": "application/json",
1964
- Authorization: `Bearer ${this.apiKey}`
1965
- },
1966
- body: JSON.stringify({
1967
- model: this.model,
1968
- messages: [
1969
- { role: "system", content: WEEKLY_SYSTEM_PROMPT },
1970
- { role: "user", content: buildWeeklyMessage(payload) }
1971
- ],
1972
- max_tokens: 1024
1973
- })
1974
- });
1975
- if (!response.ok) {
1976
- const body = await response.text();
1977
- throw new Error(`OpenAI API error ${response.status}: ${body}`);
1978
- }
1979
- const data = await response.json();
1980
- return data.choices[0]?.message?.content ?? "";
1981
- }
1982
- };
1983
-
1984
- // src/ai/providers/ollama.ts
1985
- var OllamaProvider = class {
1986
- model;
1987
- baseUrl;
1988
- constructor(model, baseUrl) {
1989
- this.model = model || "llama3";
1990
- this.baseUrl = baseUrl || "http://localhost:11434";
1991
- }
1992
- async summarize(payload) {
1993
- const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
1994
- method: "POST",
1995
- headers: { "Content-Type": "application/json" },
1996
- body: JSON.stringify({
1997
- model: this.model,
1998
- stream: false,
1999
- messages: [
2000
- { role: "system", content: getSystemPrompt() },
2001
- { role: "user", content: buildUserMessage(payload) }
2002
- ],
2003
- options: {
2004
- num_predict: 300,
2005
- // hard cap on output tokens — forces conciseness
2006
- temperature: 0.3
2007
- // lower temperature = more focused, less wandering
2008
- }
2009
- })
2010
- });
2011
- if (!response.ok) {
2012
- const body = await response.text();
2013
- throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
2014
- }
2015
- const data = await response.json();
2016
- return data.message?.content ?? "";
2017
- }
2018
- async summarizeWeek(payload) {
2019
- const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
2020
- method: "POST",
2021
- headers: { "Content-Type": "application/json" },
2022
- body: JSON.stringify({
2023
- model: this.model,
2024
- stream: false,
2025
- messages: [
2026
- { role: "system", content: WEEKLY_SYSTEM_PROMPT },
2027
- { role: "user", content: buildWeeklyMessage(payload) }
2028
- ],
2029
- options: {
2030
- num_predict: 400,
2031
- temperature: 0.3
2032
- }
2033
- })
2034
- });
2035
- if (!response.ok) {
2036
- const body = await response.text();
2037
- throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
2038
- }
2039
- const data = await response.json();
2040
- return data.message?.content ?? "";
2041
- }
2042
- };
2043
-
2044
- // src/ai/router.ts
2045
- function getProvider(config) {
2046
- const { provider, apiKey, model, baseUrl } = config.ai;
2047
- if (!provider) {
2048
- throw new Error(
2049
- "No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
2050
- );
2051
- }
2052
- switch (provider) {
2053
- case "anthropic": {
2054
- if (!apiKey) throw new Error("Anthropic requires an API key.\nRun: workscribe config set ai.apiKey <your-key>");
2055
- return new AnthropicProvider(apiKey, model || void 0);
2056
- }
2057
- case "openai": {
2058
- if (!apiKey) throw new Error("OpenAI requires an API key.\nRun: workscribe config set ai.apiKey <your-key>");
2059
- return new OpenAIProvider(apiKey, model || void 0);
2060
- }
2061
- case "openai-compatible": {
2062
- if (!apiKey)
2063
- throw new Error("This provider requires an API key.\nRun: workscribe config set ai.apiKey <your-key>");
2064
- if (!baseUrl)
2065
- throw new Error("openai-compatible requires a base URL.\nRun: workscribe config set ai.baseUrl <url>");
2066
- return new OpenAIProvider(apiKey, model || void 0, baseUrl);
2067
- }
2068
- case "ollama": {
2069
- return new OllamaProvider(model || void 0, baseUrl || void 0);
2070
- }
2071
- default:
2072
- throw new Error(
2073
- `Unknown provider: "${provider}"
2074
- Supported providers: anthropic, openai, ollama, openai-compatible`
2075
- );
2076
- }
2077
- }
2078
-
2079
2161
  // src/integrations/webhook.ts
2080
2162
  async function postToWebhook(url, text) {
2081
2163
  const response = await fetchWithTimeout(
@@ -2121,7 +2203,7 @@ function eventRowToPayload(row) {
2121
2203
  };
2122
2204
  if (row.category === "other" && typeof metadata.hint === "string") {
2123
2205
  payload.hint = metadata.hint;
2124
- const { hint: _h, tool: _t, ...rest } = metadata;
2206
+ const { hint: _hint, tool: _tool, ...rest } = metadata;
2125
2207
  payload.metadata = rest;
2126
2208
  }
2127
2209
  return payload;
@@ -2146,21 +2228,21 @@ async function handleWebhook(opts, config, summary) {
2146
2228
  const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
2147
2229
  await postToWebhook(webhookUrl, body);
2148
2230
  log.success("Posted.");
2149
- } catch (e) {
2150
- log.error(`Webhook failed: ${e.message}`);
2231
+ } catch (error) {
2232
+ log.error(`Webhook failed: ${error.message}`);
2151
2233
  }
2152
2234
  } else if (opts.slack && !config.integrations?.slackWebhook) {
2153
2235
  log.warn("--slack requires integrations.slackWebhook to be set in config");
2154
2236
  }
2155
2237
  }
2156
2238
  function registerSummary(program2) {
2157
- program2.command("summary").description("Generate an AI work summary").option("--date <YYYY-MM-DD>", "date to summarize (default: today)").option("--week", "summarize the current week").option("--fresh", "bypass cache and regenerate").option("--slack", "post summary to Slack webhook from config").option("--webhook <url>", "post summary to a custom webhook URL").action(async (opts) => {
2239
+ program2.command("summary").description("Generate an AI work summary").option("--date <YYYY-MM-DD>", "date to summarize (default: today)").option("--week", "summarize the current week").option("--fresh", "bypass cache and regenerate").option("--verbose", "show all captured events including those not sent to AI").option("--slack", "post summary to Slack webhook from config").option("--webhook <url>", "post summary to a custom webhook URL").action(async (opts) => {
2158
2240
  const config = readConfig();
2159
2241
  let provider;
2160
2242
  try {
2161
2243
  provider = getProvider(config);
2162
- } catch (e) {
2163
- log.error(e.message);
2244
+ } catch (error) {
2245
+ log.error(error.message);
2164
2246
  process.exit(1);
2165
2247
  }
2166
2248
  if (opts.week) {
@@ -2200,8 +2282,8 @@ function registerSummary(program2) {
2200
2282
  import_fs14.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2201
2283
  import_fs14.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2202
2284
  await handleWebhook(opts, config, summary);
2203
- } catch (e) {
2204
- log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
2285
+ } catch (error) {
2286
+ log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
2205
2287
  process.exit(1);
2206
2288
  }
2207
2289
  return;
@@ -2210,8 +2292,8 @@ function registerSummary(program2) {
2210
2292
  if (opts.date) {
2211
2293
  try {
2212
2294
  date = parseDate(opts.date);
2213
- } catch (e) {
2214
- log.error(e.message);
2295
+ } catch (error) {
2296
+ log.error(error.message);
2215
2297
  process.exit(1);
2216
2298
  }
2217
2299
  }
@@ -2243,12 +2325,35 @@ function registerSummary(program2) {
2243
2325
  return;
2244
2326
  }
2245
2327
  }
2328
+ const excluded = buildExcludedSet(config.summary?.includeCategories ?? []);
2329
+ const allSessionPayloads = sessions.map((session) => sessionToPayload(session, date));
2330
+ if (opts.verbose) {
2331
+ log.plain("");
2332
+ log.plain(import_chalk6.default.bold(" Captured events (--verbose):"));
2333
+ for (const sessionPayload of allSessionPayloads) {
2334
+ const branch = sessionPayload.branch ? ` (${sessionPayload.branch})` : "";
2335
+ log.plain(
2336
+ import_chalk6.default.dim(
2337
+ `
2338
+ ${sessionPayload.project ?? "unknown"}${branch} \u2014 ${formatDuration(sessionPayload.durationMinutes)}`
2339
+ )
2340
+ );
2341
+ for (const event of sessionPayload.events) {
2342
+ const tag = excluded.has(event.category) ? import_chalk6.default.dim(` [excluded] ${event.category}`) : ` ${event.category}`;
2343
+ const hint = event.hint ? import_chalk6.default.dim(` [${event.hint}]`) : "";
2344
+ const meta = Object.keys(event.metadata).length > 0 ? import_chalk6.default.dim(` ${JSON.stringify(event.metadata)}`) : "";
2345
+ log.plain(` ${tag}${hint}${meta}`);
2346
+ }
2347
+ }
2348
+ log.plain("");
2349
+ }
2246
2350
  log.plain("");
2247
2351
  log.info(`Generating summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
2248
2352
  const allSummaries = [];
2249
2353
  const jsonSessions = [];
2250
- for (const session of sessions) {
2251
- const sessionPayload = { date, sessions: [sessionToPayload(session, date)] };
2354
+ for (let index = 0; index < sessions.length; index++) {
2355
+ const session = sessions[index];
2356
+ const sessionPayload = { date, sessions: [allSessionPayloads[index]] };
2252
2357
  try {
2253
2358
  const bullets = await provider.summarize(sessionPayload);
2254
2359
  const project = session.repoName ?? "unknown";
@@ -2267,8 +2372,8 @@ ${bullets.trim()}`;
2267
2372
  bullets: parseBullets(bullets)
2268
2373
  });
2269
2374
  }
2270
- } catch (e) {
2271
- log.error(`Summary failed [${config.ai.provider}]: ${e.message}`);
2375
+ } catch (error) {
2376
+ log.error(`Summary failed [${config.ai.provider}]: ${error.message}`);
2272
2377
  process.exit(1);
2273
2378
  }
2274
2379
  }
@@ -2280,6 +2385,14 @@ ${bullets.trim()}`;
2280
2385
  log.plain(fullSummary);
2281
2386
  }
2282
2387
  log.plain("");
2388
+ const excludedCounts = countExcludedEvents(allSessionPayloads, excluded);
2389
+ if (excludedCounts.size > 0) {
2390
+ const total = [...excludedCounts.values()].reduce((sum, count) => sum + count, 0);
2391
+ const breakdown = [...excludedCounts.entries()].map(([category, count]) => `${category} \xD7${count}`).join(", ");
2392
+ log.plain(import_chalk6.default.dim(` \u2139 ${total} event${total === 1 ? "" : "s"} not included in summary (${breakdown})`));
2393
+ log.plain(import_chalk6.default.dim(" Use --verbose to inspect \xB7 workscribe config set summary.includeCategories to opt in"));
2394
+ log.plain("");
2395
+ }
2283
2396
  await handleWebhook(opts, config, fullSummary);
2284
2397
  });
2285
2398
  }
@@ -2740,9 +2853,133 @@ function registerProfile(program2) {
2740
2853
  });
2741
2854
  }
2742
2855
 
2856
+ // src/cli/note.ts
2857
+ function registerNote(program2) {
2858
+ program2.command("note <text>").description("Attach a free-text annotation to the current session").action((text) => {
2859
+ const { repoName, branch } = getGitContext(process.cwd());
2860
+ insertEvent({
2861
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
2862
+ command: "workscribe note",
2863
+ cwd: process.cwd(),
2864
+ exit_code: null,
2865
+ duration_ms: null,
2866
+ repo_name: repoName,
2867
+ branch,
2868
+ category: "note",
2869
+ metadata: JSON.stringify({ text })
2870
+ });
2871
+ log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
2872
+ });
2873
+ }
2874
+
2875
+ // src/cli/week.ts
2876
+ var import_chalk11 = __toESM(require("chalk"));
2877
+ var import_fs19 = __toESM(require("fs"));
2878
+ var import_path14 = __toESM(require("path"));
2879
+ var import_os7 = __toESM(require("os"));
2880
+ function eventRowToPayload3(row) {
2881
+ let metadata = {};
2882
+ try {
2883
+ if (row.metadata) metadata = JSON.parse(row.metadata);
2884
+ } catch {
2885
+ }
2886
+ const payload = {
2887
+ category: row.category,
2888
+ metadata,
2889
+ exitCode: row.exit_code,
2890
+ durationMs: row.duration_ms
2891
+ };
2892
+ if (row.category === "other" && typeof metadata.hint === "string") {
2893
+ payload.hint = metadata.hint;
2894
+ const { hint: _hint, tool: _tool, ...rest } = metadata;
2895
+ payload.metadata = rest;
2896
+ }
2897
+ return payload;
2898
+ }
2899
+ function sessionToPayload2(session, date) {
2900
+ return {
2901
+ date,
2902
+ project: session.repoName,
2903
+ branch: session.events[0]?.branch ?? null,
2904
+ startTime: formatDate(session.startedAt),
2905
+ endTime: formatDate(session.endedAt),
2906
+ durationMinutes: session.durationMinutes,
2907
+ events: session.events.map(eventRowToPayload3)
2908
+ };
2909
+ }
2910
+ async function postSummary(opts, config, summary) {
2911
+ const webhookUrl = opts.webhook ?? (opts.slack ? config.integrations?.slackWebhook : void 0);
2912
+ if (!webhookUrl) {
2913
+ if (opts.slack) log.warn("--slack requires integrations.slackWebhook to be set in config");
2914
+ return;
2915
+ }
2916
+ try {
2917
+ log.info("Posting to webhook\u2026");
2918
+ const username = import_os7.default.userInfo().username;
2919
+ const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
2920
+ await postToWebhook(webhookUrl, body);
2921
+ log.success("Posted.");
2922
+ } catch (error) {
2923
+ log.error(`Webhook failed: ${error.message}`);
2924
+ }
2925
+ }
2926
+ function registerWeek(program2) {
2927
+ program2.command("week").description("Generate an AI weekly digest (Mon\u2013today)").option("--fresh", "bypass cache and regenerate").option("--slack", "post to Slack webhook from config").option("--webhook <url>", "post to a custom webhook URL").action(async (opts) => {
2928
+ const config = readConfig();
2929
+ let provider;
2930
+ try {
2931
+ provider = getProvider(config);
2932
+ } catch (error) {
2933
+ log.error(error.message);
2934
+ process.exit(1);
2935
+ }
2936
+ const today = todayDate();
2937
+ const weekStart = startOfWeek(today);
2938
+ const days = eachDayOfRange(weekStart, today);
2939
+ const weeklyDays = [];
2940
+ for (const day of days) {
2941
+ const sessions = getOrBuildSessions(day);
2942
+ if (sessions.length > 0) {
2943
+ weeklyDays.push({ date: day, sessions: sessions.map((session) => sessionToPayload2(session, day)) });
2944
+ }
2945
+ }
2946
+ if (weeklyDays.length === 0) {
2947
+ log.plain("No activity captured this week.");
2948
+ return;
2949
+ }
2950
+ const cacheKey = isoWeekKey(today);
2951
+ const cacheFile = import_path14.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2952
+ if (!opts.fresh && import_fs19.default.existsSync(cacheFile)) {
2953
+ const cached = import_fs19.default.readFileSync(cacheFile, "utf-8");
2954
+ log.plain("");
2955
+ log.plain(cached);
2956
+ log.plain("");
2957
+ log.plain(import_chalk11.default.dim(" (cached \u2014 use --fresh to regenerate)"));
2958
+ await postSummary(opts, config, cached);
2959
+ return;
2960
+ }
2961
+ const payload = { weekStart, weekEnd: today, days: weeklyDays };
2962
+ log.plain("");
2963
+ log.info(`Generating weekly digest via ${import_chalk11.default.cyan(config.ai.provider)}\u2026`);
2964
+ try {
2965
+ const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((day) => day.sessions) });
2966
+ log.plain("");
2967
+ log.plain(summary);
2968
+ log.plain("");
2969
+ import_fs19.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2970
+ import_fs19.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2971
+ await postSummary(opts, config, summary);
2972
+ } catch (error) {
2973
+ log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
2974
+ process.exit(1);
2975
+ }
2976
+ });
2977
+ }
2978
+
2743
2979
  // src/index.ts
2980
+ assertNodeVersion(process.versions.node);
2744
2981
  var program = new import_commander.Command();
2745
- program.name("workscribe").description("Automatic developer work journal").version("1.10.0");
2982
+ program.name("workscribe").description("Automatic developer work journal").version("1.11.0");
2746
2983
  registerCapture(program);
2747
2984
  registerHook(program);
2748
2985
  registerInit(program);
@@ -2757,5 +2994,7 @@ registerUndo(program);
2757
2994
  registerClear(program);
2758
2995
  registerUninstall(program);
2759
2996
  registerProfile(program);
2997
+ registerNote(program);
2998
+ registerWeek(program);
2760
2999
  program.parse();
2761
3000
  //# sourceMappingURL=index.js.map