@nightlab-agent/date-check 0.1.3 → 0.1.4
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +13 -1
- package/CHANGELOG.md +17 -0
- package/README.md +41 -3
- package/hooks/register.tsx +80 -5
- package/hooks/stamps.ts +70 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "date-check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Checks the dates and times Claude Code writes into changelogs, logs, release notes and handoffs against the real clock before the write lands: a stamp in the future is refused with the real time, a stale one gets a note.",
|
|
5
5
|
"homepage": "https://github.com/nightlab-agent/date-check#readme",
|
|
6
6
|
"repository": "https://github.com/nightlab-agent/date-check",
|
|
@@ -39,6 +39,18 @@
|
|
|
39
39
|
"title": "Notes on old stamps",
|
|
40
40
|
"description": "Tell the agent when a stamp it adds is older than the limits above. Off: only stamps in the future are acted on.",
|
|
41
41
|
"default": true
|
|
42
|
+
},
|
|
43
|
+
"promptClock": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"title": "The clock on every prompt",
|
|
46
|
+
"description": "Each prompt carries the local weekday, date, time and zone, and how long ago your previous message came, so Claude has the real time instead of a guess. Off: nothing is added to prompts.",
|
|
47
|
+
"default": true
|
|
48
|
+
},
|
|
49
|
+
"fill": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"title": "Fill placeholders",
|
|
52
|
+
"description": "Replace ⟦NOW⟧, ⟦DATE⟧, ⟦TIME⟧ and ⟦NOW-ISO⟧ in what Claude writes with the local time as the write lands, so Claude need not type the time. Off: placeholders stay as written.",
|
|
53
|
+
"default": true
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
- Every prompt now carries the clock, so Claude knows the time instead of guessing it: the
|
|
6
|
+
local weekday, date, time, UTC offset and zone name, and how long ago your previous message
|
|
7
|
+
came (`it is Sunday 2026-11-01 05:58 local time, UTC-05:00 (America/New_York, EST). The
|
|
8
|
+
person's previous message came 3 h 12 min ago.`). A background task's notification or
|
|
9
|
+
another plugin's prompt gets the time but does not count as your previous message. The
|
|
10
|
+
"clock on every prompt" setting turns it off. Before, Claude Code gave Claude the date only,
|
|
11
|
+
with no time and no zone.
|
|
12
|
+
- Claude can leave the time to the clock: `⟦NOW⟧` (date and time), `⟦DATE⟧`, `⟦TIME⟧` or
|
|
13
|
+
`⟦NOW-ISO⟧` in what it writes (a Write or an Edit and its file name, a shell command) is replaced with the
|
|
14
|
+
machine's local time as the call lands, and Claude is told what went in. One inside
|
|
15
|
+
backticks or a code block is kept as written. The prompt clock tells Claude about `⟦NOW⟧`, `⟦DATE⟧`
|
|
16
|
+
and `⟦TIME⟧`; the "fill placeholders" setting turns them off. The names follow a request in
|
|
17
|
+
MikalaiKryvusha/KAIF#170, which found that a clock in the prompt still left an agent writing
|
|
18
|
+
the minute after.
|
|
19
|
+
|
|
3
20
|
## 0.1.3 (2026-11-01)
|
|
4
21
|
|
|
5
22
|
- A new Markdown or text file whose name carries a date more than a day before today
|
package/README.md
CHANGED
|
@@ -3,13 +3,21 @@
|
|
|
3
3
|
A Claude Code mod that checks the dates and times Claude writes into your files
|
|
4
4
|
against the real clock, before the write lands.
|
|
5
5
|
|
|
6
|
+
Install with one line at the Claude Code prompt (scopes, the shell form and npm
|
|
7
|
+
are under [Install](#install)):
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/plugin install date-check --marketplace nightlab-agent/date-check
|
|
11
|
+
```
|
|
12
|
+
|
|
6
13
|
Agents guess the time. In one day, one agent stamped a reply 15:50 at 15:33
|
|
7
14
|
and a note 16:05 at 16:01, though its notes told it to read the clock. People
|
|
8
15
|
ask for Claude to be told the time
|
|
9
16
|
([anthropics/claude-code#32913](https://github.com/anthropics/claude-code/issues/32913)),
|
|
10
17
|
and hooks that do that exist; none of the ones we found checks what Claude then
|
|
11
18
|
writes. A mod (a plugin that changes what Claude Code does) can read the real
|
|
12
|
-
clock; this one
|
|
19
|
+
clock; this one does both: it tells Claude the time with every prompt, and it
|
|
20
|
+
reads the clock again at the moment Claude writes a stamp.
|
|
13
21
|
|
|
14
22
|

|
|
15
23
|
|
|
@@ -28,6 +36,33 @@ note on the old stamp gave it the real time, and it wrote the line it had held
|
|
|
28
36
|
back as `- 02:11 release went out`. A stamp Claude guesses on its own has not
|
|
29
37
|
yet been seen on a screen.
|
|
30
38
|
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
Seen on 2026-11-01 at 05:54 EST, same terminal, with the clock on every
|
|
42
|
+
prompt (new in 0.1.4). The same request as above, to
|
|
43
|
+
start a log line with the current time and not run commands: this time Claude
|
|
44
|
+
wrote `05:54 Release went out.` and said the time came from the session
|
|
45
|
+
context. Asked what day and time it was, which zone, and how long ago the
|
|
46
|
+
previous message came, it answered Sunday 1 November, 05:54, America/New_York
|
|
47
|
+
on Eastern Standard Time, under a minute ago.
|
|
48
|
+
|
|
49
|
+
- Each prompt carries one line Claude reads and you do not see: the local
|
|
50
|
+
weekday, date, time, UTC offset and zone name, and how long ago your
|
|
51
|
+
previous message came. A background task's notification gets the time but
|
|
52
|
+
does not count as your message. The "clock on every prompt" setting turns it off.
|
|
53
|
+
- Claude can leave the time to the clock: `⟦NOW⟧` (date and time), `⟦DATE⟧`,
|
|
54
|
+
`⟦TIME⟧` or `⟦NOW-ISO⟧` in what it writes (a file and its name, an edit, a shell command)
|
|
55
|
+
is replaced with the machine's local time as the write lands, and Claude is
|
|
56
|
+
told what went in. One inside backticks or a code block is kept as written.
|
|
57
|
+
`⟦NOW⟧` has a space in it, so in a file name use `⟦DATE⟧`. The prompt clock
|
|
58
|
+
tells Claude about `⟦NOW⟧`, `⟦DATE⟧` and `⟦TIME⟧`; the "fill placeholders" setting turns them off.
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
Seen in a preview on 2026-11-01: asked to start a log line with the exact time
|
|
63
|
+
it is written, Claude wrote `06:14 The release went out.` and said the time
|
|
64
|
+
was filled in from the clock as the file was written.
|
|
65
|
+
|
|
31
66
|
- A stamp more than 2 minutes in the future is refused, and the refusal tells
|
|
32
67
|
Claude the real date and time. If the future date is meant (a deadline, a
|
|
33
68
|
planned release), Claude writes the same line again and it goes through.
|
|
@@ -86,11 +121,14 @@ year old and offered to rename it to `daily-brief-2026-11-01.md`.
|
|
|
86
121
|
|
|
87
122
|
## What it writes
|
|
88
123
|
|
|
89
|
-
|
|
90
|
-
lines. Nothing leaves your machine through this mod: it makes no network calls.
|
|
124
|
+
No files. It adds one line beside each prompt for Claude to read (the clock, above), and it
|
|
125
|
+
reads the file a Write or an Edit is about to change, to judge only the new lines. Nothing leaves your machine through this mod: it makes no network calls.
|
|
91
126
|
|
|
92
127
|
## How it works
|
|
93
128
|
|
|
129
|
+
- One hook, `prompt.submit`, which Claude Code runs as each prompt enters: it reads Claude
|
|
130
|
+
Code's clock and attaches the clock line to the prompt. A failure of the mod lets the
|
|
131
|
+
prompt through unchanged.
|
|
94
132
|
- One hook, `tool.call`, which Claude Code runs around each tool call (it can
|
|
95
133
|
stop the call first). On Write, Edit and Bash it finds the stamps the call adds, compares
|
|
96
134
|
each with Claude Code's clock, and either refuses the call with the real time
|
package/hooks/register.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EngineInterface, Register } from 'claude-code'
|
|
2
|
-
import { dateOfFile, dayOf, judge, judgeName, LIMITS, newStamps } from './stamps'
|
|
2
|
+
import { clockText, dateOfFile, dayOf, fillNow, gapText, judge, judgeName, LIMITS, newStamps, unfill } from './stamps'
|
|
3
3
|
import type { Limits, Verdict } from './stamps'
|
|
4
4
|
|
|
5
5
|
// date-check, the hooks module. Before a Write, an Edit, or a shell command that appends to
|
|
@@ -13,8 +13,13 @@ import type { Limits, Verdict } from './stamps'
|
|
|
13
13
|
// on 2026-11-01) is refused once with the real date, the same way; a later day is a plan.
|
|
14
14
|
// Lines the file already had are never judged: only what this call adds. Only Markdown and
|
|
15
15
|
// text files (isText): a date in code is data, not a stamp.
|
|
16
|
+
// And before the agent writes anything: each prompt carries the clock (promptClock), the local
|
|
17
|
+
// weekday, date, time and zone, and how long ago the person's previous message came, so the
|
|
18
|
+
// agent has the real time instead of a guess.
|
|
19
|
+
// And so the agent need not type a time at all: ⟦NOW⟧, ⟦DATE⟧, ⟦TIME⟧ and ⟦NOW-ISO⟧ in what a
|
|
20
|
+
// call writes are filled from the clock as it lands (fill; fillNow says which spellings).
|
|
16
21
|
|
|
17
|
-
type Options = { aheadMinutes?: unknown; staleMinutes?: unknown; staleDays?: unknown; staleNotes?: unknown }
|
|
22
|
+
type Options = { aheadMinutes?: unknown; staleMinutes?: unknown; staleDays?: unknown; staleNotes?: unknown; promptClock?: unknown; fill?: unknown }
|
|
18
23
|
type Call = { tool: string; file_path?: string; content?: string; old_string?: string; new_string?: string; replace_all?: boolean; command?: string }
|
|
19
24
|
|
|
20
25
|
const num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : d)
|
|
@@ -97,6 +102,31 @@ export const staleNote = (vs: Verdict[]) =>
|
|
|
97
102
|
`date-check: ${vs.map(quote).join('; ')} ${vs.length === 1 ? 'is' : 'are'} older than now (${vs[0]!.real}). ` +
|
|
98
103
|
`If ${vs.length === 1 ? 'it records' : 'they record'} something past, leave ${vs.length === 1 ? 'it' : 'them'}; if ${vs.length === 1 ? 'it was' : 'they were'} meant as now, correct ${vs.length === 1 ? 'it' : 'them'}.`
|
|
99
104
|
|
|
105
|
+
/** Prompts the person sent: typed, from Remote Control or Slack, or the SDK host's own turn. */
|
|
106
|
+
const PERSON = new Set(['composer', 'bridge', 'slack-ping', 'sdk'])
|
|
107
|
+
|
|
108
|
+
export const clockLine = (now: number, since?: number, fill = true) =>
|
|
109
|
+
`date-check: it is ${clockText(now)}.` +
|
|
110
|
+
(since === undefined ? '' : ` The person's previous message came ${gapText(now - since)} ago.`) +
|
|
111
|
+
(fill ? ' To record the moment a file is written, write ⟦NOW⟧ (date and time), ⟦DATE⟧ (also in a file name) or ⟦TIME⟧: the clock fills it in as the write lands.' : '')
|
|
112
|
+
|
|
113
|
+
/** The call with its placeholders filled from the clock (the text it writes, and a Write's or Edit's file path), and which ones were. */
|
|
114
|
+
const filledCall = (x: Call, now: number): { call: Call; filled: string[] } => {
|
|
115
|
+
const key = x.tool === 'Write' ? 'content' : x.tool === 'Edit' ? 'new_string' : 'command'
|
|
116
|
+
const call: Call = { ...x }
|
|
117
|
+
const filled = new Set<string>()
|
|
118
|
+
const v = x[key]
|
|
119
|
+
if (typeof v === 'string' && v.includes('⟦')) {
|
|
120
|
+
const f = fillNow(v, now, x.tool !== 'Bash')
|
|
121
|
+
if (f.filled.length) { call[key] = f.text; f.filled.forEach(t => filled.add(t)) }
|
|
122
|
+
}
|
|
123
|
+
if (x.tool !== 'Bash' && x.file_path?.includes('⟦')) {
|
|
124
|
+
const f = fillNow(x.file_path, now, false)
|
|
125
|
+
if (f.filled.length) { call.file_path = f.text; f.filled.forEach(t => filled.add(t)) }
|
|
126
|
+
}
|
|
127
|
+
return filled.size ? { call, filled: [...filled] } : { call: x, filled: [] }
|
|
128
|
+
}
|
|
129
|
+
|
|
100
130
|
export const register: Register = (on, options: Options = {}) => {
|
|
101
131
|
const limits: Limits = {
|
|
102
132
|
aheadMinutes: num(options.aheadMinutes, LIMITS.aheadMinutes),
|
|
@@ -104,16 +134,59 @@ export const register: Register = (on, options: Options = {}) => {
|
|
|
104
134
|
staleDays: num(options.staleDays, LIMITS.staleDays),
|
|
105
135
|
}
|
|
106
136
|
const notes = options.staleNotes !== false
|
|
137
|
+
const fill = options.fill !== false
|
|
107
138
|
// A future stamp refused once, by file and line: written again after that refusal, it is
|
|
108
139
|
// meant. Each refusal and each call's start take a number in order, so a call that began
|
|
109
140
|
// before the refusal (two calls at once) is refused too.
|
|
110
141
|
const meant = new Map<string, number>()
|
|
111
142
|
let order = 0
|
|
112
143
|
|
|
144
|
+
if (options.promptClock !== false) {
|
|
145
|
+
// The person's prompts in the order they entered, each with its clock reading once it
|
|
146
|
+
// came back. A prompt measures its gap from the latest earlier one: not from a later one
|
|
147
|
+
// whose reading came back first (two at once), and not from one dropped beneath.
|
|
148
|
+
const sent: { at?: number; dropped?: boolean }[] = []
|
|
149
|
+
on('prompt.submit', async ($, e, next) => {
|
|
150
|
+
const me = PERSON.has(e.origin?.kind) ? { at: undefined as number | undefined, dropped: false } : undefined
|
|
151
|
+
const earlier = sent.slice()
|
|
152
|
+
if (me) sent.push(me)
|
|
153
|
+
if (sent.length > 50) sent.splice(0, sent.length - 50)
|
|
154
|
+
let line: string | undefined
|
|
155
|
+
try {
|
|
156
|
+
const now = await $.clock.now()
|
|
157
|
+
let since: number | undefined
|
|
158
|
+
if (me) {
|
|
159
|
+
const before = earlier.filter(x => !x.dropped)
|
|
160
|
+
// One still waiting for its reading entered with this one: under a minute.
|
|
161
|
+
since = before.some(x => x.at === undefined) ? now : before.length ? Math.max(...before.map(x => x.at!)) : undefined
|
|
162
|
+
me.at = now
|
|
163
|
+
}
|
|
164
|
+
line = clockLine(now, since, fill)
|
|
165
|
+
} catch {
|
|
166
|
+
// A failure of this mod never stops the prompt; a prompt with no reading is no one's previous one.
|
|
167
|
+
if (me) me.dropped = true
|
|
168
|
+
}
|
|
169
|
+
const done = await next(line ? { ...e, context: [...(e.context ?? []), line] } : e)
|
|
170
|
+
if (me && done.drop !== undefined) me.dropped = true
|
|
171
|
+
return done
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
113
175
|
on('tool.call', async ($, e, next) => {
|
|
114
|
-
|
|
176
|
+
let x = e as unknown as Call
|
|
115
177
|
if (x.tool !== 'Write' && x.tool !== 'Edit' && x.tool !== 'Bash') return next(e)
|
|
116
178
|
const began = ++order
|
|
179
|
+
let filled: string[] = []
|
|
180
|
+
if (fill) {
|
|
181
|
+
try {
|
|
182
|
+
const f = filledCall(x, await $.clock.now())
|
|
183
|
+
x = f.call
|
|
184
|
+
filled = f.filled
|
|
185
|
+
} catch {
|
|
186
|
+
// No reading: the call goes on as written.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const sent = (filled.length ? { ...e, ...x } : e) as typeof e
|
|
117
190
|
let verdicts: Verdict[] = []
|
|
118
191
|
let name: ReturnType<typeof judgeName>
|
|
119
192
|
let file = ''
|
|
@@ -132,7 +205,8 @@ export const register: Register = (on, options: Options = {}) => {
|
|
|
132
205
|
verdicts = []
|
|
133
206
|
name = undefined
|
|
134
207
|
}
|
|
135
|
-
|
|
208
|
+
// Keyed by the line as the agent wrote it, so a retry with the same placeholder a minute later is the same line.
|
|
209
|
+
const key = (v: Verdict) => `${file}\n${unfill(v.stamp.line, filled)}`
|
|
136
210
|
const future = verdicts.filter(v => v.kind === 'future')
|
|
137
211
|
const fresh = future.filter(v => !((meant.get(key(v)) ?? Infinity) < began))
|
|
138
212
|
// By the file's name alone, so a retry spelled another way (relative, absolute) is the same file.
|
|
@@ -145,9 +219,10 @@ export const register: Register = (on, options: Options = {}) => {
|
|
|
145
219
|
if (freshName) mark(nameKey)
|
|
146
220
|
return { deny: [...(freshName ? [nameRefusal(file, freshName)] : []), ...(fresh.length ? [refusal(fresh)] : [])].join('\n') }
|
|
147
221
|
}
|
|
148
|
-
const done = await next(
|
|
222
|
+
const done = await next(sent)
|
|
149
223
|
if (done.deny !== undefined || done.isError) return done
|
|
150
224
|
const said = [
|
|
225
|
+
...(filled.length ? [`date-check: filled from the clock: ${filled.join(', ')}.`] : []),
|
|
151
226
|
...(name ? [`date-check: kept the date ${name.date} in the new file's name as written the second time.`] : []),
|
|
152
227
|
...(future.length ? [`date-check: kept ${future.map(v => `"${v.stamp.text}"`).join(', ')} as written the second time (a date in the future on purpose).`] : []),
|
|
153
228
|
...(notes && verdicts.some(v => v.kind === 'stale') ? [staleNote(verdicts.filter(v => v.kind === 'stale'))] : []),
|
package/hooks/stamps.ts
CHANGED
|
@@ -164,6 +164,76 @@ export const nowText = (ms: number) => {
|
|
|
164
164
|
return `${dayOf(ms)} ${pad(d.getHours())}:${pad(d.getMinutes())} local time, ${zone}`
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Placeholders the agent writes instead of typing a time, filled from the clock as the call
|
|
169
|
+
* lands: `⟦NOW⟧` 2026-11-01 06:20, `⟦DATE⟧` 2026-11-01, `⟦TIME⟧` 06:20, `⟦NOW-ISO⟧`
|
|
170
|
+
* 2026-11-01T06:20:41-05:00, all local. One in code is kept as written (a README that shows
|
|
171
|
+
* the placeholder): a code span, a ``` or ~~~ fence (closed by the same mark, as long or
|
|
172
|
+
* longer), and, in a file (`indented`), an indented code line, as the judge reads code.
|
|
173
|
+
* Exactly these spellings: `⟦now⟧`, `[NOW]` and `⟦NOW ⟧` are text. ⟦NOW⟧ has a space in
|
|
174
|
+
* it: in a file name or an unquoted shell word, ⟦DATE⟧ is the one to use.
|
|
175
|
+
*/
|
|
176
|
+
export const fillNow = (text: string, ms: number, indented = true): { text: string; filled: string[] } => {
|
|
177
|
+
const d = new Date(ms)
|
|
178
|
+
const o = -d.getTimezoneOffset()
|
|
179
|
+
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
180
|
+
const iso = `${dayOf(ms)}T${time}:${pad(d.getSeconds())}${o < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(o) / 60))}:${pad(Math.abs(o) % 60)}`
|
|
181
|
+
const value: Record<string, string> = { 'NOW-ISO': iso, NOW: `${dayOf(ms)} ${time}`, DATE: dayOf(ms), TIME: time }
|
|
182
|
+
const filled = new Set<string>()
|
|
183
|
+
let fence: string | undefined
|
|
184
|
+
const lines = text.split('\n').map(line => {
|
|
185
|
+
const mark = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1]
|
|
186
|
+
if (fence !== undefined) {
|
|
187
|
+
if (mark && mark[0] === fence[0] && mark.length >= fence.length && /^ {0,3}[`~]+\s*$/.test(line)) fence = undefined
|
|
188
|
+
return line
|
|
189
|
+
}
|
|
190
|
+
if (mark && !(mark[0] === '`' && line.slice(line.indexOf(mark) + mark.length).includes('`'))) { fence = mark; return line }
|
|
191
|
+
if (indented && indentedCode(line)) return line
|
|
192
|
+
return line.replace(/(?<!`)(`+)(?!`)[^]*?(?<!`)\1(?!`)|⟦(NOW-ISO|NOW|DATE|TIME)⟧/g, (m, code: string | undefined, tok: string | undefined) => {
|
|
193
|
+
if (code !== undefined || !tok) return m
|
|
194
|
+
filled.add(`⟦${tok}⟧ ${value[tok]}`)
|
|
195
|
+
return value[tok]!
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
return { text: lines.join('\n'), filled: [...filled] }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A line as written before filling: each filled value back to its placeholder, longest first. */
|
|
202
|
+
export const unfill = (line: string, filled: string[]) =>
|
|
203
|
+
filled.map(f => [f.slice(0, f.indexOf('⟧') + 1), f.slice(f.indexOf('⟧') + 2)] as const)
|
|
204
|
+
.sort((x, y) => y[1].length - x[1].length)
|
|
205
|
+
.reduce((l, [tok, v]) => l.split(v).join(tok), line)
|
|
206
|
+
|
|
207
|
+
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
|
208
|
+
/** The zone's name where the runtime knows it (`America/New_York, EST`); '' where it does not. */
|
|
209
|
+
const zoneName = (ms: number) => {
|
|
210
|
+
try {
|
|
211
|
+
const iana = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
212
|
+
const short = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).formatToParts(new Date(ms)).find(p => p.type === 'timeZoneName')?.value
|
|
213
|
+
return [...new Set([iana, short && !/^GMT[+-]/.test(short) ? short : ''].filter(Boolean))].join(', ')
|
|
214
|
+
} catch {
|
|
215
|
+
return ''
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The clock a prompt carries: weekday, date, local time and offset, and the zone's name:
|
|
220
|
+
* `Sunday 2026-11-01 05:58 local time, UTC-05:00 (America/New_York, EST)`.
|
|
221
|
+
*/
|
|
222
|
+
export const clockText = (ms: number) => {
|
|
223
|
+
const name = zoneName(ms)
|
|
224
|
+
return `${DAYS[new Date(ms).getDay()]} ${nowText(ms)}${name ? ` (${name})` : ''}`
|
|
225
|
+
}
|
|
226
|
+
/** A span of time as a person says it: `under a minute`, `12 min`, `3 h 5 min`, `2 days 4 h`. */
|
|
227
|
+
export const gapText = (ms: number) => {
|
|
228
|
+
const m = Math.floor(Math.max(0, ms) / 60000)
|
|
229
|
+
if (m < 1) return 'under a minute'
|
|
230
|
+
if (m < 60) return `${m} min`
|
|
231
|
+
const h = Math.floor(m / 60)
|
|
232
|
+
if (h < 24) return m % 60 ? `${h} h ${m % 60} min` : `${h} h`
|
|
233
|
+
const d = Math.floor(h / 24)
|
|
234
|
+
return `${d} day${d === 1 ? '' : 's'}${h % 24 ? ` ${h % 24} h` : ''}`
|
|
235
|
+
}
|
|
236
|
+
|
|
167
237
|
/** The local zone's UTC offset in minutes at a moment (New York in winter: -300). */
|
|
168
238
|
export type ZoneAt = (ms: number) => number
|
|
169
239
|
const localZone: ZoneAt = ms => -new Date(ms).getTimezoneOffset()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nightlab-agent/date-check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Claude Code mod: checks the dates and times Claude Code writes into changelogs, logs, release notes and handoffs against the real clock before the write lands; a stamp in the future is refused with the real time.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|