@workscribe/cli 1.1.3 → 1.2.1

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
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_commander = require("commander");
28
28
 
29
29
  // src/capture/index.ts
30
- var import_fs3 = __toESM(require("fs"));
30
+ var import_fs4 = __toESM(require("fs"));
31
31
 
32
32
  // src/db/index.ts
33
33
  var import_node_sqlite = require("node:sqlite");
@@ -42,6 +42,7 @@ var CONFIG_PATH = import_path.default.join(WORKSCRIBE_DIR, "config.json");
42
42
  var DEBUG_LOG_PATH = import_path.default.join(WORKSCRIBE_DIR, "debug.log");
43
43
  var EXPORTS_DIR = import_path.default.join(WORKSCRIBE_DIR, "exports");
44
44
  var PAUSE_FLAG_PATH = import_path.default.join(WORKSCRIBE_DIR, "paused");
45
+ var WEEKLY_CACHE_DIR = import_path.default.join(WORKSCRIBE_DIR, "weekly-cache");
45
46
 
46
47
  // src/db/index.ts
47
48
  var _db = null;
@@ -151,6 +152,27 @@ function updateSessionSummary(sessionId, summary) {
151
152
  const stmt = db.prepare("UPDATE sessions SET summary = $summary WHERE id = $id");
152
153
  stmt.run({ summary, id: sessionId });
153
154
  }
155
+ function getLastEventIds(n) {
156
+ const db = getDb();
157
+ const rows = db.prepare("SELECT id FROM events ORDER BY id DESC LIMIT $n").all({ n });
158
+ return rows.map((r) => r.id);
159
+ }
160
+ function getEventsByIds(ids) {
161
+ if (ids.length === 0) return [];
162
+ const db = getDb();
163
+ const placeholders = ids.map(() => "?").join(", ");
164
+ return db.prepare(`SELECT * FROM events WHERE id IN (${placeholders}) ORDER BY id DESC`).all(...ids);
165
+ }
166
+ function deleteEventsByIds(ids) {
167
+ if (ids.length === 0) return;
168
+ const db = getDb();
169
+ const placeholders = ids.map(() => "?").join(", ");
170
+ const affectedDates = db.prepare(`SELECT DISTINCT date(captured_at) as date FROM events WHERE id IN (${placeholders})`).all(...ids);
171
+ db.prepare(`DELETE FROM events WHERE id IN (${placeholders})`).run(...ids);
172
+ for (const { date } of affectedDates) {
173
+ clearSessionsForDate(date);
174
+ }
175
+ }
154
176
  function clearSessionsForDate(date) {
155
177
  const db = getDb();
156
178
  db.prepare(`
@@ -202,14 +224,38 @@ function redact(command, extraPatterns = []) {
202
224
  // src/capture/git.ts
203
225
  var import_child_process = require("child_process");
204
226
  var import_path2 = __toESM(require("path"));
205
- function getGitContext(cwd) {
227
+ var import_fs2 = __toESM(require("fs"));
228
+ function nameFromRemote(root) {
229
+ try {
230
+ const url = (0, import_child_process.execSync)("git remote get-url origin", {
231
+ cwd: root,
232
+ timeout: 2e3,
233
+ stdio: "pipe"
234
+ }).toString().trim();
235
+ const match = url.match(/\/([^/]+?)(?:\.git)?$/);
236
+ return match ? match[1] : null;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ function nameFromPackageJson(root) {
242
+ try {
243
+ const raw = import_fs2.default.readFileSync(import_path2.default.join(root, "package.json"), "utf-8");
244
+ const pkg = JSON.parse(raw);
245
+ if (!pkg.name) return null;
246
+ return pkg.name.replace(/^@[^/]+\//, "");
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+ function getGitContext(cwd, nameOverride) {
206
252
  try {
207
253
  const root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
208
254
  cwd,
209
255
  timeout: 2e3,
210
256
  stdio: "pipe"
211
257
  }).toString().trim();
212
- const repoName = import_path2.default.basename(root);
258
+ const repoName = nameOverride ?? nameFromRemote(root) ?? nameFromPackageJson(root) ?? import_path2.default.basename(root);
213
259
  let branch = null;
214
260
  try {
215
261
  branch = (0, import_child_process.execSync)("git branch --show-current", {
@@ -379,7 +425,7 @@ function normalizeCommand(command) {
379
425
  }
380
426
 
381
427
  // src/config/index.ts
382
- var import_fs2 = __toESM(require("fs"));
428
+ var import_fs3 = __toESM(require("fs"));
383
429
  var DEFAULTS = {
384
430
  ai: {
385
431
  provider: "",
@@ -401,9 +447,9 @@ var DEFAULTS = {
401
447
  }
402
448
  };
403
449
  function readConfig() {
404
- if (!import_fs2.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
450
+ if (!import_fs3.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
405
451
  try {
406
- const raw = import_fs2.default.readFileSync(CONFIG_PATH, "utf-8");
452
+ const raw = import_fs3.default.readFileSync(CONFIG_PATH, "utf-8");
407
453
  const parsed = JSON.parse(raw);
408
454
  return deepMerge(DEFAULTS, parsed);
409
455
  } catch {
@@ -411,9 +457,9 @@ function readConfig() {
411
457
  }
412
458
  }
413
459
  function writeConfig(config) {
414
- import_fs2.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
460
+ import_fs3.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
415
461
  try {
416
- import_fs2.default.chmodSync(CONFIG_PATH, 384);
462
+ import_fs3.default.chmodSync(CONFIG_PATH, 384);
417
463
  } catch {
418
464
  }
419
465
  }
@@ -478,14 +524,15 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
478
524
 
479
525
  // src/capture/index.ts
480
526
  function captureCommand(opts) {
481
- if (import_fs3.default.existsSync(PAUSE_FLAG_PATH)) return;
527
+ if (import_fs4.default.existsSync(PAUSE_FLAG_PATH)) return;
482
528
  const config = readConfig();
483
529
  const { command, cwd, exitCode, durationMs } = opts;
484
530
  if (shouldFilter(command, config.capture.ignore)) return;
485
531
  const last = getLastEvent();
486
532
  if (isDuplicate(command, cwd, last ?? void 0)) return;
487
533
  const safeCommand = redact(command, config.redact.extra);
488
- const { repoName, branch } = getGitContext(cwd);
534
+ const nameOverride = config.projects?.[cwd] ?? null;
535
+ const { repoName, branch } = getGitContext(cwd, nameOverride);
489
536
  const { category, metadata } = normalizeCommand(safeCommand);
490
537
  insertEvent({
491
538
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -502,7 +549,7 @@ function captureCommand(opts) {
502
549
 
503
550
  // src/utils/logger.ts
504
551
  var import_chalk = __toESM(require("chalk"));
505
- var import_fs4 = __toESM(require("fs"));
552
+ var import_fs5 = __toESM(require("fs"));
506
553
  var isDebug = process.env.WORKSCRIBE_DEBUG === "1";
507
554
  var log = {
508
555
  info: (msg) => console.log(import_chalk.default.cyan("\u2192"), msg),
@@ -516,7 +563,7 @@ function debugLog(context, error) {
516
563
  const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${context}] ${error instanceof Error ? error.stack : String(error)}
517
564
  `;
518
565
  try {
519
- import_fs4.default.appendFileSync(DEBUG_LOG_PATH, entry, { mode: 384 });
566
+ import_fs5.default.appendFileSync(DEBUG_LOG_PATH, entry, { mode: 384 });
520
567
  } catch {
521
568
  }
522
569
  }
@@ -539,7 +586,7 @@ function registerCapture(program2) {
539
586
  }
540
587
 
541
588
  // src/cli/init.ts
542
- var import_fs5 = __toESM(require("fs"));
589
+ var import_fs6 = __toESM(require("fs"));
543
590
  var import_path3 = __toESM(require("path"));
544
591
  var import_os2 = __toESM(require("os"));
545
592
  var import_prompts = __toESM(require("prompts"));
@@ -552,26 +599,26 @@ function detectShell() {
552
599
  return null;
553
600
  }
554
601
  function isHookInstalled(rcFile) {
555
- if (!import_fs5.default.existsSync(rcFile)) return false;
556
- return import_fs5.default.readFileSync(rcFile, "utf-8").includes(HOOK_START);
602
+ if (!import_fs6.default.existsSync(rcFile)) return false;
603
+ return import_fs6.default.readFileSync(rcFile, "utf-8").includes(HOOK_START);
557
604
  }
558
605
  function installHook(rcFile, hookContent) {
559
606
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
560
- import_fs5.default.copyFileSync(rcFile, backup);
607
+ import_fs6.default.copyFileSync(rcFile, backup);
561
608
  log.info(`Backed up ${rcFile} \u2192 ${import_path3.default.basename(backup)}`);
562
- import_fs5.default.appendFileSync(rcFile, `
609
+ import_fs6.default.appendFileSync(rcFile, `
563
610
  ${hookContent}
564
611
  `);
565
612
  }
566
613
  function registerInit(program2) {
567
614
  program2.command("init").description("Install shell hooks and set up Workscribe").action(async () => {
568
615
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
569
- if (!import_fs5.default.existsSync(WORKSCRIBE_DIR)) {
570
- import_fs5.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
616
+ if (!import_fs6.default.existsSync(WORKSCRIBE_DIR)) {
617
+ import_fs6.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
571
618
  log.success(`Created ${WORKSCRIBE_DIR}`);
572
619
  } else {
573
620
  try {
574
- import_fs5.default.chmodSync(WORKSCRIBE_DIR, 448);
621
+ import_fs6.default.chmodSync(WORKSCRIBE_DIR, 448);
575
622
  } catch {
576
623
  }
577
624
  }
@@ -586,11 +633,11 @@ function registerInit(program2) {
586
633
  log.info(`Shell hook already installed in ${shell.rcFile}`);
587
634
  } else {
588
635
  const hookFile = import_path3.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
589
- if (!import_fs5.default.existsSync(hookFile)) {
636
+ if (!import_fs6.default.existsSync(hookFile)) {
590
637
  log.warn(`Hook template not found at ${hookFile}`);
591
638
  } else {
592
- const hookContent = import_fs5.default.readFileSync(hookFile, "utf-8");
593
- if (!import_fs5.default.existsSync(shell.rcFile)) import_fs5.default.writeFileSync(shell.rcFile, "");
639
+ const hookContent = import_fs6.default.readFileSync(hookFile, "utf-8");
640
+ if (!import_fs6.default.existsSync(shell.rcFile)) import_fs6.default.writeFileSync(shell.rcFile, "");
594
641
  installHook(shell.rcFile, hookContent);
595
642
  log.success(`Shell hook installed in ${shell.rcFile}`);
596
643
  }
@@ -642,7 +689,7 @@ function registerInit(program2) {
642
689
  log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
643
690
  }
644
691
  }
645
- if (!import_fs5.default.existsSync(CONFIG_PATH)) {
692
+ if (!import_fs6.default.existsSync(CONFIG_PATH)) {
646
693
  writeConfig(config);
647
694
  log.success("Config file created");
648
695
  }
@@ -661,7 +708,7 @@ function registerInit(program2) {
661
708
  }
662
709
 
663
710
  // src/cli/status.ts
664
- var import_fs6 = __toESM(require("fs"));
711
+ var import_fs7 = __toESM(require("fs"));
665
712
  var import_chalk3 = __toESM(require("chalk"));
666
713
 
667
714
  // src/utils/format.ts
@@ -682,6 +729,35 @@ function parseDate(input) {
682
729
  if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
683
730
  return input;
684
731
  }
732
+ function localDateStr(d) {
733
+ const y = d.getFullYear();
734
+ const m = String(d.getMonth() + 1).padStart(2, "0");
735
+ const day = String(d.getDate()).padStart(2, "0");
736
+ return `${y}-${m}-${day}`;
737
+ }
738
+ function startOfWeek(dateStr) {
739
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
740
+ const day = d.getDay();
741
+ const diff = day === 0 ? -6 : 1 - day;
742
+ d.setDate(d.getDate() + diff);
743
+ return localDateStr(d);
744
+ }
745
+ function eachDayOfRange(start, end) {
746
+ const days = [];
747
+ const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
748
+ const last = /* @__PURE__ */ new Date(end + "T00:00:00");
749
+ while (cur <= last) {
750
+ days.push(localDateStr(cur));
751
+ cur.setDate(cur.getDate() + 1);
752
+ }
753
+ return days;
754
+ }
755
+ function isoWeekKey(dateStr) {
756
+ const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
757
+ const jan4 = new Date(d.getFullYear(), 0, 4);
758
+ const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
759
+ return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
760
+ }
685
761
 
686
762
  // src/cli/status.ts
687
763
  function registerStatus(program2) {
@@ -690,7 +766,7 @@ function registerStatus(program2) {
690
766
  const total = getEventCount();
691
767
  const todayEvents = getEventsForDate(todayDate());
692
768
  const last = getLastEvent();
693
- const isPaused = import_fs6.default.existsSync(PAUSE_FLAG_PATH);
769
+ const isPaused = import_fs7.default.existsSync(PAUSE_FLAG_PATH);
694
770
  log.plain("");
695
771
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
696
772
  log.plain("");
@@ -900,6 +976,8 @@ function registerSessions(program2) {
900
976
 
901
977
  // src/cli/summary.ts
902
978
  var import_chalk6 = __toESM(require("chalk"));
979
+ var import_fs8 = __toESM(require("fs"));
980
+ var import_path4 = __toESM(require("path"));
903
981
 
904
982
  // src/ai/prompt.ts
905
983
  var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
@@ -916,13 +994,12 @@ Events:
916
994
  - code_commit {"message":"fix: auth token validation"}
917
995
 
918
996
  OUTPUT:
919
- ## api-service (feature/auth) \u2014 1h 30m
920
997
  - Switched to the authentication feature branch
921
998
  - Added JWT and bcrypt dependencies
922
999
  - Resolved failing auth tests
923
1000
  - Committed authentication fixes
924
1001
 
925
- Now summarise the following. Output ONLY the summary, nothing else. Maximum 5 bullets.`;
1002
+ Now summarise the following. Output ONLY the bullet points, nothing else. Maximum 5 bullets.`;
926
1003
  function buildUserMessage(payload) {
927
1004
  const lines = [`Date: ${payload.date}`, ""];
928
1005
  for (const session of payload.sessions) {
@@ -944,6 +1021,62 @@ function buildUserMessage(payload) {
944
1021
  }
945
1022
  return lines.join("\n");
946
1023
  }
1024
+ var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
1025
+
1026
+ INPUT:
1027
+ Week: 2026-05-18 to 2026-05-22
1028
+
1029
+ Monday 2026-05-18:
1030
+ Project: api-service (feature/auth) \u2014 2h 10m
1031
+ Events: 3x code_commit, 2x test_run, dependency_install
1032
+
1033
+ Tuesday 2026-05-19:
1034
+ Project: api-service (feature/auth) \u2014 1h 30m
1035
+ Events: 2x code_commit, test_run, build
1036
+
1037
+ Wednesday 2026-05-20:
1038
+ Project: dashboard (main) \u2014 3h 00m
1039
+ Events: 4x code_commit, 2x build, test_run
1040
+
1041
+ Thursday 2026-05-21:
1042
+ Project: api-service (feature/auth) \u2014 1h 00m
1043
+ Events: code_commit {"message":"feat: merge auth"}, branch_switch
1044
+
1045
+ Friday 2026-05-22:
1046
+ Project: dashboard (main) \u2014 2h 00m
1047
+ Events: 3x code_commit, build
1048
+
1049
+ OUTPUT:
1050
+ ## Week of 18 May \u2014 9h 40m across 2 projects
1051
+
1052
+ **api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
1053
+ **dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
1054
+
1055
+ Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
1056
+ var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1057
+ function buildWeeklyMessage(payload) {
1058
+ const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
1059
+ for (const { date, sessions } of payload.days) {
1060
+ const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
1061
+ lines.push(`${dayName} ${date}:`);
1062
+ for (const s of sessions) {
1063
+ const project = s.project ?? "unknown";
1064
+ const branch = s.branch ? ` (${s.branch})` : "";
1065
+ lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
1066
+ const counts = /* @__PURE__ */ new Map();
1067
+ for (const e of s.events) {
1068
+ const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
1069
+ const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
1070
+ const key = `${e.category}${meta}${failed}`;
1071
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1072
+ }
1073
+ const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
1074
+ lines.push(` Events: ${eventLine}`);
1075
+ }
1076
+ lines.push("");
1077
+ }
1078
+ return lines.join("\n");
1079
+ }
947
1080
 
948
1081
  // src/ai/fetch.ts
949
1082
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -992,6 +1125,28 @@ var AnthropicProvider = class {
992
1125
  const data = await response.json();
993
1126
  return data.content.find((c) => c.type === "text")?.text ?? "";
994
1127
  }
1128
+ async summarizeWeek(payload) {
1129
+ const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
1130
+ method: "POST",
1131
+ headers: {
1132
+ "Content-Type": "application/json",
1133
+ "x-api-key": this.apiKey,
1134
+ "anthropic-version": "2023-06-01"
1135
+ },
1136
+ body: JSON.stringify({
1137
+ model: this.model,
1138
+ max_tokens: 1024,
1139
+ system: WEEKLY_SYSTEM_PROMPT,
1140
+ messages: [{ role: "user", content: buildWeeklyMessage(payload) }]
1141
+ })
1142
+ });
1143
+ if (!response.ok) {
1144
+ const body = await response.text();
1145
+ throw new Error(`Anthropic API error ${response.status}: ${body}`);
1146
+ }
1147
+ const data = await response.json();
1148
+ return data.content.find((c) => c.type === "text")?.text ?? "";
1149
+ }
995
1150
  };
996
1151
 
997
1152
  // src/ai/providers/openai.ts
@@ -1027,6 +1182,29 @@ var OpenAIProvider = class {
1027
1182
  const data = await response.json();
1028
1183
  return data.choices[0]?.message?.content ?? "";
1029
1184
  }
1185
+ async summarizeWeek(payload) {
1186
+ const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
1187
+ method: "POST",
1188
+ headers: {
1189
+ "Content-Type": "application/json",
1190
+ Authorization: `Bearer ${this.apiKey}`
1191
+ },
1192
+ body: JSON.stringify({
1193
+ model: this.model,
1194
+ messages: [
1195
+ { role: "system", content: WEEKLY_SYSTEM_PROMPT },
1196
+ { role: "user", content: buildWeeklyMessage(payload) }
1197
+ ],
1198
+ max_tokens: 1024
1199
+ })
1200
+ });
1201
+ if (!response.ok) {
1202
+ const body = await response.text();
1203
+ throw new Error(`OpenAI API error ${response.status}: ${body}`);
1204
+ }
1205
+ const data = await response.json();
1206
+ return data.choices[0]?.message?.content ?? "";
1207
+ }
1030
1208
  };
1031
1209
 
1032
1210
  // src/ai/providers/ollama.ts
@@ -1063,6 +1241,30 @@ var OllamaProvider = class {
1063
1241
  const data = await response.json();
1064
1242
  return data.message?.content ?? "";
1065
1243
  }
1244
+ async summarizeWeek(payload) {
1245
+ const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
1246
+ method: "POST",
1247
+ headers: { "Content-Type": "application/json" },
1248
+ body: JSON.stringify({
1249
+ model: this.model,
1250
+ stream: false,
1251
+ messages: [
1252
+ { role: "system", content: WEEKLY_SYSTEM_PROMPT },
1253
+ { role: "user", content: buildWeeklyMessage(payload) }
1254
+ ],
1255
+ options: {
1256
+ num_predict: 400,
1257
+ temperature: 0.3
1258
+ }
1259
+ })
1260
+ });
1261
+ if (!response.ok) {
1262
+ const body = await response.text();
1263
+ throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
1264
+ }
1265
+ const data = await response.json();
1266
+ return data.message?.content ?? "";
1267
+ }
1066
1268
  };
1067
1269
 
1068
1270
  // src/ai/router.ts
@@ -1098,6 +1300,23 @@ Supported providers: anthropic, openai, ollama, openai-compatible`
1098
1300
  }
1099
1301
  }
1100
1302
 
1303
+ // src/integrations/webhook.ts
1304
+ async function postToWebhook(url, text) {
1305
+ const response = await fetchWithTimeout(
1306
+ url,
1307
+ {
1308
+ method: "POST",
1309
+ headers: { "Content-Type": "application/json" },
1310
+ body: JSON.stringify({ text })
1311
+ },
1312
+ 1e4
1313
+ );
1314
+ if (!response.ok) {
1315
+ const body = await response.text();
1316
+ throw new Error(`Webhook POST failed ${response.status}: ${body}`);
1317
+ }
1318
+ }
1319
+
1101
1320
  // src/cli/summary.ts
1102
1321
  function eventRowToPayload(row) {
1103
1322
  let metadata = {};
@@ -1112,8 +1331,84 @@ function eventRowToPayload(row) {
1112
1331
  durationMs: row.duration_ms
1113
1332
  };
1114
1333
  }
1334
+ function sessionToPayload(s, date) {
1335
+ return {
1336
+ date,
1337
+ project: s.repoName,
1338
+ branch: s.events[0]?.branch ?? null,
1339
+ startTime: formatDate(s.startedAt),
1340
+ endTime: formatDate(s.endedAt),
1341
+ durationMinutes: s.durationMinutes,
1342
+ events: s.events.map(eventRowToPayload)
1343
+ };
1344
+ }
1345
+ async function handleWebhook(opts, config, summary) {
1346
+ const webhookUrl = opts.webhook ?? (opts.slack ? config.integrations?.slackWebhook : void 0);
1347
+ if (webhookUrl) {
1348
+ try {
1349
+ log.info("Posting to webhook\u2026");
1350
+ await postToWebhook(webhookUrl, summary);
1351
+ log.success("Posted.");
1352
+ } catch (e) {
1353
+ log.error(`Webhook failed: ${e.message}`);
1354
+ }
1355
+ } else if (opts.slack && !config.integrations?.slackWebhook) {
1356
+ log.warn("--slack requires integrations.slackWebhook to be set in config");
1357
+ }
1358
+ }
1115
1359
  function registerSummary(program2) {
1116
- 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").action(async (opts) => {
1360
+ 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) => {
1361
+ const config = readConfig();
1362
+ let provider;
1363
+ try {
1364
+ provider = getProvider(config);
1365
+ } catch (e) {
1366
+ log.error(e.message);
1367
+ process.exit(1);
1368
+ }
1369
+ if (opts.week) {
1370
+ const today = todayDate();
1371
+ const weekStart = startOfWeek(today);
1372
+ const days = eachDayOfRange(weekStart, today);
1373
+ const weeklyDays = [];
1374
+ for (const day of days) {
1375
+ const sessions2 = getOrBuildSessions(day);
1376
+ if (sessions2.length > 0) {
1377
+ weeklyDays.push({ date: day, sessions: sessions2.map((s) => sessionToPayload(s, day)) });
1378
+ }
1379
+ }
1380
+ if (weeklyDays.length === 0) {
1381
+ log.plain("No activity captured this week.");
1382
+ return;
1383
+ }
1384
+ const cacheKey = isoWeekKey(today);
1385
+ const cacheFile = import_path4.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
1386
+ if (!opts.fresh && import_fs8.default.existsSync(cacheFile)) {
1387
+ const cached = import_fs8.default.readFileSync(cacheFile, "utf-8");
1388
+ log.plain("");
1389
+ log.plain(cached);
1390
+ log.plain("");
1391
+ log.plain(import_chalk6.default.dim(" (cached \u2014 use --fresh to regenerate)"));
1392
+ await handleWebhook(opts, config, cached);
1393
+ return;
1394
+ }
1395
+ const payload = { weekStart, weekEnd: today, days: weeklyDays };
1396
+ log.plain("");
1397
+ log.info(`Generating weekly summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
1398
+ try {
1399
+ const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((d) => d.sessions) });
1400
+ log.plain("");
1401
+ log.plain(summary);
1402
+ log.plain("");
1403
+ import_fs8.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
1404
+ import_fs8.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
1405
+ await handleWebhook(opts, config, summary);
1406
+ } catch (e) {
1407
+ log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
1408
+ process.exit(1);
1409
+ }
1410
+ return;
1411
+ }
1117
1412
  let date = todayDate();
1118
1413
  if (opts.date) {
1119
1414
  try {
@@ -1140,44 +1435,37 @@ function registerSummary(program2) {
1140
1435
  return;
1141
1436
  }
1142
1437
  }
1143
- const config = readConfig();
1144
- let provider;
1145
- try {
1146
- provider = getProvider(config);
1147
- } catch (e) {
1148
- log.error(e.message);
1149
- process.exit(1);
1150
- }
1151
- const sessionPayloads = sessions.map((s) => ({
1152
- date,
1153
- project: s.repoName,
1154
- branch: s.events[0]?.branch ?? null,
1155
- startTime: formatDate(s.startedAt),
1156
- endTime: formatDate(s.endedAt),
1157
- durationMinutes: s.durationMinutes,
1158
- events: s.events.map(eventRowToPayload)
1159
- }));
1160
- const payload = { date, sessions: sessionPayloads };
1161
1438
  log.plain("");
1162
1439
  log.info(`Generating summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
1163
- try {
1164
- const summary = await provider.summarize(payload);
1165
- log.plain("");
1166
- log.plain(summary);
1167
- log.plain("");
1168
- if (sessions[0]) {
1169
- updateSessionSummary(sessions[0].id, summary);
1440
+ const allSummaries = [];
1441
+ for (const session of sessions) {
1442
+ const sessionPayload = { date, sessions: [sessionToPayload(session, date)] };
1443
+ try {
1444
+ const bullets = await provider.summarize(sessionPayload);
1445
+ const project = session.repoName ?? "unknown";
1446
+ const branch = session.events[0]?.branch ? ` (${session.events[0].branch})` : "";
1447
+ const duration = formatDuration(session.durationMinutes);
1448
+ const heading = `## ${project}${branch} \u2014 ${duration}`;
1449
+ const summary = `${heading}
1450
+ ${bullets.trim()}`;
1451
+ allSummaries.push(summary);
1452
+ updateSessionSummary(session.id, summary);
1453
+ } catch (e) {
1454
+ log.error(`Summary failed [${config.ai.provider}]: ${e.message}`);
1455
+ process.exit(1);
1170
1456
  }
1171
- } catch (e) {
1172
- log.error(`Summary failed [${config.ai.provider}]: ${e.message}`);
1173
- process.exit(1);
1174
1457
  }
1458
+ const fullSummary = allSummaries.join("\n\n");
1459
+ log.plain("");
1460
+ log.plain(fullSummary);
1461
+ log.plain("");
1462
+ await handleWebhook(opts, config, fullSummary);
1175
1463
  });
1176
1464
  }
1177
1465
 
1178
1466
  // src/cli/export.ts
1179
- var import_fs7 = __toESM(require("fs"));
1180
- var import_path4 = __toESM(require("path"));
1467
+ var import_fs9 = __toESM(require("fs"));
1468
+ var import_path5 = __toESM(require("path"));
1181
1469
  function eventRowToPayload2(row) {
1182
1470
  let metadata = {};
1183
1471
  try {
@@ -1222,9 +1510,9 @@ function registerExport(program2) {
1222
1510
  const payload = { date, sessions: sessionPayloads };
1223
1511
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
1224
1512
  const summary = await provider.summarize(payload);
1225
- const outputPath = opts.output ?? import_path4.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
1226
- if (!import_fs7.default.existsSync(EXPORTS_DIR)) import_fs7.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
1227
- import_fs7.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
1513
+ const outputPath = opts.output ?? import_path5.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
1514
+ if (!import_fs9.default.existsSync(EXPORTS_DIR)) import_fs9.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
1515
+ import_fs9.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
1228
1516
 
1229
1517
  ${summary}
1230
1518
  `, { encoding: "utf-8", mode: 384 });
@@ -1261,7 +1549,7 @@ function registerConfig(program2) {
1261
1549
  }
1262
1550
  });
1263
1551
  }
1264
- var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["apiKey", "api_key", "secret", "token", "password", "passwd"]);
1552
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["apiKey", "api_key", "secret", "token", "password", "passwd", "webhook", "slackWebhook"]);
1265
1553
  function maskValue(key, value) {
1266
1554
  if (SENSITIVE_KEYS.has(key) && typeof value === "string" && value.length > 0) {
1267
1555
  return import_chalk7.default.dim(`${value.slice(0, 4)}${"*".repeat(8)} (${value.length} chars)`);
@@ -1282,29 +1570,58 @@ function printConfig(obj, indent) {
1282
1570
  }
1283
1571
 
1284
1572
  // src/cli/pause.ts
1285
- var import_fs8 = __toESM(require("fs"));
1573
+ var import_fs10 = __toESM(require("fs"));
1286
1574
  function registerPause(program2) {
1287
1575
  program2.command("pause").description("Pause capture temporarily").action(() => {
1288
- if (import_fs8.default.existsSync(PAUSE_FLAG_PATH)) {
1576
+ if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) {
1289
1577
  log.info("Capture is already paused. Run workscribe resume to restart.");
1290
1578
  return;
1291
1579
  }
1292
- import_fs8.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
1580
+ import_fs10.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
1293
1581
  log.success("Capture paused. Run workscribe resume to restart.");
1294
1582
  });
1295
1583
  program2.command("resume").description("Resume capture after a pause").action(() => {
1296
- if (!import_fs8.default.existsSync(PAUSE_FLAG_PATH)) {
1584
+ if (!import_fs10.default.existsSync(PAUSE_FLAG_PATH)) {
1297
1585
  log.info("Capture is not paused.");
1298
1586
  return;
1299
1587
  }
1300
- import_fs8.default.rmSync(PAUSE_FLAG_PATH);
1588
+ import_fs10.default.rmSync(PAUSE_FLAG_PATH);
1301
1589
  log.success("Capture resumed.");
1302
1590
  });
1303
1591
  }
1304
1592
 
1593
+ // src/cli/undo.ts
1594
+ function registerUndo(program2) {
1595
+ program2.command("undo").description("Remove the last N captured events").option("-n, --count <number>", "number of events to remove", "1").option("--dry-run", "preview what would be deleted without deleting").action((opts) => {
1596
+ const count = parseInt(opts.count, 10);
1597
+ if (isNaN(count) || count < 1) {
1598
+ log.error("--count must be a positive integer");
1599
+ process.exit(1);
1600
+ }
1601
+ const ids = getLastEventIds(count);
1602
+ if (ids.length === 0) {
1603
+ log.plain("No events to undo.");
1604
+ return;
1605
+ }
1606
+ const events = getEventsByIds(ids);
1607
+ log.plain("");
1608
+ log.plain(`Events to remove (${events.length}):`);
1609
+ for (const e of events) {
1610
+ log.plain(` ${e.captured_at} ${e.category} ${e.command}`);
1611
+ }
1612
+ log.plain("");
1613
+ if (opts.dryRun) {
1614
+ log.plain("Dry run \u2014 nothing deleted.");
1615
+ return;
1616
+ }
1617
+ deleteEventsByIds(ids);
1618
+ log.success(`Removed ${ids.length} event${ids.length > 1 ? "s" : ""}.`);
1619
+ });
1620
+ }
1621
+
1305
1622
  // src/index.ts
1306
1623
  var program = new import_commander.Command();
1307
- program.name("workscribe").description("Automatic developer work journal").version("1.1.3");
1624
+ program.name("workscribe").description("Automatic developer work journal").version("1.2.1");
1308
1625
  registerCapture(program);
1309
1626
  registerInit(program);
1310
1627
  registerStatus(program);
@@ -1314,5 +1631,6 @@ registerSummary(program);
1314
1631
  registerExport(program);
1315
1632
  registerConfig(program);
1316
1633
  registerPause(program);
1634
+ registerUndo(program);
1317
1635
  program.parse();
1318
1636
  //# sourceMappingURL=index.js.map