logdig 0.1.1 → 0.1.2
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/QUICKSTART.md +9 -5
- package/README.md +34 -11
- package/bin/logdig.js +70 -43
- package/package.json +1 -1
- package/src/cli-status.js +35 -0
- package/src/index.js +20 -19
- package/src/journal.js +74 -31
- package/src/pi-client.js +7 -4
- package/src/session-runner.js +126 -74
- package/src/settings.js +10 -0
- package/src/setup.js +8 -3
- package/src/transcript.js +13 -16
- package/src/work-blocks.js +99 -0
package/src/transcript.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
const READ_TOOLS = new Set([
|
|
@@ -45,12 +44,6 @@ export function sessionFromJsonl(text, sourcePath = "<session>") {
|
|
|
45
44
|
return parseSessionJsonl(text, sourcePath);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
export function fingerprintSession(session) {
|
|
49
|
-
return createHash("sha256")
|
|
50
|
-
.update(JSON.stringify([session.header, session.entries]))
|
|
51
|
-
.digest("hex");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
47
|
export function textContent(content) {
|
|
55
48
|
if (typeof content === "string") return content;
|
|
56
49
|
if (!Array.isArray(content)) return "";
|
|
@@ -69,7 +62,7 @@ export function redactAndClip(value, maxLength = 1800) {
|
|
|
69
62
|
return `${text.slice(0, maxLength)}\n[truncated]`;
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
function timestampOf(entry) {
|
|
65
|
+
export function timestampOf(entry) {
|
|
73
66
|
const value = entry.timestamp ?? entry.message?.timestamp;
|
|
74
67
|
if (typeof value === "number") return value;
|
|
75
68
|
if (typeof value === "string") {
|
|
@@ -121,8 +114,8 @@ export function sessionMetrics(session) {
|
|
|
121
114
|
return metrics;
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
function
|
|
125
|
-
|
|
117
|
+
export function localTimeFormatter(timeZone) {
|
|
118
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
126
119
|
timeZone,
|
|
127
120
|
year: "numeric",
|
|
128
121
|
month: "2-digit",
|
|
@@ -130,7 +123,11 @@ function localParts(timestamp, timeZone) {
|
|
|
130
123
|
hour: "2-digit",
|
|
131
124
|
minute: "2-digit",
|
|
132
125
|
hourCycle: "h23",
|
|
133
|
-
})
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function localParts(timestamp, formatter) {
|
|
130
|
+
const parts = formatter.formatToParts(new Date(timestamp));
|
|
134
131
|
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
135
132
|
return {
|
|
136
133
|
date: `${values.year}-${values.month}-${values.day}`,
|
|
@@ -172,10 +169,10 @@ function outputExcerpt(content, limit = 420) {
|
|
|
172
169
|
return `${text.slice(0, half)}\n[…output clipped…]\n${text.slice(-half)}`;
|
|
173
170
|
}
|
|
174
171
|
|
|
175
|
-
function eventFor(entry,
|
|
172
|
+
function eventFor(entry, formatter, toolCalls) {
|
|
176
173
|
const timestamp = timestampOf(entry);
|
|
177
174
|
if (timestamp === undefined) return undefined;
|
|
178
|
-
const { date, time } = localParts(timestamp,
|
|
175
|
+
const { date, time } = localParts(timestamp, formatter);
|
|
179
176
|
const message = entry.message;
|
|
180
177
|
|
|
181
178
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
@@ -251,14 +248,14 @@ function eventFor(entry, timeZone, toolCalls) {
|
|
|
251
248
|
return undefined;
|
|
252
249
|
}
|
|
253
250
|
|
|
254
|
-
export function eventsForSession(session, timeZone) {
|
|
251
|
+
export function eventsForSession(session, timeZone, extraction = { formatter: localTimeFormatter(timeZone), toolCalls: new Map() }) {
|
|
255
252
|
const events = [];
|
|
256
|
-
const toolCalls =
|
|
253
|
+
const { formatter, toolCalls } = extraction;
|
|
257
254
|
const cwd = session.header.cwd || "";
|
|
258
255
|
const project = (cwd ? path.basename(cwd) : "").replace(/^\d{4}-\d{2}-\d{2}-(?=.)/, "") || "unknown project";
|
|
259
256
|
|
|
260
257
|
for (const entry of session.entries) {
|
|
261
|
-
const generated = eventFor(entry,
|
|
258
|
+
const generated = eventFor(entry, formatter, toolCalls);
|
|
262
259
|
if (!generated) continue;
|
|
263
260
|
for (const event of Array.isArray(generated) ? generated : [generated]) {
|
|
264
261
|
events.push({
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { eventsForSession, localParts, localTimeFormatter, timestampOf } from "./transcript.js";
|
|
3
|
+
|
|
4
|
+
const CONTINUATION_GAP_MS = 4 * 60 * 60 * 1000;
|
|
5
|
+
const CONTEXT_LIMIT = 3200;
|
|
6
|
+
const CONTEXT_EVENTS = 6;
|
|
7
|
+
const ACTIVITY_ROLES = new Set(["user", "assistant", "toolResult", "bashExecution"]);
|
|
8
|
+
|
|
9
|
+
function precedingContext(events) {
|
|
10
|
+
const context = [];
|
|
11
|
+
let size = 0;
|
|
12
|
+
for (let index = events.length - 1; index >= 0 && context.length < CONTEXT_EVENTS; index--) {
|
|
13
|
+
const event = events[index];
|
|
14
|
+
const length = JSON.stringify(event).length;
|
|
15
|
+
if (size + length > CONTEXT_LIMIT) break;
|
|
16
|
+
context.unshift(event);
|
|
17
|
+
size += length;
|
|
18
|
+
}
|
|
19
|
+
return context;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function workBlocksForSession(session, timeZone) {
|
|
23
|
+
const formatter = localTimeFormatter(timeZone);
|
|
24
|
+
const blocks = [];
|
|
25
|
+
let current;
|
|
26
|
+
let lastActivity;
|
|
27
|
+
let userIndex = 0;
|
|
28
|
+
const pending = [];
|
|
29
|
+
|
|
30
|
+
for (const entry of session.entries) {
|
|
31
|
+
const contextSummary = ["compaction", "branch_summary"].includes(entry.type) && typeof entry.summary === "string" && entry.summary.trim();
|
|
32
|
+
const activity = (entry.type === "message" && ACTIVITY_ROLES.has(entry.message?.role)) || contextSummary;
|
|
33
|
+
const timestamp = activity ? timestampOf(entry) : undefined;
|
|
34
|
+
if (timestamp !== undefined) {
|
|
35
|
+
const { date, time } = localParts(timestamp, formatter);
|
|
36
|
+
const user = entry.message?.role === "user";
|
|
37
|
+
if (user) userIndex++;
|
|
38
|
+
if (!current || (user && date > current.date && timestamp - lastActivity >= CONTINUATION_GAP_MS)) {
|
|
39
|
+
const blockId = createHash("sha256")
|
|
40
|
+
.update(JSON.stringify([session.header.id, entry.id || [timestamp, userIndex]]))
|
|
41
|
+
.digest("hex");
|
|
42
|
+
current = {
|
|
43
|
+
...session,
|
|
44
|
+
header: blocks.length ? { ...session.header, timestamp: new Date(timestamp).toISOString() } : session.header,
|
|
45
|
+
entries: blocks.length ? [] : pending.splice(0),
|
|
46
|
+
blockId,
|
|
47
|
+
blockStart: new Date(timestamp).toISOString(),
|
|
48
|
+
date,
|
|
49
|
+
time,
|
|
50
|
+
timezone: timeZone,
|
|
51
|
+
activityDates: new Set(),
|
|
52
|
+
};
|
|
53
|
+
blocks.push(current);
|
|
54
|
+
}
|
|
55
|
+
current.activityDates.add(date);
|
|
56
|
+
lastActivity = Math.max(lastActivity ?? timestamp, timestamp);
|
|
57
|
+
}
|
|
58
|
+
if (current) current.entries.push(entry);
|
|
59
|
+
else pending.push(entry);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const journalable = [];
|
|
63
|
+
const extraction = { formatter, toolCalls: new Map() };
|
|
64
|
+
for (const block of blocks) {
|
|
65
|
+
const events = eventsForSession(block, timeZone, extraction);
|
|
66
|
+
if (!events.length) continue;
|
|
67
|
+
const previous = journalable.at(-1);
|
|
68
|
+
journalable.push({
|
|
69
|
+
...block,
|
|
70
|
+
blockIndex: journalable.length,
|
|
71
|
+
project: events[0].project,
|
|
72
|
+
events,
|
|
73
|
+
context: previous ? precedingContext(previous.events) : [],
|
|
74
|
+
previousBlockId: previous?.blockId,
|
|
75
|
+
activityDates: [...block.activityDates],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return journalable;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function blockInRange(block, range) {
|
|
82
|
+
return !range.firstDate || block.activityDates.some((date) => date >= range.firstDate && date <= range.lastDate);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Legacy entries used their last user timestamp. Blocks that disappear after a
|
|
86
|
+
// timezone change also need reassignment, without altering their linked notes.
|
|
87
|
+
export function assignLegacyBlocks(versions, blocks) {
|
|
88
|
+
if (!blocks.length) return;
|
|
89
|
+
const currentIds = new Set(blocks.map((block) => block.blockId));
|
|
90
|
+
for (const version of versions) {
|
|
91
|
+
if (currentIds.has(version.blockId)) continue;
|
|
92
|
+
const anchor = `${version.date} ${version.time}`;
|
|
93
|
+
const block = blocks.findLast((candidate) => version.blockStart
|
|
94
|
+
? candidate.blockStart <= version.blockStart
|
|
95
|
+
: `${candidate.date} ${candidate.time}` <= anchor) || blocks[0];
|
|
96
|
+
version.blockId = block.blockId;
|
|
97
|
+
version.legacy = true;
|
|
98
|
+
}
|
|
99
|
+
}
|