@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/README.md +2 -0
- package/dist/index.js +670 -431
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/shell/hook.bash +1 -1
- package/shell/hook.zsh +1 -1
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
|
|
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
|
|
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
|
-
|
|
1393
|
-
log.
|
|
1394
|
-
|
|
1395
|
-
|
|
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(`
|
|
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 (
|
|
1698
|
-
log.error(
|
|
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:
|
|
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 (
|
|
2150
|
-
log.error(`Webhook failed: ${
|
|
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 (
|
|
2163
|
-
log.error(
|
|
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 (
|
|
2204
|
-
log.error(`Weekly summary failed [${config.ai.provider}]: ${
|
|
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 (
|
|
2214
|
-
log.error(
|
|
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 (
|
|
2251
|
-
const
|
|
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 (
|
|
2271
|
-
log.error(`Summary failed [${config.ai.provider}]: ${
|
|
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.
|
|
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
|