@workscribe/cli 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1402,6 +1402,42 @@ var init_session_lookup = __esm({
1402
1402
  }
1403
1403
  });
1404
1404
 
1405
+ // src/db/transaction.ts
1406
+ function isDatabaseLocked(error) {
1407
+ return error?.errcode === SQLITE_BUSY;
1408
+ }
1409
+ function rollbackQuietly(db) {
1410
+ try {
1411
+ db.exec("ROLLBACK");
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ function withImmediateTransaction(db, work, options = {}) {
1416
+ const { busyTimeoutMs } = options;
1417
+ if (busyTimeoutMs !== void 0) db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
1418
+ try {
1419
+ db.exec("BEGIN IMMEDIATE");
1420
+ try {
1421
+ const result = work();
1422
+ db.exec("COMMIT");
1423
+ return result;
1424
+ } catch (error) {
1425
+ rollbackQuietly(db);
1426
+ throw error;
1427
+ }
1428
+ } finally {
1429
+ if (busyTimeoutMs !== void 0) db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
1430
+ }
1431
+ }
1432
+ var BUSY_TIMEOUT_MS, SQLITE_BUSY;
1433
+ var init_transaction = __esm({
1434
+ "src/db/transaction.ts"() {
1435
+ "use strict";
1436
+ BUSY_TIMEOUT_MS = 5e3;
1437
+ SQLITE_BUSY = 5;
1438
+ }
1439
+ });
1440
+
1405
1441
  // src/db/index.ts
1406
1442
  function getDb() {
1407
1443
  if (_db) return _db;
@@ -1419,7 +1455,7 @@ function getDb() {
1419
1455
  }
1420
1456
  db.exec("PRAGMA journal_mode = WAL");
1421
1457
  db.exec("PRAGMA foreign_keys = ON");
1422
- db.exec("PRAGMA busy_timeout = 5000");
1458
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
1423
1459
  applyMigrations(db, MIGRATIONS);
1424
1460
  _db = db;
1425
1461
  return _db;
@@ -1475,10 +1511,30 @@ function getEventsForDate(date) {
1475
1511
  const stmt = db.prepare(`
1476
1512
  SELECT * FROM events
1477
1513
  WHERE date(captured_at, 'localtime') = $date
1478
- ORDER BY captured_at ASC
1514
+ ORDER BY captured_at ASC, id ASC
1479
1515
  `);
1480
1516
  return stmt.all({ date });
1481
1517
  }
1518
+ function countEventsBetween(start, end) {
1519
+ const row = getDb().prepare("SELECT COUNT(*) AS count FROM events WHERE captured_at >= $start AND captured_at < $end").get({ start, end });
1520
+ return row.count;
1521
+ }
1522
+ function sumSessionEventCounts(date) {
1523
+ const row = getDb().prepare("SELECT COALESCE(SUM(event_count), 0) AS total FROM sessions WHERE date = $date").get({ date });
1524
+ return row.total;
1525
+ }
1526
+ function getLatestFileSaveAt(filePath) {
1527
+ const row = getDb().prepare(
1528
+ `SELECT captured_at FROM events
1529
+ WHERE file_path = $file_path AND category = 'code_edit'
1530
+ ORDER BY captured_at DESC
1531
+ LIMIT 1`
1532
+ ).get({ file_path: filePath });
1533
+ return row?.captured_at ?? null;
1534
+ }
1535
+ function runInImmediateTransaction(work, options) {
1536
+ return withImmediateTransaction(getDb(), work, options);
1537
+ }
1482
1538
  function getEventCount() {
1483
1539
  const db = getDb();
1484
1540
  const stmt = db.prepare("SELECT COUNT(*) as count FROM events");
@@ -1507,7 +1563,7 @@ function getEventsBySessionId(sessionId) {
1507
1563
  SELECT e.* FROM events e
1508
1564
  INNER JOIN session_events se ON se.event_id = e.id
1509
1565
  WHERE se.session_id = $session_id
1510
- ORDER BY e.captured_at ASC
1566
+ ORDER BY e.captured_at ASC, e.id ASC
1511
1567
  `);
1512
1568
  return stmt.all({ session_id: sessionId });
1513
1569
  }
@@ -1600,6 +1656,7 @@ var init_db = __esm({
1600
1656
  init_migrations();
1601
1657
  init_migrate();
1602
1658
  init_session_lookup();
1659
+ init_transaction();
1603
1660
  _db = null;
1604
1661
  }
1605
1662
  });
@@ -3006,10 +3063,10 @@ function installIntoFolder(options) {
3006
3063
  const outcome = installSkill({
3007
3064
  sourceDir: options.sourceDir,
3008
3065
  skillsFolder: target.skillsFolder,
3009
- version: "2.0.1",
3066
+ version: "2.1.0",
3010
3067
  force: options.force
3011
3068
  });
3012
- return describeInstallOutcome({ ...target, outcome, version: "2.0.1" });
3069
+ return describeInstallOutcome({ ...target, outcome, version: "2.1.0" });
3013
3070
  } catch (error) {
3014
3071
  return describeInstallFailure({ ...target, error });
3015
3072
  }
@@ -3028,8 +3085,8 @@ function removeSkills(agents, options) {
3028
3085
  }
3029
3086
  function formatAgentSkillLine(agent, homeDir = import_os6.default.homedir()) {
3030
3087
  const location = displayPath(import_path14.default.join(homeDir, agent.skillsFolder, SKILL_NAME), homeDir);
3031
- const installState = readInstallState(import_path14.default.join(homeDir, agent.skillsFolder), "2.0.1");
3032
- const status = describeSkillStatus(installState, "2.0.1");
3088
+ const installState = readInstallState(import_path14.default.join(homeDir, agent.skillsFolder), "2.1.0");
3089
+ const status = describeSkillStatus(installState, "2.1.0");
3033
3090
  return `${agent.name.padEnd(AGENT_NAME_COLUMN_WIDTH)} ${STATUS_COLOURS[status.level](status.text)} ${import_chalk2.default.dim(location)}`;
3034
3091
  }
3035
3092
  function formatUndetectedAgentLine(agent) {
@@ -3315,10 +3372,10 @@ async function runAIToolsStep() {
3315
3372
  }
3316
3373
  async function offerSkillInstall(group, sourceDir) {
3317
3374
  const target = describeTarget(group);
3318
- const state = readInstallState(target.skillsFolder, "2.0.1");
3375
+ const state = readInstallState(target.skillsFolder, "2.1.0");
3319
3376
  const knownOutcome = outcomeWithoutPrompt(state);
3320
3377
  if (knownOutcome) {
3321
- printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "2.0.1" }));
3378
+ printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "2.1.0" }));
3322
3379
  return;
3323
3380
  }
3324
3381
  const { installSkill: shouldInstall } = await (0, import_prompts2.default)({
@@ -3478,6 +3535,12 @@ function localDateStr(d) {
3478
3535
  function localDateOf(isoString) {
3479
3536
  return localDateStr(new Date(isoString));
3480
3537
  }
3538
+ function localDayBounds(date) {
3539
+ const start = /* @__PURE__ */ new Date(`${date}T00:00:00`);
3540
+ const end = new Date(start);
3541
+ end.setDate(end.getDate() + 1);
3542
+ return { start: start.toISOString(), end: end.toISOString() };
3543
+ }
3481
3544
  function startOfWeek(dateStr) {
3482
3545
  const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
3483
3546
  const day = d.getDay();
@@ -3998,35 +4061,48 @@ var init_grouper = __esm({
3998
4061
  });
3999
4062
 
4000
4063
  // src/sessions/detector.ts
4001
- function getOrBuildSessions(date) {
4002
- const existing = getSessionsForDate(date);
4003
- const events = getEventsForDate(date);
4004
- if (events.length === 0) return [];
4005
- const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
4006
- if (existing.length > 0 && cachedEventCount === events.length) {
4007
- return existing.map((sessionRow) => ({
4008
- id: sessionRow.id,
4009
- date: sessionRow.date,
4010
- startedAt: sessionRow.started_at,
4011
- endedAt: sessionRow.ended_at,
4012
- repoName: sessionRow.repo_name,
4013
- durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
4014
- eventCount: sessionRow.event_count,
4015
- summary: sessionRow.summary,
4016
- events: getEventsBySessionId(sessionRow.id)
4017
- }));
4018
- }
4064
+ function isDayGrouped(date) {
4065
+ const { start, end } = localDayBounds(date);
4066
+ return countEventsBetween(start, end) === sumSessionEventCounts(date);
4067
+ }
4068
+ function regroupDay(date) {
4019
4069
  const savedSummaries = /* @__PURE__ */ new Map();
4020
- for (const s of existing) {
4021
- if (s.summary) savedSummaries.set(`${s.repo_name}|${s.started_at}`, s.summary);
4070
+ for (const sessionRow of getSessionsForDate(date)) {
4071
+ if (sessionRow.summary) savedSummaries.set(`${sessionRow.repo_name}|${sessionRow.started_at}`, sessionRow.summary);
4022
4072
  }
4023
4073
  clearSessionsForDate(date);
4024
- return buildSessions(date, events, savedSummaries);
4074
+ buildSessions(date, getEventsForDate(date), savedSummaries);
4075
+ }
4076
+ function ensureDayGrouped(date, options = {}) {
4077
+ if (isDayGrouped(date)) return;
4078
+ runInImmediateTransaction(
4079
+ () => {
4080
+ if (isDayGrouped(date)) return;
4081
+ regroupDay(date);
4082
+ },
4083
+ { busyTimeoutMs: options.lockWaitMs }
4084
+ );
4025
4085
  }
4026
- function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map()) {
4086
+ function toSession(sessionRow) {
4087
+ return {
4088
+ id: sessionRow.id,
4089
+ date: sessionRow.date,
4090
+ startedAt: sessionRow.started_at,
4091
+ endedAt: sessionRow.ended_at,
4092
+ repoName: sessionRow.repo_name,
4093
+ durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
4094
+ eventCount: sessionRow.event_count,
4095
+ summary: sessionRow.summary,
4096
+ events: getEventsBySessionId(sessionRow.id)
4097
+ };
4098
+ }
4099
+ function getOrBuildSessions(date) {
4100
+ ensureDayGrouped(date);
4101
+ return getSessionsForDate(date).map(toSession);
4102
+ }
4103
+ function buildSessions(date, events, savedSummaries) {
4027
4104
  const config = readConfig();
4028
4105
  const idleMs = config.session.idleTimeout * 60 * 1e3;
4029
- const sessions = [];
4030
4106
  const timeGroups = splitByIdleGap(events, idleMs);
4031
4107
  for (const group of timeGroups) {
4032
4108
  if (group.length === 0) continue;
@@ -4048,26 +4124,15 @@ function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map())
4048
4124
  for (const event of repoGroup) {
4049
4125
  insertSessionEvent(sessionId, event.id);
4050
4126
  }
4051
- sessions.push({
4052
- id: sessionId,
4053
- date,
4054
- startedAt: first.captured_at,
4055
- endedAt: last.captured_at,
4056
- repoName,
4057
- durationMinutes: minutesBetween(first.captured_at, last.captured_at),
4058
- eventCount: repoGroup.length,
4059
- summary,
4060
- events: repoGroup
4061
- });
4062
4127
  }
4063
4128
  }
4064
- return sessions;
4065
4129
  }
4066
4130
  var init_detector = __esm({
4067
4131
  "src/sessions/detector.ts"() {
4068
4132
  "use strict";
4069
4133
  init_db();
4070
4134
  init_config();
4135
+ init_format();
4071
4136
  init_grouper();
4072
4137
  }
4073
4138
  });
@@ -4182,18 +4247,18 @@ var init_session_summary = __esm({
4182
4247
  });
4183
4248
 
4184
4249
  // src/summary/git-commits.ts
4185
- function runGit(cwd, args) {
4250
+ function runGit(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
4186
4251
  try {
4187
- return (0, import_child_process6.execFileSync)("git", args, { cwd, timeout: GIT_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4252
+ return (0, import_child_process6.execFileSync)("git", args, { cwd, timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4188
4253
  } catch {
4189
4254
  return null;
4190
4255
  }
4191
4256
  }
4192
- function getRepoRoot(cwd) {
4193
- return runGit(cwd, ["rev-parse", "--show-toplevel"]) || null;
4257
+ function getRepoRoot(cwd, timeoutMs) {
4258
+ return runGit(cwd, ["rev-parse", "--show-toplevel"], timeoutMs) || null;
4194
4259
  }
4195
- function getUserEmail(repoRoot) {
4196
- return runGit(repoRoot, ["config", "user.email"]) || null;
4260
+ function getUserEmail(repoRoot, timeoutMs) {
4261
+ return runGit(repoRoot, ["config", "user.email"], timeoutMs) || null;
4197
4262
  }
4198
4263
  function getOthersSubjects(repoRoot, author) {
4199
4264
  if (!author) return [];
@@ -4211,15 +4276,20 @@ function getOthersSubjects(repoRoot, author) {
4211
4276
  }
4212
4277
  return [...theirs].filter((subject) => !mine.has(subject));
4213
4278
  }
4214
- function getCommitsInWindow({ repoRoot, since, until, author }) {
4279
+ function getCommitsInWindow({ repoRoot, since, until, author, filePath, timeoutMs }) {
4215
4280
  const shiftedBy = (iso, milliseconds) => new Date(new Date(iso).getTime() + milliseconds).toISOString();
4216
- const output = runGit(repoRoot, [
4217
- "log",
4218
- `--since=${shiftedBy(since, -WINDOW_LEAD_MS)}`,
4219
- `--until=${shiftedBy(until, WINDOW_TRAIL_MS)}`,
4220
- "--format=%h%x09%ce%x09%s",
4221
- "--reverse"
4222
- ]);
4281
+ const output = runGit(
4282
+ repoRoot,
4283
+ [
4284
+ "log",
4285
+ `--since=${shiftedBy(since, -WINDOW_LEAD_MS)}`,
4286
+ `--until=${shiftedBy(until, WINDOW_TRAIL_MS)}`,
4287
+ "--format=%h%x09%ce%x09%s",
4288
+ "--reverse",
4289
+ ...filePath ? ["--", filePath] : []
4290
+ ],
4291
+ timeoutMs
4292
+ );
4223
4293
  if (!output) return [];
4224
4294
  const wantedAuthor = author?.toLowerCase() ?? null;
4225
4295
  return output.split("\n").flatMap((line) => {
@@ -4230,11 +4300,38 @@ function getCommitsInWindow({ repoRoot, since, until, author }) {
4230
4300
  return [{ hash, subject }];
4231
4301
  });
4232
4302
  }
4233
- var import_child_process6, GIT_TIMEOUT_MS, WINDOW_LEAD_MS, WINDOW_TRAIL_MS, AUTHORSHIP_HISTORY_DEPTH;
4303
+ function getFileCommits(options) {
4304
+ const now = options.now ?? Date.now;
4305
+ const deadline = now() + options.budgetMs;
4306
+ const timeLeft = () => {
4307
+ const milliseconds = deadline - now();
4308
+ return milliseconds >= 1 ? milliseconds : null;
4309
+ };
4310
+ const rootTimeout = timeLeft();
4311
+ if (rootTimeout === null) return { repoRoot: null, commits: [] };
4312
+ const repoRoot = getRepoRoot(import_path17.default.dirname(options.filePath), rootTimeout);
4313
+ if (!repoRoot) return { repoRoot: null, commits: [] };
4314
+ const emailTimeout = timeLeft();
4315
+ if (emailTimeout === null) return { repoRoot, commits: [] };
4316
+ const author = getUserEmail(repoRoot, emailTimeout);
4317
+ const logTimeout = timeLeft();
4318
+ if (logTimeout === null) return { repoRoot, commits: [] };
4319
+ const commits = getCommitsInWindow({
4320
+ repoRoot,
4321
+ since: options.since,
4322
+ until: options.until,
4323
+ author,
4324
+ filePath: options.filePath,
4325
+ timeoutMs: logTimeout
4326
+ });
4327
+ return { repoRoot, commits };
4328
+ }
4329
+ var import_child_process6, import_path17, GIT_TIMEOUT_MS, WINDOW_LEAD_MS, WINDOW_TRAIL_MS, AUTHORSHIP_HISTORY_DEPTH;
4234
4330
  var init_git_commits = __esm({
4235
4331
  "src/summary/git-commits.ts"() {
4236
4332
  "use strict";
4237
4333
  import_child_process6 = require("child_process");
4334
+ import_path17 = __toESM(require("path"));
4238
4335
  GIT_TIMEOUT_MS = 3e3;
4239
4336
  WINDOW_LEAD_MS = 12e4;
4240
4337
  WINDOW_TRAIL_MS = 6e4;
@@ -4853,7 +4950,7 @@ function registerExport(program) {
4853
4950
  }
4854
4951
  const payload = buildDayPayload(date, sessions, locationContextFrom(config));
4855
4952
  const summary = await createSummaryWriter(config).summarize(payload);
4856
- const outputPath = options.output ?? import_path17.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
4953
+ const outputPath = options.output ?? import_path18.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
4857
4954
  if (!import_fs21.default.existsSync(EXPORTS_DIR)) import_fs21.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
4858
4955
  import_fs21.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
4859
4956
 
@@ -4862,12 +4959,12 @@ ${summary}
4862
4959
  log.success(`Saved: ${outputPath}`);
4863
4960
  });
4864
4961
  }
4865
- var import_fs21, import_path17;
4962
+ var import_fs21, import_path18;
4866
4963
  var init_export = __esm({
4867
4964
  "src/cli/export.ts"() {
4868
4965
  "use strict";
4869
4966
  import_fs21 = __toESM(require("fs"));
4870
- import_path17 = __toESM(require("path"));
4967
+ import_path18 = __toESM(require("path"));
4871
4968
  init_detector();
4872
4969
  init_config();
4873
4970
  init_logger();
@@ -5081,7 +5178,7 @@ var init_clear = __esm({
5081
5178
 
5082
5179
  // src/cli/uninstall.ts
5083
5180
  function completionRcFiles() {
5084
- return [".zshrc", ".bashrc", ".bash_profile"].map((file) => import_path18.default.join(import_os11.default.homedir(), file));
5181
+ return [".zshrc", ".bashrc", ".bash_profile"].map((file) => import_path19.default.join(import_os11.default.homedir(), file));
5085
5182
  }
5086
5183
  function removeCompletionsAndToolHooks() {
5087
5184
  for (const rcFile of completionRcFiles()) {
@@ -5098,10 +5195,10 @@ function removeCompletionsAndToolHooks() {
5098
5195
  }
5099
5196
  function detectShellRcFiles() {
5100
5197
  const candidates = [
5101
- { name: "zsh", rcFile: import_path18.default.join(import_os11.default.homedir(), ".zshrc") },
5102
- { name: "bash", rcFile: import_path18.default.join(import_os11.default.homedir(), ".bashrc") },
5103
- { name: "bash", rcFile: import_path18.default.join(import_os11.default.homedir(), ".bash_profile") },
5104
- { name: "fish", rcFile: import_path18.default.join(import_os11.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
5198
+ { name: "zsh", rcFile: import_path19.default.join(import_os11.default.homedir(), ".zshrc") },
5199
+ { name: "bash", rcFile: import_path19.default.join(import_os11.default.homedir(), ".bashrc") },
5200
+ { name: "bash", rcFile: import_path19.default.join(import_os11.default.homedir(), ".bash_profile") },
5201
+ { name: "fish", rcFile: import_path19.default.join(import_os11.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
5105
5202
  ];
5106
5203
  return candidates.filter((candidate) => import_fs23.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
5107
5204
  }
@@ -5158,12 +5255,12 @@ function registerUninstall(program) {
5158
5255
  log.plain("");
5159
5256
  });
5160
5257
  }
5161
- var import_fs23, import_path18, import_os11, import_prompts4, import_chalk10;
5258
+ var import_fs23, import_path19, import_os11, import_prompts4, import_chalk10;
5162
5259
  var init_uninstall = __esm({
5163
5260
  "src/cli/uninstall.ts"() {
5164
5261
  "use strict";
5165
5262
  import_fs23 = __toESM(require("fs"));
5166
- import_path18 = __toESM(require("path"));
5263
+ import_path19 = __toESM(require("path"));
5167
5264
  import_os11 = __toESM(require("os"));
5168
5265
  import_prompts4 = __toESM(require("prompts"));
5169
5266
  import_chalk10 = __toESM(require("chalk"));
@@ -5220,7 +5317,7 @@ function registerProfile(program) {
5220
5317
  log.plain(import_chalk11.default.dim(" " + "\u2500".repeat(60)));
5221
5318
  for (const entry of entries) {
5222
5319
  const badge = activeNames.has(entry.name) ? import_chalk11.default.green(" [active]") : "";
5223
- const installed = import_fs24.default.existsSync(import_path19.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk11.default.dim(" installed") : "";
5320
+ const installed = import_fs24.default.existsSync(import_path20.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk11.default.dim(" installed") : "";
5224
5321
  log.plain(` ${import_chalk11.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
5225
5322
  if (entry.description) log.plain(` ${import_chalk11.default.dim(" ".repeat(18) + entry.description)}`);
5226
5323
  }
@@ -5256,7 +5353,7 @@ function registerProfile(program) {
5256
5353
  continue;
5257
5354
  }
5258
5355
  activeNames.delete(name);
5259
- const cachePath = import_path19.default.join(PROFILES_DIR, `${name}.json`);
5356
+ const cachePath = import_path20.default.join(PROFILES_DIR, `${name}.json`);
5260
5357
  if (import_fs24.default.existsSync(cachePath)) import_fs24.default.unlinkSync(cachePath);
5261
5358
  log.success(`Profile "${name}" removed`);
5262
5359
  }
@@ -5384,12 +5481,12 @@ function registerProfile(program) {
5384
5481
  }
5385
5482
  });
5386
5483
  }
5387
- var import_fs24, import_path19, import_chalk11, MIN_SUGGEST_COUNT;
5484
+ var import_fs24, import_path20, import_chalk11, MIN_SUGGEST_COUNT;
5388
5485
  var init_profile = __esm({
5389
5486
  "src/cli/profile.ts"() {
5390
5487
  "use strict";
5391
5488
  import_fs24 = __toESM(require("fs"));
5392
- import_path19 = __toESM(require("path"));
5489
+ import_path20 = __toESM(require("path"));
5393
5490
  import_chalk11 = __toESM(require("chalk"));
5394
5491
  init_config();
5395
5492
  init_paths();
@@ -5409,6 +5506,9 @@ function realPath(target) {
5409
5506
  return null;
5410
5507
  }
5411
5508
  }
5509
+ function resolvedPath(target) {
5510
+ return realPath(target) ?? target;
5511
+ }
5412
5512
  function currentDirectory(where = { pwd: process.env.PWD, cwd: process.cwd() }) {
5413
5513
  if (where.pwd && realPath(where.pwd) === realPath(where.cwd)) return where.pwd;
5414
5514
  return where.cwd;
@@ -5416,15 +5516,15 @@ function currentDirectory(where = { pwd: process.env.PWD, cwd: process.cwd() })
5416
5516
  function rootForms(cwd, root) {
5417
5517
  const resolvedCwd = realPath(cwd);
5418
5518
  if (!resolvedCwd) return [root];
5419
- const reached = import_path20.default.resolve(cwd, import_path20.default.relative(resolvedCwd, root));
5519
+ const reached = import_path21.default.resolve(cwd, import_path21.default.relative(resolvedCwd, root));
5420
5520
  return [.../* @__PURE__ */ new Set([root, reached])];
5421
5521
  }
5422
- var import_fs25, import_path20;
5522
+ var import_fs25, import_path21;
5423
5523
  var init_working_directory = __esm({
5424
5524
  "src/utils/working-directory.ts"() {
5425
5525
  "use strict";
5426
5526
  import_fs25 = __toESM(require("fs"));
5427
- import_path20 = __toESM(require("path"));
5527
+ import_path21 = __toESM(require("path"));
5428
5528
  }
5429
5529
  });
5430
5530
 
@@ -5505,9 +5605,9 @@ function redactConfig() {
5505
5605
  function registerBugReport(program) {
5506
5606
  program.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
5507
5607
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5508
- const reportPath = import_path21.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
5608
+ const reportPath = import_path22.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
5509
5609
  const systemInfo = [
5510
- `Workscribe version : ${"2.0.1"}`,
5610
+ `Workscribe version : ${"2.1.0"}`,
5511
5611
  `Node.js version : ${process.version}`,
5512
5612
  `Platform : ${process.platform}`,
5513
5613
  `Architecture : ${process.arch}`,
@@ -5538,8 +5638,8 @@ function registerBugReport(program) {
5538
5638
  process.exit(1);
5539
5639
  }
5540
5640
  const params = new URLSearchParams({
5541
- title: `Bug report \u2014 workscribe ${"2.0.1"}`,
5542
- body: `<!-- Attach ${import_path21.default.basename(reportPath)} or paste the relevant sections below -->
5641
+ title: `Bug report \u2014 workscribe ${"2.1.0"}`,
5642
+ body: `<!-- Attach ${import_path22.default.basename(reportPath)} or paste the relevant sections below -->
5543
5643
 
5544
5644
  **Describe the issue:**
5545
5645
 
@@ -5556,13 +5656,13 @@ function registerBugReport(program) {
5556
5656
  log.plain("");
5557
5657
  });
5558
5658
  }
5559
- var import_fs26, import_os12, import_path21, import_chalk12, GITHUB_ISSUES_URL, DEBUG_LOG_TAIL_LINES;
5659
+ var import_fs26, import_os12, import_path22, import_chalk12, GITHUB_ISSUES_URL, DEBUG_LOG_TAIL_LINES;
5560
5660
  var init_bug_report = __esm({
5561
5661
  "src/cli/bug-report.ts"() {
5562
5662
  "use strict";
5563
5663
  import_fs26 = __toESM(require("fs"));
5564
5664
  import_os12 = __toESM(require("os"));
5565
- import_path21 = __toESM(require("path"));
5665
+ import_path22 = __toESM(require("path"));
5566
5666
  import_chalk12 = __toESM(require("chalk"));
5567
5667
  init_paths();
5568
5668
  init_config();
@@ -5629,11 +5729,108 @@ var init_alerts3 = __esm({
5629
5729
  }
5630
5730
  });
5631
5731
 
5732
+ // src/summary/file-details.ts
5733
+ function displayPath2(absoluteFilePath, repoRoot) {
5734
+ if (repoRoot) {
5735
+ const relative = import_path23.default.relative(repoRoot, absoluteFilePath);
5736
+ if (relative && !relative.startsWith("..") && !import_path23.default.isAbsolute(relative)) return relative;
5737
+ }
5738
+ return import_path23.default.basename(absoluteFilePath);
5739
+ }
5740
+ function otherFilesBySaves(sessionEvents, filePath) {
5741
+ const saves = /* @__PURE__ */ new Map();
5742
+ for (const event of sessionEvents) {
5743
+ if (event.category !== CODE_EDIT || !event.file_path || event.file_path === filePath) continue;
5744
+ const seen = saves.get(event.file_path);
5745
+ saves.set(event.file_path, {
5746
+ count: (seen?.count ?? 0) + 1,
5747
+ lastSavedAt: seen && seen.lastSavedAt > event.captured_at ? seen.lastSavedAt : event.captured_at
5748
+ });
5749
+ }
5750
+ return [...saves.entries()].sort(([, first], [, second]) => second.count - first.count || second.lastSavedAt.localeCompare(first.lastSavedAt)).map(([otherPath]) => otherPath);
5751
+ }
5752
+ function savesOf(sessionEvents, filePath) {
5753
+ return sessionEvents.filter((event) => event.category === CODE_EDIT && event.file_path === filePath);
5754
+ }
5755
+ function buildFileDetails(options) {
5756
+ const { filePath, sessionEvents, displayPathOf } = options;
5757
+ const saves = savesOf(sessionEvents, filePath);
5758
+ if (saves.length === 0) return null;
5759
+ const savedTimes = saves.map((save) => save.captured_at).sort();
5760
+ const latestBranch = [...saves].reverse().find((save) => save.branch)?.branch ?? null;
5761
+ const otherFiles = otherFilesBySaves(sessionEvents, filePath);
5762
+ return {
5763
+ path: filePath,
5764
+ relativePath: displayPathOf(filePath),
5765
+ firstSavedAt: savedTimes[0],
5766
+ lastSavedAt: savedTimes.at(-1) ?? savedTimes[0],
5767
+ saveCount: saves.length,
5768
+ branch: latestBranch ?? options.sessionBranch,
5769
+ commits: options.commits,
5770
+ editedAlongside: {
5771
+ files: otherFiles.slice(0, EDITED_ALONGSIDE_LIMIT).map(displayPathOf),
5772
+ total: otherFiles.length
5773
+ }
5774
+ };
5775
+ }
5776
+ var import_path23, EDITED_ALONGSIDE_LIMIT, CODE_EDIT;
5777
+ var init_file_details = __esm({
5778
+ "src/summary/file-details.ts"() {
5779
+ "use strict";
5780
+ import_path23 = __toESM(require("path"));
5781
+ EDITED_ALONGSIDE_LIMIT = 5;
5782
+ CODE_EDIT = "code_edit";
5783
+ }
5784
+ });
5785
+
5786
+ // src/cli/context-file.ts
5787
+ function groupDayOfLatestSave(filePath) {
5788
+ const latestSave = getLatestFileSaveAt(filePath);
5789
+ if (!latestSave) return;
5790
+ try {
5791
+ ensureDayGrouped(localDateOf(latestSave), { lockWaitMs: LOCK_WAIT_MS });
5792
+ } catch (error) {
5793
+ if (!isDatabaseLocked(error)) throw error;
5794
+ }
5795
+ }
5796
+ function getFileDetails(filePath, session) {
5797
+ const sessionEvents = getEventsBySessionId(session.id);
5798
+ if (savesOf(sessionEvents, filePath).length === 0) return null;
5799
+ const { repoRoot, commits } = getFileCommits({
5800
+ filePath: resolvedPath(filePath),
5801
+ since: session.started_at,
5802
+ until: session.ended_at,
5803
+ budgetMs: GIT_BUDGET_MS
5804
+ });
5805
+ return buildFileDetails({
5806
+ filePath,
5807
+ sessionEvents,
5808
+ sessionBranch: getSessionBranch(session.id),
5809
+ commits,
5810
+ displayPathOf: (storedPath) => displayPath2(resolvedPath(storedPath), repoRoot)
5811
+ });
5812
+ }
5813
+ var LOCK_WAIT_MS, GIT_BUDGET_MS;
5814
+ var init_context_file = __esm({
5815
+ "src/cli/context-file.ts"() {
5816
+ "use strict";
5817
+ init_db();
5818
+ init_transaction();
5819
+ init_detector();
5820
+ init_file_details();
5821
+ init_git_commits();
5822
+ init_format();
5823
+ init_working_directory();
5824
+ LOCK_WAIT_MS = 1e3;
5825
+ GIT_BUDGET_MS = 2e3;
5826
+ }
5827
+ });
5828
+
5632
5829
  // src/cli/context.ts
5633
5830
  function sessionMinutes(session) {
5634
5831
  return (new Date(session.ended_at).getTime() - new Date(session.started_at).getTime()) / 6e4;
5635
5832
  }
5636
- function toSession(row) {
5833
+ function toSession2(row) {
5637
5834
  return {
5638
5835
  id: row.id,
5639
5836
  date: row.date,
@@ -5648,7 +5845,7 @@ function toSession(row) {
5648
5845
  }
5649
5846
  function renderTemplateSummary(row) {
5650
5847
  const config = readConfig();
5651
- const payload = sessionToPayload(toSession(row), row.date, locationContextFrom(config));
5848
+ const payload = sessionToPayload(toSession2(row), row.date, locationContextFrom(config));
5652
5849
  const bullets = describeSession(payload, config.summary?.includeCategories ?? []);
5653
5850
  return `${formatSessionHeading(payload)}
5654
5851
  ${toBulletList(bullets)}`;
@@ -5658,6 +5855,7 @@ function resolveContextSummary(row) {
5658
5855
  }
5659
5856
  function toContextJson(session) {
5660
5857
  return {
5858
+ repoName: session.repo_name,
5661
5859
  date: session.date,
5662
5860
  startedAt: session.started_at,
5663
5861
  endedAt: session.ended_at,
@@ -5668,6 +5866,9 @@ function toContextJson(session) {
5668
5866
  summary: resolveContextSummary(session)
5669
5867
  };
5670
5868
  }
5869
+ function toFileContextJson(session, filePath) {
5870
+ return { ...toContextJson(session), file: getFileDetails(filePath, session) };
5871
+ }
5671
5872
  function printSessionContext(session, heading) {
5672
5873
  const ago = timeAgo(session.started_at);
5673
5874
  const branch = getSessionBranch(session.id);
@@ -5688,7 +5889,8 @@ function printSessionContext(session, heading) {
5688
5889
  function registerContext(program) {
5689
5890
  program.command("context").description("Show the last session for the current repo").option("--file <path>", "narrow to last session that touched this file").option("--json", "output as JSON instead of formatted text \u2014 null when no session is found").action((opts) => {
5690
5891
  if (opts.file) {
5691
- const normalizedFile = import_path22.default.normalize(opts.file);
5892
+ const normalizedFile = import_path24.default.normalize(opts.file);
5893
+ groupDayOfLatestSave(normalizedFile);
5692
5894
  const session = getLastSessionTouchingFile(normalizedFile);
5693
5895
  if (!session) {
5694
5896
  if (opts.json) log.plain(JSON.stringify(null));
@@ -5696,10 +5898,10 @@ function registerContext(program) {
5696
5898
  return;
5697
5899
  }
5698
5900
  if (opts.json) {
5699
- log.plain(JSON.stringify(toContextJson(session)));
5901
+ log.plain(JSON.stringify(toFileContextJson(session, normalizedFile)));
5700
5902
  return;
5701
5903
  }
5702
- printSessionContext(session, `Last session touching ${import_path22.default.basename(normalizedFile)}`);
5904
+ printSessionContext(session, `Last session touching ${import_path24.default.basename(normalizedFile)}`);
5703
5905
  return;
5704
5906
  }
5705
5907
  const config = readConfig();
@@ -5725,11 +5927,11 @@ function registerContext(program) {
5725
5927
  printSessionContext(sessions[0], `Last session on ${repoName}`);
5726
5928
  });
5727
5929
  }
5728
- var import_path22, import_chalk14;
5930
+ var import_path24, import_chalk14;
5729
5931
  var init_context = __esm({
5730
5932
  "src/cli/context.ts"() {
5731
5933
  "use strict";
5732
- import_path22 = __toESM(require("path"));
5934
+ import_path24 = __toESM(require("path"));
5733
5935
  import_chalk14 = __toESM(require("chalk"));
5734
5936
  init_db();
5735
5937
  init_git();
@@ -5738,6 +5940,7 @@ var init_context = __esm({
5738
5940
  init_config();
5739
5941
  init_format();
5740
5942
  init_logger();
5943
+ init_context_file();
5741
5944
  init_describe();
5742
5945
  init_utils3();
5743
5946
  init_payload();
@@ -5843,7 +6046,7 @@ function isNewer(latest, current) {
5843
6046
  return latestPatch > currentPatch;
5844
6047
  }
5845
6048
  function checkForUpdate() {
5846
- const current = "2.0.1";
6049
+ const current = "2.1.0";
5847
6050
  const cache = readCache();
5848
6051
  if (cache && isNewer(cache.latestVersion, current)) {
5849
6052
  process.stderr.write(
@@ -5866,7 +6069,7 @@ function checkForUpdate() {
5866
6069
  function buildTelemetryPayload(config) {
5867
6070
  return {
5868
6071
  installId: config.telemetry.installId,
5869
- cliVersion: "2.0.1",
6072
+ cliVersion: "2.1.0",
5870
6073
  platform: import_os13.default.platform(),
5871
6074
  nodeVersion: process.version
5872
6075
  };
@@ -5942,7 +6145,7 @@ var require_main = __commonJS({
5942
6145
  pingTelemetry();
5943
6146
  }
5944
6147
  var program = new import_commander.Command();
5945
- program.name("workscribe").description("Automatic developer work journal").version("2.0.1");
6148
+ program.name("workscribe").description("Automatic developer work journal").version("2.1.0");
5946
6149
  registerCapture(program);
5947
6150
  registerHook(program);
5948
6151
  registerInit(program);