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 +29 -4
- package/core/__tests__/utils/date-helper.test.ts +44 -0
- package/core/storage/ideas-storage.ts +7 -7
- package/core/storage/shipped-storage.ts +3 -6
- package/core/storage/state-storage.ts +3 -3
- package/core/utils/date-helper.ts +10 -0
- package/dist/bin/prjct.mjs +20 -15
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.
|
|
3
|
+
## [1.7.0] - 2026-02-07
|
|
4
4
|
|
|
5
|
-
###
|
|
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
|
-
|
|
8
|
-
-
|
|
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
|
|
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} _(${
|
|
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
|
|
62
|
+
const rel = toRelative(idea.addedAt)
|
|
63
63
|
const feat = idea.convertedTo ? ` \u2192 ${idea.convertedTo}` : ''
|
|
64
|
-
lines.push(`- \u2713 ${idea.text}${feat} _(${
|
|
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
|
|
74
|
-
lines.push(`- ${idea.text} _(${
|
|
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
|
|
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} - ${
|
|
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
|
+
}
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -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
|
|
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} _(${
|
|
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
|
|
10738
|
+
const rel = toRelative(idea.addedAt);
|
|
10732
10739
|
const feat = idea.convertedTo ? ` \u2192 ${idea.convertedTo}` : "";
|
|
10733
|
-
lines.push(`- \u2713 ${idea.text}${feat} _(${
|
|
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
|
|
10741
|
-
lines.push(`- ${idea.text} _(${
|
|
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
|
|
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} - ${
|
|
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.
|
|
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.
|
|
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",
|