@workscribe/cli 1.1.2 → 1.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 +21 -2
- package/dist/index.js +400 -77
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
450
|
+
if (!import_fs3.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
405
451
|
try {
|
|
406
|
-
const raw =
|
|
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
|
-
|
|
460
|
+
import_fs3.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
415
461
|
try {
|
|
416
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
556
|
-
return
|
|
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
|
-
|
|
607
|
+
import_fs6.default.copyFileSync(rcFile, backup);
|
|
561
608
|
log.info(`Backed up ${rcFile} \u2192 ${import_path3.default.basename(backup)}`);
|
|
562
|
-
|
|
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 (!
|
|
570
|
-
|
|
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
|
-
|
|
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 (!
|
|
636
|
+
if (!import_fs6.default.existsSync(hookFile)) {
|
|
590
637
|
log.warn(`Hook template not found at ${hookFile}`);
|
|
591
638
|
} else {
|
|
592
|
-
const hookContent =
|
|
593
|
-
if (!
|
|
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 (!
|
|
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
|
|
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 =
|
|
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,39 +976,103 @@ 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
|
-
var SYSTEM_PROMPT = `You
|
|
906
|
-
|
|
907
|
-
Rules:
|
|
908
|
-
- Write in past tense, professional tone
|
|
909
|
-
- Describe what was ACCOMPLISHED, not what commands were run
|
|
910
|
-
- Never write literal command names (e.g. do not write "ran npm install" \u2014 write "added HTTP client dependency")
|
|
911
|
-
- Never list commands literally
|
|
912
|
-
- Each bullet point should describe a meaningful unit of work
|
|
913
|
-
- Group related events into single bullet points
|
|
914
|
-
- If tests failed then passed, describe it as "resolved failing tests" not two separate events
|
|
915
|
-
- Return 3-6 bullet points per project section
|
|
916
|
-
- Do not pad or add filler sentences
|
|
917
|
-
- Output only the bullet points grouped under project headers using this exact format:
|
|
983
|
+
var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
|
|
918
984
|
|
|
919
|
-
|
|
985
|
+
INPUT:
|
|
986
|
+
Project: api-service
|
|
987
|
+
Branch: feature/auth
|
|
988
|
+
Duration: 1h 30m
|
|
989
|
+
Events:
|
|
990
|
+
- branch_switch {"branch":"feature/auth"}
|
|
991
|
+
- 2x dependency_install {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
992
|
+
- 2x test_run {"tool":"jest"} [failed]
|
|
993
|
+
- test_run {"tool":"jest"}
|
|
994
|
+
- code_commit {"message":"fix: auth token validation"}
|
|
920
995
|
|
|
921
|
-
|
|
922
|
-
-
|
|
996
|
+
OUTPUT:
|
|
997
|
+
## api-service (feature/auth) \u2014 1h 30m
|
|
998
|
+
- Switched to the authentication feature branch
|
|
999
|
+
- Added JWT and bcrypt dependencies
|
|
1000
|
+
- Resolved failing auth tests
|
|
1001
|
+
- Committed authentication fixes
|
|
923
1002
|
|
|
924
|
-
|
|
1003
|
+
Now summarise the following. Output ONLY the summary, nothing else. Maximum 5 bullets.`;
|
|
925
1004
|
function buildUserMessage(payload) {
|
|
926
1005
|
const lines = [`Date: ${payload.date}`, ""];
|
|
927
1006
|
for (const session of payload.sessions) {
|
|
928
1007
|
lines.push(`Project: ${session.project ?? "unknown"}`);
|
|
929
1008
|
if (session.branch) lines.push(`Branch: ${session.branch}`);
|
|
930
|
-
lines.push(`
|
|
1009
|
+
lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
|
|
931
1010
|
lines.push("Events:");
|
|
1011
|
+
const counts = /* @__PURE__ */ new Map();
|
|
932
1012
|
for (const event of session.events) {
|
|
933
1013
|
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
934
|
-
const
|
|
935
|
-
|
|
1014
|
+
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1015
|
+
const key = `${event.category}${meta}${failed}`;
|
|
1016
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1017
|
+
}
|
|
1018
|
+
for (const [key, count] of counts) {
|
|
1019
|
+
lines.push(` - ${count > 1 ? `${count}x ` : ""}${key}`);
|
|
1020
|
+
}
|
|
1021
|
+
lines.push("");
|
|
1022
|
+
}
|
|
1023
|
+
return lines.join("\n");
|
|
1024
|
+
}
|
|
1025
|
+
var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
|
|
1026
|
+
|
|
1027
|
+
INPUT:
|
|
1028
|
+
Week: 2026-05-18 to 2026-05-22
|
|
1029
|
+
|
|
1030
|
+
Monday 2026-05-18:
|
|
1031
|
+
Project: api-service (feature/auth) \u2014 2h 10m
|
|
1032
|
+
Events: 3x code_commit, 2x test_run, dependency_install
|
|
1033
|
+
|
|
1034
|
+
Tuesday 2026-05-19:
|
|
1035
|
+
Project: api-service (feature/auth) \u2014 1h 30m
|
|
1036
|
+
Events: 2x code_commit, test_run, build
|
|
1037
|
+
|
|
1038
|
+
Wednesday 2026-05-20:
|
|
1039
|
+
Project: dashboard (main) \u2014 3h 00m
|
|
1040
|
+
Events: 4x code_commit, 2x build, test_run
|
|
1041
|
+
|
|
1042
|
+
Thursday 2026-05-21:
|
|
1043
|
+
Project: api-service (feature/auth) \u2014 1h 00m
|
|
1044
|
+
Events: code_commit {"message":"feat: merge auth"}, branch_switch
|
|
1045
|
+
|
|
1046
|
+
Friday 2026-05-22:
|
|
1047
|
+
Project: dashboard (main) \u2014 2h 00m
|
|
1048
|
+
Events: 3x code_commit, build
|
|
1049
|
+
|
|
1050
|
+
OUTPUT:
|
|
1051
|
+
## Week of 18 May \u2014 9h 40m across 2 projects
|
|
1052
|
+
|
|
1053
|
+
**api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
|
|
1054
|
+
**dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
|
|
1055
|
+
|
|
1056
|
+
Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
|
|
1057
|
+
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1058
|
+
function buildWeeklyMessage(payload) {
|
|
1059
|
+
const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
|
|
1060
|
+
for (const { date, sessions } of payload.days) {
|
|
1061
|
+
const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
|
|
1062
|
+
lines.push(`${dayName} ${date}:`);
|
|
1063
|
+
for (const s of sessions) {
|
|
1064
|
+
const project = s.project ?? "unknown";
|
|
1065
|
+
const branch = s.branch ? ` (${s.branch})` : "";
|
|
1066
|
+
lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
|
|
1067
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const e of s.events) {
|
|
1069
|
+
const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
|
|
1070
|
+
const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
|
|
1071
|
+
const key = `${e.category}${meta}${failed}`;
|
|
1072
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1073
|
+
}
|
|
1074
|
+
const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
|
|
1075
|
+
lines.push(` Events: ${eventLine}`);
|
|
936
1076
|
}
|
|
937
1077
|
lines.push("");
|
|
938
1078
|
}
|
|
@@ -986,6 +1126,28 @@ var AnthropicProvider = class {
|
|
|
986
1126
|
const data = await response.json();
|
|
987
1127
|
return data.content.find((c) => c.type === "text")?.text ?? "";
|
|
988
1128
|
}
|
|
1129
|
+
async summarizeWeek(payload) {
|
|
1130
|
+
const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
|
|
1131
|
+
method: "POST",
|
|
1132
|
+
headers: {
|
|
1133
|
+
"Content-Type": "application/json",
|
|
1134
|
+
"x-api-key": this.apiKey,
|
|
1135
|
+
"anthropic-version": "2023-06-01"
|
|
1136
|
+
},
|
|
1137
|
+
body: JSON.stringify({
|
|
1138
|
+
model: this.model,
|
|
1139
|
+
max_tokens: 1024,
|
|
1140
|
+
system: WEEKLY_SYSTEM_PROMPT,
|
|
1141
|
+
messages: [{ role: "user", content: buildWeeklyMessage(payload) }]
|
|
1142
|
+
})
|
|
1143
|
+
});
|
|
1144
|
+
if (!response.ok) {
|
|
1145
|
+
const body = await response.text();
|
|
1146
|
+
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1147
|
+
}
|
|
1148
|
+
const data = await response.json();
|
|
1149
|
+
return data.content.find((c) => c.type === "text")?.text ?? "";
|
|
1150
|
+
}
|
|
989
1151
|
};
|
|
990
1152
|
|
|
991
1153
|
// src/ai/providers/openai.ts
|
|
@@ -1021,6 +1183,29 @@ var OpenAIProvider = class {
|
|
|
1021
1183
|
const data = await response.json();
|
|
1022
1184
|
return data.choices[0]?.message?.content ?? "";
|
|
1023
1185
|
}
|
|
1186
|
+
async summarizeWeek(payload) {
|
|
1187
|
+
const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
|
|
1188
|
+
method: "POST",
|
|
1189
|
+
headers: {
|
|
1190
|
+
"Content-Type": "application/json",
|
|
1191
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1192
|
+
},
|
|
1193
|
+
body: JSON.stringify({
|
|
1194
|
+
model: this.model,
|
|
1195
|
+
messages: [
|
|
1196
|
+
{ role: "system", content: WEEKLY_SYSTEM_PROMPT },
|
|
1197
|
+
{ role: "user", content: buildWeeklyMessage(payload) }
|
|
1198
|
+
],
|
|
1199
|
+
max_tokens: 1024
|
|
1200
|
+
})
|
|
1201
|
+
});
|
|
1202
|
+
if (!response.ok) {
|
|
1203
|
+
const body = await response.text();
|
|
1204
|
+
throw new Error(`OpenAI API error ${response.status}: ${body}`);
|
|
1205
|
+
}
|
|
1206
|
+
const data = await response.json();
|
|
1207
|
+
return data.choices[0]?.message?.content ?? "";
|
|
1208
|
+
}
|
|
1024
1209
|
};
|
|
1025
1210
|
|
|
1026
1211
|
// src/ai/providers/ollama.ts
|
|
@@ -1041,7 +1226,37 @@ var OllamaProvider = class {
|
|
|
1041
1226
|
messages: [
|
|
1042
1227
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
1043
1228
|
{ role: "user", content: buildUserMessage(payload) }
|
|
1044
|
-
]
|
|
1229
|
+
],
|
|
1230
|
+
options: {
|
|
1231
|
+
num_predict: 300,
|
|
1232
|
+
// hard cap on output tokens — forces conciseness
|
|
1233
|
+
temperature: 0.3
|
|
1234
|
+
// lower temperature = more focused, less wandering
|
|
1235
|
+
}
|
|
1236
|
+
})
|
|
1237
|
+
});
|
|
1238
|
+
if (!response.ok) {
|
|
1239
|
+
const body = await response.text();
|
|
1240
|
+
throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
|
|
1241
|
+
}
|
|
1242
|
+
const data = await response.json();
|
|
1243
|
+
return data.message?.content ?? "";
|
|
1244
|
+
}
|
|
1245
|
+
async summarizeWeek(payload) {
|
|
1246
|
+
const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
|
|
1247
|
+
method: "POST",
|
|
1248
|
+
headers: { "Content-Type": "application/json" },
|
|
1249
|
+
body: JSON.stringify({
|
|
1250
|
+
model: this.model,
|
|
1251
|
+
stream: false,
|
|
1252
|
+
messages: [
|
|
1253
|
+
{ role: "system", content: WEEKLY_SYSTEM_PROMPT },
|
|
1254
|
+
{ role: "user", content: buildWeeklyMessage(payload) }
|
|
1255
|
+
],
|
|
1256
|
+
options: {
|
|
1257
|
+
num_predict: 400,
|
|
1258
|
+
temperature: 0.3
|
|
1259
|
+
}
|
|
1045
1260
|
})
|
|
1046
1261
|
});
|
|
1047
1262
|
if (!response.ok) {
|
|
@@ -1086,6 +1301,23 @@ Supported providers: anthropic, openai, ollama, openai-compatible`
|
|
|
1086
1301
|
}
|
|
1087
1302
|
}
|
|
1088
1303
|
|
|
1304
|
+
// src/integrations/webhook.ts
|
|
1305
|
+
async function postToWebhook(url, text) {
|
|
1306
|
+
const response = await fetchWithTimeout(
|
|
1307
|
+
url,
|
|
1308
|
+
{
|
|
1309
|
+
method: "POST",
|
|
1310
|
+
headers: { "Content-Type": "application/json" },
|
|
1311
|
+
body: JSON.stringify({ text })
|
|
1312
|
+
},
|
|
1313
|
+
1e4
|
|
1314
|
+
);
|
|
1315
|
+
if (!response.ok) {
|
|
1316
|
+
const body = await response.text();
|
|
1317
|
+
throw new Error(`Webhook POST failed ${response.status}: ${body}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1089
1321
|
// src/cli/summary.ts
|
|
1090
1322
|
function eventRowToPayload(row) {
|
|
1091
1323
|
let metadata = {};
|
|
@@ -1100,8 +1332,84 @@ function eventRowToPayload(row) {
|
|
|
1100
1332
|
durationMs: row.duration_ms
|
|
1101
1333
|
};
|
|
1102
1334
|
}
|
|
1335
|
+
function sessionToPayload(s, date) {
|
|
1336
|
+
return {
|
|
1337
|
+
date,
|
|
1338
|
+
project: s.repoName,
|
|
1339
|
+
branch: s.events[0]?.branch ?? null,
|
|
1340
|
+
startTime: formatDate(s.startedAt),
|
|
1341
|
+
endTime: formatDate(s.endedAt),
|
|
1342
|
+
durationMinutes: s.durationMinutes,
|
|
1343
|
+
events: s.events.map(eventRowToPayload)
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
async function handleWebhook(opts, config, summary) {
|
|
1347
|
+
const webhookUrl = opts.webhook ?? (opts.slack ? config.integrations?.slackWebhook : void 0);
|
|
1348
|
+
if (webhookUrl) {
|
|
1349
|
+
try {
|
|
1350
|
+
log.info("Posting to webhook\u2026");
|
|
1351
|
+
await postToWebhook(webhookUrl, summary);
|
|
1352
|
+
log.success("Posted.");
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
log.error(`Webhook failed: ${e.message}`);
|
|
1355
|
+
}
|
|
1356
|
+
} else if (opts.slack && !config.integrations?.slackWebhook) {
|
|
1357
|
+
log.warn("--slack requires integrations.slackWebhook to be set in config");
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1103
1360
|
function registerSummary(program2) {
|
|
1104
|
-
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) => {
|
|
1361
|
+
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) => {
|
|
1362
|
+
const config = readConfig();
|
|
1363
|
+
let provider;
|
|
1364
|
+
try {
|
|
1365
|
+
provider = getProvider(config);
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
log.error(e.message);
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
}
|
|
1370
|
+
if (opts.week) {
|
|
1371
|
+
const today = todayDate();
|
|
1372
|
+
const weekStart = startOfWeek(today);
|
|
1373
|
+
const days = eachDayOfRange(weekStart, today);
|
|
1374
|
+
const weeklyDays = [];
|
|
1375
|
+
for (const day of days) {
|
|
1376
|
+
const sessions2 = getOrBuildSessions(day);
|
|
1377
|
+
if (sessions2.length > 0) {
|
|
1378
|
+
weeklyDays.push({ date: day, sessions: sessions2.map((s) => sessionToPayload(s, day)) });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (weeklyDays.length === 0) {
|
|
1382
|
+
log.plain("No activity captured this week.");
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const cacheKey = isoWeekKey(today);
|
|
1386
|
+
const cacheFile = import_path4.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
1387
|
+
if (!opts.fresh && import_fs8.default.existsSync(cacheFile)) {
|
|
1388
|
+
const cached = import_fs8.default.readFileSync(cacheFile, "utf-8");
|
|
1389
|
+
log.plain("");
|
|
1390
|
+
log.plain(cached);
|
|
1391
|
+
log.plain("");
|
|
1392
|
+
log.plain(import_chalk6.default.dim(" (cached \u2014 use --fresh to regenerate)"));
|
|
1393
|
+
await handleWebhook(opts, config, cached);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const payload2 = { weekStart, weekEnd: today, days: weeklyDays };
|
|
1397
|
+
log.plain("");
|
|
1398
|
+
log.info(`Generating weekly summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
|
|
1399
|
+
try {
|
|
1400
|
+
const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload2) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((d) => d.sessions) });
|
|
1401
|
+
log.plain("");
|
|
1402
|
+
log.plain(summary);
|
|
1403
|
+
log.plain("");
|
|
1404
|
+
import_fs8.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
1405
|
+
import_fs8.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
1406
|
+
await handleWebhook(opts, config, summary);
|
|
1407
|
+
} catch (e) {
|
|
1408
|
+
log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
|
|
1409
|
+
process.exit(1);
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1105
1413
|
let date = todayDate();
|
|
1106
1414
|
if (opts.date) {
|
|
1107
1415
|
try {
|
|
@@ -1128,23 +1436,7 @@ function registerSummary(program2) {
|
|
|
1128
1436
|
return;
|
|
1129
1437
|
}
|
|
1130
1438
|
}
|
|
1131
|
-
const
|
|
1132
|
-
let provider;
|
|
1133
|
-
try {
|
|
1134
|
-
provider = getProvider(config);
|
|
1135
|
-
} catch (e) {
|
|
1136
|
-
log.error(e.message);
|
|
1137
|
-
process.exit(1);
|
|
1138
|
-
}
|
|
1139
|
-
const sessionPayloads = sessions.map((s) => ({
|
|
1140
|
-
date,
|
|
1141
|
-
project: s.repoName,
|
|
1142
|
-
branch: s.events[0]?.branch ?? null,
|
|
1143
|
-
startTime: formatDate(s.startedAt),
|
|
1144
|
-
endTime: formatDate(s.endedAt),
|
|
1145
|
-
durationMinutes: s.durationMinutes,
|
|
1146
|
-
events: s.events.map(eventRowToPayload)
|
|
1147
|
-
}));
|
|
1439
|
+
const sessionPayloads = sessions.map((s) => sessionToPayload(s, date));
|
|
1148
1440
|
const payload = { date, sessions: sessionPayloads };
|
|
1149
1441
|
log.plain("");
|
|
1150
1442
|
log.info(`Generating summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
|
|
@@ -1156,6 +1448,7 @@ function registerSummary(program2) {
|
|
|
1156
1448
|
if (sessions[0]) {
|
|
1157
1449
|
updateSessionSummary(sessions[0].id, summary);
|
|
1158
1450
|
}
|
|
1451
|
+
await handleWebhook(opts, config, summary);
|
|
1159
1452
|
} catch (e) {
|
|
1160
1453
|
log.error(`Summary failed [${config.ai.provider}]: ${e.message}`);
|
|
1161
1454
|
process.exit(1);
|
|
@@ -1164,8 +1457,8 @@ function registerSummary(program2) {
|
|
|
1164
1457
|
}
|
|
1165
1458
|
|
|
1166
1459
|
// src/cli/export.ts
|
|
1167
|
-
var
|
|
1168
|
-
var
|
|
1460
|
+
var import_fs9 = __toESM(require("fs"));
|
|
1461
|
+
var import_path5 = __toESM(require("path"));
|
|
1169
1462
|
function eventRowToPayload2(row) {
|
|
1170
1463
|
let metadata = {};
|
|
1171
1464
|
try {
|
|
@@ -1210,9 +1503,9 @@ function registerExport(program2) {
|
|
|
1210
1503
|
const payload = { date, sessions: sessionPayloads };
|
|
1211
1504
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
1212
1505
|
const summary = await provider.summarize(payload);
|
|
1213
|
-
const outputPath = opts.output ??
|
|
1214
|
-
if (!
|
|
1215
|
-
|
|
1506
|
+
const outputPath = opts.output ?? import_path5.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
|
|
1507
|
+
if (!import_fs9.default.existsSync(EXPORTS_DIR)) import_fs9.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
|
|
1508
|
+
import_fs9.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
1216
1509
|
|
|
1217
1510
|
${summary}
|
|
1218
1511
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -1249,7 +1542,7 @@ function registerConfig(program2) {
|
|
|
1249
1542
|
}
|
|
1250
1543
|
});
|
|
1251
1544
|
}
|
|
1252
|
-
var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["apiKey", "api_key", "secret", "token", "password", "passwd"]);
|
|
1545
|
+
var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["apiKey", "api_key", "secret", "token", "password", "passwd", "webhook", "slackWebhook"]);
|
|
1253
1546
|
function maskValue(key, value) {
|
|
1254
1547
|
if (SENSITIVE_KEYS.has(key) && typeof value === "string" && value.length > 0) {
|
|
1255
1548
|
return import_chalk7.default.dim(`${value.slice(0, 4)}${"*".repeat(8)} (${value.length} chars)`);
|
|
@@ -1270,29 +1563,58 @@ function printConfig(obj, indent) {
|
|
|
1270
1563
|
}
|
|
1271
1564
|
|
|
1272
1565
|
// src/cli/pause.ts
|
|
1273
|
-
var
|
|
1566
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1274
1567
|
function registerPause(program2) {
|
|
1275
1568
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
1276
|
-
if (
|
|
1569
|
+
if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
1277
1570
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
1278
1571
|
return;
|
|
1279
1572
|
}
|
|
1280
|
-
|
|
1573
|
+
import_fs10.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
1281
1574
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
1282
1575
|
});
|
|
1283
1576
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
1284
|
-
if (!
|
|
1577
|
+
if (!import_fs10.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
1285
1578
|
log.info("Capture is not paused.");
|
|
1286
1579
|
return;
|
|
1287
1580
|
}
|
|
1288
|
-
|
|
1581
|
+
import_fs10.default.rmSync(PAUSE_FLAG_PATH);
|
|
1289
1582
|
log.success("Capture resumed.");
|
|
1290
1583
|
});
|
|
1291
1584
|
}
|
|
1292
1585
|
|
|
1586
|
+
// src/cli/undo.ts
|
|
1587
|
+
function registerUndo(program2) {
|
|
1588
|
+
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) => {
|
|
1589
|
+
const count = parseInt(opts.count, 10);
|
|
1590
|
+
if (isNaN(count) || count < 1) {
|
|
1591
|
+
log.error("--count must be a positive integer");
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
const ids = getLastEventIds(count);
|
|
1595
|
+
if (ids.length === 0) {
|
|
1596
|
+
log.plain("No events to undo.");
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const events = getEventsByIds(ids);
|
|
1600
|
+
log.plain("");
|
|
1601
|
+
log.plain(`Events to remove (${events.length}):`);
|
|
1602
|
+
for (const e of events) {
|
|
1603
|
+
log.plain(` ${e.captured_at} ${e.category} ${e.command}`);
|
|
1604
|
+
}
|
|
1605
|
+
log.plain("");
|
|
1606
|
+
if (opts.dryRun) {
|
|
1607
|
+
log.plain("Dry run \u2014 nothing deleted.");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
deleteEventsByIds(ids);
|
|
1611
|
+
log.success(`Removed ${ids.length} event${ids.length > 1 ? "s" : ""}.`);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1293
1615
|
// src/index.ts
|
|
1294
1616
|
var program = new import_commander.Command();
|
|
1295
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
1617
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.2.0");
|
|
1296
1618
|
registerCapture(program);
|
|
1297
1619
|
registerInit(program);
|
|
1298
1620
|
registerStatus(program);
|
|
@@ -1302,5 +1624,6 @@ registerSummary(program);
|
|
|
1302
1624
|
registerExport(program);
|
|
1303
1625
|
registerConfig(program);
|
|
1304
1626
|
registerPause(program);
|
|
1627
|
+
registerUndo(program);
|
|
1305
1628
|
program.parse();
|
|
1306
1629
|
//# sourceMappingURL=index.js.map
|