prjct-cli 1.6.13 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,12 +1,37 @@
1
1
  # Changelog
2
2
 
3
- ## [1.6.13] - 2026-02-07
3
+ ## [1.7.0] - 2026-02-07
4
4
 
5
- ### Refactoring
5
+ ### Features
6
+
7
+ - use relative timestamps to reduce token waste (PRJ-274) (#139)
8
+ - use relative timestamps to reduce token waste (PRJ-274)
9
+
10
+
11
+ ## [1.6.16] - 2026-02-07
12
+
13
+ ### Improvement
14
+ - **Use relative timestamps to reduce token waste (PRJ-274)**: Added `toRelative()` function using `date-fns` `formatDistanceToNowStrict`. Replaced raw ISO-8601 timestamps in Markdown context files (`now.md`, `ideas.md`, `shipped.md`) with human-readable relative time ("5 minutes ago", "3 days ago"). JSON storage retains full ISO timestamps — no data loss.
15
+
16
+ ### Implementation Details
17
+ Added `date-fns` as a dependency and created a thin `toRelative(date)` wrapper around `formatDistanceToNowStrict` in `core/utils/date-helper.ts`. Updated `toMarkdown()` in `state-storage.ts` (Started/Paused fields), `ideas-storage.ts` (all 3 sections: pending, converted, archived), and `shipped-storage.ts` (ship date per entry). 6 new unit tests added covering minutes, hours, days, months, Date objects, and ISO string inputs.
18
+
19
+ ### Learnings
20
+ - `date-fns` `formatDistanceToNowStrict` gives exact units ("5 minutes ago" not "about 5 minutes ago") — better for token efficiency
21
+ - Tests need `setSystemTime()` from `bun:test` since `formatDistanceToNowStrict` uses system clock internally
22
+
23
+ ### Test Plan
6
24
 
7
- - remove unused templates and dead code (PRJ-293) (#138)
8
- - remove unused templates and dead code (PRJ-293)
25
+ #### For QA
26
+ 1. Run `bun test core/__tests__/utils/date-helper.test.ts` verify all 55 tests pass (6 new for `toRelative`)
27
+ 2. Run `bun run build` — verify build succeeds
28
+ 3. Run `prjct sync` — verify `context/now.md` shows relative timestamps instead of raw ISO
29
+ 4. Check `ideas.md` and `shipped.md` for relative date format
9
30
 
31
+ #### For Users
32
+ **What changed:** Timestamps in context files now show "5 minutes ago", "3 days ago" instead of raw ISO-8601 strings.
33
+ **How to use:** No action needed — automatic.
34
+ **Breaking changes:** None.
10
35
 
11
36
  ## [1.6.15] - 2026-02-07
12
37
 
@@ -21,6 +21,7 @@ import {
21
21
  isToday,
22
22
  isWithinLastDays,
23
23
  parseDate,
24
+ toRelative,
24
25
  } from '../../utils/date-helper'
25
26
 
26
27
  describe('DateHelper', () => {
@@ -402,4 +403,47 @@ describe('DateHelper', () => {
402
403
  expect(original.getMinutes()).toBe(30)
403
404
  })
404
405
  })
406
+
407
+ describe('toRelative', () => {
408
+ it('should show minutes for dates within 1 hour', () => {
409
+ setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
410
+ const fiveMinAgo = '2025-10-15T11:55:00.000Z'
411
+ expect(toRelative(fiveMinAgo)).toBe('5 minutes ago')
412
+ setSystemTime()
413
+ })
414
+
415
+ it('should show hours for dates within 24 hours', () => {
416
+ setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
417
+ const threeHoursAgo = '2025-10-15T09:00:00.000Z'
418
+ expect(toRelative(threeHoursAgo)).toBe('3 hours ago')
419
+ setSystemTime()
420
+ })
421
+
422
+ it('should show days for dates within 7 days', () => {
423
+ setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
424
+ const twoDaysAgo = '2025-10-13T12:00:00.000Z'
425
+ expect(toRelative(twoDaysAgo)).toBe('2 days ago')
426
+ setSystemTime()
427
+ })
428
+
429
+ it('should show months for dates older than 30 days', () => {
430
+ setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
431
+ const twoMonthsAgo = '2025-08-15T12:00:00.000Z'
432
+ expect(toRelative(twoMonthsAgo)).toBe('2 months ago')
433
+ setSystemTime()
434
+ })
435
+
436
+ it('should accept Date objects', () => {
437
+ setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
438
+ const date = new Date('2025-10-15T11:00:00.000Z')
439
+ expect(toRelative(date)).toBe('1 hour ago')
440
+ setSystemTime()
441
+ })
442
+
443
+ it('should accept ISO string timestamps', () => {
444
+ setSystemTime(new Date('2025-10-15T12:00:00.000Z'))
445
+ expect(toRelative('2025-10-14T12:00:00.000Z')).toBe('1 day ago')
446
+ setSystemTime()
447
+ })
448
+ })
405
449
  })
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { generateUUID } from '../schemas'
9
9
  import type { Idea, IdeaPriority, IdeaStatus, IdeasJson } from '../types'
10
- import { getTimestamp } from '../utils/date-helper'
10
+ import { getTimestamp, toRelative } from '../utils/date-helper'
11
11
  import { StorageManager } from './storage-manager'
12
12
 
13
13
  class IdeasStorage extends StorageManager<IdeasJson> {
@@ -45,10 +45,10 @@ class IdeasStorage extends StorageManager<IdeasJson> {
45
45
  lines.push('## Brain Dump')
46
46
  if (pending.length > 0) {
47
47
  pending.forEach((idea) => {
48
- const date = idea.addedAt.split('T')[0]
48
+ const rel = toRelative(idea.addedAt)
49
49
  const tags = idea.tags.length > 0 ? ` ${idea.tags.map((t) => `#${t}`).join(' ')}` : ''
50
50
  const priority = idea.priority !== 'medium' ? ` [${idea.priority.toUpperCase()}]` : ''
51
- lines.push(`- ${idea.text}${priority} _(${date})_${tags}`)
51
+ lines.push(`- ${idea.text}${priority} _(${rel})_${tags}`)
52
52
  })
53
53
  } else {
54
54
  lines.push('_No pending ideas_')
@@ -59,9 +59,9 @@ class IdeasStorage extends StorageManager<IdeasJson> {
59
59
  if (converted.length > 0) {
60
60
  lines.push('## Converted')
61
61
  converted.forEach((idea) => {
62
- const date = idea.addedAt.split('T')[0]
62
+ const rel = toRelative(idea.addedAt)
63
63
  const feat = idea.convertedTo ? ` \u2192 ${idea.convertedTo}` : ''
64
- lines.push(`- \u2713 ${idea.text}${feat} _(${date})_`)
64
+ lines.push(`- \u2713 ${idea.text}${feat} _(${rel})_`)
65
65
  })
66
66
  lines.push('')
67
67
  }
@@ -70,8 +70,8 @@ class IdeasStorage extends StorageManager<IdeasJson> {
70
70
  if (archived.length > 0) {
71
71
  lines.push('## Archived')
72
72
  archived.forEach((idea) => {
73
- const date = idea.addedAt.split('T')[0]
74
- lines.push(`- ${idea.text} _(${date})_`)
73
+ const rel = toRelative(idea.addedAt)
74
+ lines.push(`- ${idea.text} _(${rel})_`)
75
75
  })
76
76
  lines.push('')
77
77
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { generateUUID } from '../schemas'
9
9
  import type { ShippedFeature, ShippedJson } from '../types'
10
- import { getTimestamp } from '../utils/date-helper'
10
+ import { getTimestamp, toRelative } from '../utils/date-helper'
11
11
  import { StorageManager } from './storage-manager'
12
12
 
13
13
  class ShippedStorage extends StorageManager<ShippedJson> {
@@ -72,13 +72,10 @@ class ShippedStorage extends StorageManager<ShippedJson> {
72
72
  .sort((a, b) => new Date(b.shippedAt).getTime() - new Date(a.shippedAt).getTime())
73
73
 
74
74
  ships.forEach((ship) => {
75
- const date = new Date(ship.shippedAt).toLocaleDateString('en-US', {
76
- month: 'short',
77
- day: 'numeric',
78
- })
75
+ const rel = toRelative(ship.shippedAt)
79
76
  const version = ship.version ? ` v${ship.version}` : ''
80
77
  const duration = ship.duration ? ` (${ship.duration})` : ''
81
- lines.push(`- **${ship.name}**${version}${duration} - ${date}`)
78
+ lines.push(`- **${ship.name}**${version}${duration} - ${rel}`)
82
79
  if (ship.description) {
83
80
  lines.push(` _${ship.description}_`)
84
81
  }
@@ -16,7 +16,7 @@ import type {
16
16
  Subtask,
17
17
  SubtaskSummary,
18
18
  } from '../schemas/state'
19
- import { getTimestamp } from '../utils/date-helper'
19
+ import { getTimestamp, toRelative } from '../utils/date-helper'
20
20
  import { md } from '../utils/markdown-builder'
21
21
  import { StorageManager } from './storage-manager'
22
22
 
@@ -52,7 +52,7 @@ class StateStorage extends StorageManager<StateJson> {
52
52
  const task = data.currentTask!
53
53
  m.bold(task.description)
54
54
  .blank()
55
- .raw(`Started: ${task.startedAt}`)
55
+ .raw(`Started: ${toRelative(task.startedAt)}`)
56
56
  .raw(`Session: ${task.sessionId}`)
57
57
  .maybe(task.featureId, (m, id) => m.raw(`Feature: ${id}`))
58
58
 
@@ -122,7 +122,7 @@ class StateStorage extends StorageManager<StateJson> {
122
122
  m.hr()
123
123
  .h2('Paused')
124
124
  .bold(prev.description)
125
- .raw(`Paused: ${prev.pausedAt}`)
125
+ .raw(`Paused: ${toRelative(prev.pausedAt)}`)
126
126
  .maybe(prev.pauseReason, (m, reason) => m.raw(`Reason: ${reason}`))
127
127
  .blank()
128
128
  .italic('Use /p:resume to continue')
@@ -7,6 +7,7 @@
7
7
  * - commands.ts (38+ inline date operations)
8
8
  */
9
9
 
10
+ import { formatDistanceToNowStrict } from 'date-fns'
10
11
  import type { DateComponents } from '../types'
11
12
 
12
13
  /**
@@ -164,3 +165,12 @@ export function getEndOfDay(date: Date): Date {
164
165
  result.setHours(23, 59, 59, 999)
165
166
  return result
166
167
  }
168
+
169
+ /**
170
+ * Convert a date/timestamp to a relative string (e.g. "5 minutes ago").
171
+ * Uses date-fns formatDistanceToNowStrict for accurate, token-friendly output.
172
+ */
173
+ export function toRelative(date: string | Date): string {
174
+ const d = typeof date === 'string' ? new Date(date) : date
175
+ return formatDistanceToNowStrict(d, { addSuffix: true })
176
+ }
@@ -667,8 +667,10 @@ __export(date_helper_exports, {
667
667
  getYearMonthDay: () => getYearMonthDay,
668
668
  isToday: () => isToday,
669
669
  isWithinLastDays: () => isWithinLastDays,
670
- parseDate: () => parseDate
670
+ parseDate: () => parseDate,
671
+ toRelative: () => toRelative
671
672
  });
673
+ import { formatDistanceToNowStrict } from "date-fns";
672
674
  function formatDate(date) {
673
675
  const year = date.getFullYear();
674
676
  const month = (date.getMonth() + 1).toString().padStart(2, "0");
@@ -755,6 +757,10 @@ function getEndOfDay(date) {
755
757
  result.setHours(23, 59, 59, 999);
756
758
  return result;
757
759
  }
760
+ function toRelative(date) {
761
+ const d = typeof date === "string" ? new Date(date) : date;
762
+ return formatDistanceToNowStrict(d, { addSuffix: true });
763
+ }
758
764
  var init_date_helper = __esm({
759
765
  "core/utils/date-helper.ts"() {
760
766
  "use strict";
@@ -774,6 +780,7 @@ var init_date_helper = __esm({
774
780
  __name(calculateDuration, "calculateDuration");
775
781
  __name(getStartOfDay, "getStartOfDay");
776
782
  __name(getEndOfDay, "getEndOfDay");
783
+ __name(toRelative, "toRelative");
777
784
  }
778
785
  });
779
786
 
@@ -10716,10 +10723,10 @@ var init_ideas_storage = __esm({
10716
10723
  lines.push("## Brain Dump");
10717
10724
  if (pending.length > 0) {
10718
10725
  pending.forEach((idea) => {
10719
- const date = idea.addedAt.split("T")[0];
10726
+ const rel = toRelative(idea.addedAt);
10720
10727
  const tags = idea.tags.length > 0 ? ` ${idea.tags.map((t) => `#${t}`).join(" ")}` : "";
10721
10728
  const priority = idea.priority !== "medium" ? ` [${idea.priority.toUpperCase()}]` : "";
10722
- lines.push(`- ${idea.text}${priority} _(${date})_${tags}`);
10729
+ lines.push(`- ${idea.text}${priority} _(${rel})_${tags}`);
10723
10730
  });
10724
10731
  } else {
10725
10732
  lines.push("_No pending ideas_");
@@ -10728,17 +10735,17 @@ var init_ideas_storage = __esm({
10728
10735
  if (converted.length > 0) {
10729
10736
  lines.push("## Converted");
10730
10737
  converted.forEach((idea) => {
10731
- const date = idea.addedAt.split("T")[0];
10738
+ const rel = toRelative(idea.addedAt);
10732
10739
  const feat = idea.convertedTo ? ` \u2192 ${idea.convertedTo}` : "";
10733
- lines.push(`- \u2713 ${idea.text}${feat} _(${date})_`);
10740
+ lines.push(`- \u2713 ${idea.text}${feat} _(${rel})_`);
10734
10741
  });
10735
10742
  lines.push("");
10736
10743
  }
10737
10744
  if (archived.length > 0) {
10738
10745
  lines.push("## Archived");
10739
10746
  archived.forEach((idea) => {
10740
- const date = idea.addedAt.split("T")[0];
10741
- lines.push(`- ${idea.text} _(${date})_`);
10747
+ const rel = toRelative(idea.addedAt);
10748
+ lines.push(`- ${idea.text} _(${rel})_`);
10742
10749
  });
10743
10750
  lines.push("");
10744
10751
  }
@@ -11466,13 +11473,10 @@ var init_shipped_storage = __esm({
11466
11473
  lines.push("");
11467
11474
  const ships = byMonth.get(month).sort((a, b) => new Date(b.shippedAt).getTime() - new Date(a.shippedAt).getTime());
11468
11475
  ships.forEach((ship) => {
11469
- const date = new Date(ship.shippedAt).toLocaleDateString("en-US", {
11470
- month: "short",
11471
- day: "numeric"
11472
- });
11476
+ const rel = toRelative(ship.shippedAt);
11473
11477
  const version = ship.version ? ` v${ship.version}` : "";
11474
11478
  const duration = ship.duration ? ` (${ship.duration})` : "";
11475
- lines.push(`- **${ship.name}**${version}${duration} - ${date}`);
11479
+ lines.push(`- **${ship.name}**${version}${duration} - ${rel}`);
11476
11480
  if (ship.description) {
11477
11481
  lines.push(` _${ship.description}_`);
11478
11482
  }
@@ -11848,7 +11852,7 @@ var init_state_storage = __esm({
11848
11852
  toMarkdown(data) {
11849
11853
  return md().h1("NOW").when(!!data.currentTask, (m) => {
11850
11854
  const task = data.currentTask;
11851
- m.bold(task.description).blank().raw(`Started: ${task.startedAt}`).raw(`Session: ${task.sessionId}`).maybe(task.featureId, (m2, id) => m2.raw(`Feature: ${id}`));
11855
+ m.bold(task.description).blank().raw(`Started: ${toRelative(task.startedAt)}`).raw(`Session: ${task.sessionId}`).maybe(task.featureId, (m2, id) => m2.raw(`Feature: ${id}`));
11852
11856
  if (task.subtasks && task.subtasks.length > 0) {
11853
11857
  m.blank().h2("Subtasks Progress").raw(
11854
11858
  `**Progress**: ${task.subtaskProgress?.completed || 0}/${task.subtaskProgress?.total || 0} (${task.subtaskProgress?.percentage || 0}%)`
@@ -11883,7 +11887,7 @@ var init_state_storage = __esm({
11883
11887
  }).when(!data.currentTask, (m) => {
11884
11888
  m.italic("No active task. Use /p:work to start.");
11885
11889
  }).maybe(data.previousTask, (m, prev) => {
11886
- m.hr().h2("Paused").bold(prev.description).raw(`Paused: ${prev.pausedAt}`).maybe(prev.pauseReason, (m2, reason2) => m2.raw(`Reason: ${reason2}`)).blank().italic("Use /p:resume to continue");
11890
+ m.hr().h2("Paused").bold(prev.description).raw(`Paused: ${toRelative(prev.pausedAt)}`).maybe(prev.pauseReason, (m2, reason2) => m2.raw(`Reason: ${reason2}`)).blank().italic("Use /p:resume to continue");
11887
11891
  }).blank().build();
11888
11892
  }
11889
11893
  // =========== Domain Methods ===========
@@ -28706,7 +28710,7 @@ var require_package = __commonJS({
28706
28710
  "package.json"(exports, module) {
28707
28711
  module.exports = {
28708
28712
  name: "prjct-cli",
28709
- version: "1.6.13",
28713
+ version: "1.7.0",
28710
28714
  description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
28711
28715
  main: "core/index.ts",
28712
28716
  bin: {
@@ -28761,6 +28765,7 @@ var require_package = __commonJS({
28761
28765
  "@linear/sdk": "^29.0.0",
28762
28766
  chalk: "^4.1.2",
28763
28767
  chokidar: "^5.0.0",
28768
+ "date-fns": "^4.1.0",
28764
28769
  esbuild: "^0.25.0",
28765
28770
  glob: "^13.0.1",
28766
28771
  hono: "^4.11.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "1.6.13",
3
+ "version": "1.7.0",
4
4
  "description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {
@@ -55,6 +55,7 @@
55
55
  "@linear/sdk": "^29.0.0",
56
56
  "chalk": "^4.1.2",
57
57
  "chokidar": "^5.0.0",
58
+ "date-fns": "^4.1.0",
58
59
  "esbuild": "^0.25.0",
59
60
  "glob": "^13.0.1",
60
61
  "hono": "^4.11.3",