@nightlab-agent/date-check 0.1.4 → 0.1.5
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 +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +4 -3
- package/hooks/register.tsx +40 -4
- package/hooks/stamps.ts +46 -9
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "date-check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
- A future date written as a release heading is refused even when the line the edit removes
|
|
6
|
+
named that date in a sentence ("Release planned for 2026-11-05" edited into
|
|
7
|
+
"## [1.0.0] - 2026-11-05"). Before, any new stamp whose text appeared anywhere in a removed
|
|
8
|
+
line was taken as carried over; now only a removed line's own stamp is.
|
|
9
|
+
- A new file named for a past day is refused in each folder. Before, the "write it again"
|
|
10
|
+
memory was kept by the file name alone, so a refusal for `log/2025-06-12.md` let a
|
|
11
|
+
different new `journal/2025-06-12.md` through unasked.
|
|
12
|
+
- A future stamp refused in a Write and written again by the shell with a relative path
|
|
13
|
+
(`echo ... >> notes.md`) now goes through as the refusal promises. Before, the retry was
|
|
14
|
+
matched by the path as typed, so the relative spelling was refused a second time. Both
|
|
15
|
+
checks now join a relative path to the session's directory and resolve `.` and `..`; on
|
|
16
|
+
Windows, `E:\p\notes.md` and a relative `notes.md` from `E:\p` are one file. A relative
|
|
17
|
+
path after a `cd` in the same command (in parentheses too) is the same file only after the
|
|
18
|
+
same `cd`. Network paths (`\\srv\share`) match in any letter case, as drive paths do.
|
|
19
|
+
- A date moved out of a code block into the text is judged. Before, the line counted as one
|
|
20
|
+
the file already had, so a sample heading in a fence turned into a real heading dated in
|
|
21
|
+
the future went through.
|
|
22
|
+
|
|
3
23
|
## 0.1.4 (2026-11-02)
|
|
4
24
|
|
|
5
25
|
- Every prompt now carries the clock, so Claude knows the time instead of guessing it: the
|
package/README.md
CHANGED
|
@@ -33,8 +33,9 @@ Seen on 2026-11-01 at 02:11 EST, same terminal. Asked to start a log line with
|
|
|
33
33
|
the current time and not to run commands, Claude did not guess: it asked for
|
|
34
34
|
the time. Then asked to add `- 00:40 backup started`, it wrote that line; the
|
|
35
35
|
note on the old stamp gave it the real time, and it wrote the line it had held
|
|
36
|
-
back as `- 02:11 release went out`.
|
|
37
|
-
|
|
36
|
+
back as `- 02:11 release went out`. Claude has been seen guessing a time on its
|
|
37
|
+
own (17:23, taken from a folder name, close enough to pass); date-check catching
|
|
38
|
+
a wrong guess Claude made on its own has not yet been seen on a screen.
|
|
38
39
|
|
|
39
40
|

|
|
40
41
|
|
|
@@ -101,7 +102,7 @@ was filled in from the clock as the file was written.
|
|
|
101
102
|
|
|
102
103
|
Settings (asked when you enable the plugin): minutes ahead allowed (2), minutes
|
|
103
104
|
behind before a note (30), days behind before a note (1), notes on old stamps
|
|
104
|
-
(on).
|
|
105
|
+
(on), the clock on every prompt (on), fill placeholders (on).
|
|
105
106
|
|
|
106
107
|

|
|
107
108
|
|
package/hooks/register.tsx
CHANGED
|
@@ -58,6 +58,40 @@ export const shellWrite = (command: string): { file: string; text: string } | un
|
|
|
58
58
|
return file === undefined ? undefined : { file, text: texts.join('\n') }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** The directory changes in a shell command (`cd a`, `(pushd b`): after one, a relative path means no one place. */
|
|
62
|
+
const dirMoves = (command: string) =>
|
|
63
|
+
[...command.matchAll(/(?:^|[;&|\n({]\s*)(cd|pushd)\s+([^;&|\n)}]*)/g)].map(m => `${m[1]} ${m[2]!.trim()}`)
|
|
64
|
+
const movesDir = (command: string) => dirMoves(command).length > 0
|
|
65
|
+
|
|
66
|
+
/** The file as one spelling, for remembering what was refused: a relative path joined to the
|
|
67
|
+
* session's directory, `.` and `..` resolved, one separator, a drive letter's path in lower
|
|
68
|
+
* case, a network path in lower case. A relative path after a `cd` in the same command is kept
|
|
69
|
+
* with the `cd`s before it, so it is one file only with the same moves; `~` and a failed reading
|
|
70
|
+
* stay as written. */
|
|
71
|
+
async function placed($: EngineInterface, file: string, moves: string[] = []): Promise<string> {
|
|
72
|
+
if (!file || file.startsWith('~')) return file
|
|
73
|
+
const absolute = /^(?:[\\/]|[A-Za-z]:[\\/])/.test(file)
|
|
74
|
+
if (!absolute && moves.length) return `${moves.join('; ')}; ${file}`
|
|
75
|
+
let full = file
|
|
76
|
+
if (!absolute) {
|
|
77
|
+
try {
|
|
78
|
+
full = `${await $.session.cwd()}/${file}`
|
|
79
|
+
} catch {
|
|
80
|
+
return file
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const drive = /^[A-Za-z]:/.exec(full)?.[0] ?? ''
|
|
84
|
+
const parts: string[] = []
|
|
85
|
+
for (const part of full.slice(drive.length).split(/[\\/]+/)) {
|
|
86
|
+
if (part === '' || part === '.') continue
|
|
87
|
+
if (part === '..') parts.pop()
|
|
88
|
+
else parts.push(part)
|
|
89
|
+
}
|
|
90
|
+
const network = /^[\\/]{2}/.test(full)
|
|
91
|
+
const one = `${network ? '//' : drive}/${parts.join('/')}`.replace(/^\/\/\//, '//')
|
|
92
|
+
return drive || network ? one.toLowerCase() : one
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
/** What a call changes in which file: the file's text before and after it. */
|
|
62
96
|
async function change($: EngineInterface, x: Call): Promise<{ file: string; before: string; after: string; created?: boolean }> {
|
|
63
97
|
const none = { file: '', before: '', after: '' }
|
|
@@ -80,7 +114,7 @@ async function change($: EngineInterface, x: Call): Promise<{ file: string; befo
|
|
|
80
114
|
if (!w) return none
|
|
81
115
|
// Whether the shell's file is new is only known when its path means one place: not under
|
|
82
116
|
// `~` (the engine does not expand it) and not after a `cd` in the same command.
|
|
83
|
-
const sure = !w.file.startsWith('~') &&
|
|
117
|
+
const sure = !w.file.startsWith('~') && !movesDir(x.command)
|
|
84
118
|
return { file: w.file, before: '', after: w.text, created: sure && !(await $.fs.exists(w.file)) }
|
|
85
119
|
}
|
|
86
120
|
return none
|
|
@@ -206,11 +240,13 @@ export const register: Register = (on, options: Options = {}) => {
|
|
|
206
240
|
name = undefined
|
|
207
241
|
}
|
|
208
242
|
// 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
|
|
243
|
+
const at = await placed($, file, x.tool === 'Bash' && typeof x.command === 'string' ? dirMoves(x.command) : [])
|
|
244
|
+
const key = (v: Verdict) => `${at}\n${unfill(v.stamp.line, filled)}`
|
|
210
245
|
const future = verdicts.filter(v => v.kind === 'future')
|
|
211
246
|
const fresh = future.filter(v => !((meant.get(key(v)) ?? Infinity) < began))
|
|
212
|
-
// By the
|
|
213
|
-
|
|
247
|
+
// By the placed path, so a retry spelled another way (relative, absolute) is the same file
|
|
248
|
+
// and a file of the same name in another folder is not.
|
|
249
|
+
const nameKey = `(name)\n${at}`
|
|
214
250
|
const freshName = name && !((meant.get(nameKey) ?? Infinity) < began) ? name : undefined
|
|
215
251
|
if (fresh.length || freshName) {
|
|
216
252
|
// The first refusal's number is kept: an older call refused late must not move it past a retry.
|
package/hooks/stamps.ts
CHANGED
|
@@ -106,27 +106,64 @@ const lineStamp = (line: string, fileDate?: string): Stamp | undefined => {
|
|
|
106
106
|
return undefined
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** For each line, whether it sits in a code fence that is closed (a fence left open to the end is not counted). */
|
|
110
|
+
const closedFences = (lines: string[]): boolean[] => {
|
|
111
|
+
const inside: boolean[] = []
|
|
112
|
+
let open = -1
|
|
113
|
+
for (const [i, l] of lines.entries()) {
|
|
114
|
+
if (/^\s*(```|~~~)/.test(l)) open = open < 0 ? i : -1
|
|
115
|
+
inside.push(open >= 0 || (/^\s*(```|~~~)/.test(l) && open < 0))
|
|
116
|
+
}
|
|
117
|
+
if (open >= 0) for (let i = open; i < lines.length; i++) inside[i] = false
|
|
118
|
+
return inside
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
/**
|
|
110
122
|
* The stamps a change adds: the stamps on lines of `after` that `before` did not have, read
|
|
111
123
|
* in the whole of `after` (so a line inside a code block the file already had is seen as
|
|
112
124
|
* code). A stamp whose text was on a line the change removes is carried over, not added.
|
|
113
125
|
*/
|
|
114
126
|
export const newStamps = (before: string, after: string, fileDate?: string): Stamp[] => {
|
|
127
|
+
// A line is the same line only in the same place: inside a closed code fence or out of one,
|
|
128
|
+
// so a sample moved out of a fence into the text is a new line, and closing a fence left open
|
|
129
|
+
// does not make every line under it new.
|
|
115
130
|
const had = new Map<string, number>()
|
|
116
|
-
|
|
131
|
+
const beforeIn = closedFences(before.split(/\r?\n/))
|
|
132
|
+
before.split(/\r?\n/).forEach((l, i) => {
|
|
133
|
+
const k = `${beforeIn[i] ? '`' : ' '}${l}`
|
|
134
|
+
had.set(k, (had.get(k) ?? 0) + 1)
|
|
135
|
+
})
|
|
117
136
|
const out: Stamp[] = []
|
|
137
|
+
const kept: string[] = []
|
|
138
|
+
const lines = after.split(/\r?\n/)
|
|
139
|
+
const afterIn = closedFences(lines)
|
|
118
140
|
let fence = false
|
|
119
|
-
for (const line of
|
|
120
|
-
const
|
|
141
|
+
for (const [i, line] of lines.entries()) {
|
|
142
|
+
const isFence = /^\s*(```|~~~)/.test(line)
|
|
143
|
+
if (isFence) fence = !fence
|
|
144
|
+
const k = `${afterIn[i] ? '`' : ' '}${line}`
|
|
145
|
+
const n = had.get(k) ?? 0
|
|
121
146
|
const isNew = n === 0
|
|
122
|
-
if (!isNew) had.set(
|
|
123
|
-
if (
|
|
124
|
-
if (fence ||
|
|
147
|
+
if (!isNew) had.set(k, n - 1)
|
|
148
|
+
if (isFence) continue
|
|
149
|
+
if (fence || indentedCode(line)) continue
|
|
125
150
|
const s = lineStamp(line, fileDate)
|
|
126
|
-
if (s) out.push(s)
|
|
151
|
+
if (s) (isNew ? out.push(s) : kept.push(s.text))
|
|
152
|
+
}
|
|
153
|
+
// Only a stamp the old text itself had carries over, read in the old text's own context (a
|
|
154
|
+
// date named in a removed sentence or code block is not one): the old text's stamps less those
|
|
155
|
+
// still on its unchanged lines.
|
|
156
|
+
const carried = new Map<string, number>()
|
|
157
|
+
if (before) {
|
|
158
|
+
for (const s of newStamps('', before, fileDate)) carried.set(s.text, (carried.get(s.text) ?? 0) + 1)
|
|
159
|
+
for (const s of kept) carried.set(s, (carried.get(s) ?? 0) - 1)
|
|
127
160
|
}
|
|
128
|
-
|
|
129
|
-
|
|
161
|
+
return out.filter(s => {
|
|
162
|
+
const n = carried.get(s.text) ?? 0
|
|
163
|
+
if (n <= 0) return true
|
|
164
|
+
carried.set(s.text, n - 1)
|
|
165
|
+
return false
|
|
166
|
+
})
|
|
130
167
|
}
|
|
131
168
|
|
|
132
169
|
/** The stamps in some lines of text. */
|
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.5",
|
|
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",
|