logdig 0.1.1 → 0.2.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/QUICKSTART.md +23 -5
- package/README.md +81 -16
- package/bin/logdig.js +128 -70
- package/package.json +1 -1
- package/src/backfill-progress.js +108 -0
- package/src/cli-status.js +60 -0
- package/src/config-menu.js +135 -0
- package/src/index.js +25 -22
- package/src/journal.js +98 -39
- package/src/pi-client.js +7 -4
- package/src/session-runner.js +173 -84
- package/src/settings.js +46 -5
- package/src/setup.js +16 -3
- package/src/transcript.js +13 -16
- package/src/work-blocks.js +101 -0
package/bin/logdig.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { listJournaledSessions } from "../src/journal.js";
|
|
5
|
+
import { listJournaledSessions, summaryCachePolicy } from "../src/journal.js";
|
|
6
6
|
import { collectSessions, parseBackfillArgument, writeSessions } from "../src/session-runner.js";
|
|
7
7
|
import { createPiModelClient } from "../src/pi-client.js";
|
|
8
8
|
import { spawnPiProcess } from "../src/pi-process.js";
|
|
9
9
|
import { environmentOverrides, loadSettings, validateSettings } from "../src/settings.js";
|
|
10
10
|
import { checkDirectory } from "../src/directories.js";
|
|
11
11
|
import { configureLogDig } from "../src/setup.js";
|
|
12
|
+
import { editConfig } from "../src/config-menu.js";
|
|
13
|
+
import { statusPrefix, statusPrefixWidth } from "../src/cli-status.js";
|
|
14
|
+
import { createBackfillProgress } from "../src/backfill-progress.js";
|
|
12
15
|
|
|
13
16
|
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
14
17
|
const relativeEntry = path.relative(process.cwd(), fileURLToPath(import.meta.url)).split(path.sep).join("/");
|
|
@@ -70,6 +73,7 @@ function runPiCommand(command, args, { cwd = process.cwd(), inherit = false, tim
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
async function showConfig() {
|
|
76
|
+
if (process.stdin.isTTY && process.stdout.isTTY) return editConfig(commandName);
|
|
73
77
|
const settings = await loadSettings();
|
|
74
78
|
const { filePath, configured, ...values } = settings;
|
|
75
79
|
console.log(`Settings: ${filePath}${configured ? "" : " (not created yet)"}`);
|
|
@@ -82,29 +86,35 @@ async function doctor() {
|
|
|
82
86
|
const settings = await loadSettings();
|
|
83
87
|
let issues = 0;
|
|
84
88
|
let warnings = 0;
|
|
85
|
-
|
|
86
|
-
const overrides = environmentOverrides();
|
|
87
|
-
if (overrides.length) console.log(`Environment overrides: ${overrides.join(", ")}`);
|
|
88
|
-
|
|
89
|
-
for (const [label, directory, writable] of [
|
|
89
|
+
const checks = [
|
|
90
90
|
["Session history", settings.sessionDirectory, false],
|
|
91
91
|
["Summary cache", settings.cacheDirectory, true],
|
|
92
92
|
["Daily notes", settings.dailyDirectory, true],
|
|
93
|
-
]
|
|
93
|
+
];
|
|
94
|
+
const statusWidth = statusPrefixWidth();
|
|
95
|
+
const labelWidth = Math.max(...checks.map(([label]) => label.length), "Pi".length);
|
|
96
|
+
const valueColumn = statusWidth + labelWidth + 2;
|
|
97
|
+
const formatLabel = (label, prefixWidth = statusWidth) => `${label}:${" ".repeat(valueColumn - prefixWidth - label.length - 1)}`;
|
|
98
|
+
|
|
99
|
+
console.log(`${statusPrefix("ok")}${formatLabel("Settings")}${settings.filePath}${settings.configured ? " (saved)" : " (not saved; using defaults/environment)"}`);
|
|
100
|
+
const overrides = environmentOverrides();
|
|
101
|
+
if (overrides.length) console.log(`Environment overrides: ${overrides.join(", ")}`);
|
|
102
|
+
|
|
103
|
+
for (const [label, directory, writable] of checks) {
|
|
94
104
|
if (!directory) {
|
|
95
|
-
console.error(
|
|
105
|
+
console.error(`${statusPrefix("error")}${formatLabel(label)}not configured. Run '${commandName} init' to choose a folder.`);
|
|
96
106
|
issues++;
|
|
97
107
|
continue;
|
|
98
108
|
}
|
|
99
109
|
try {
|
|
100
110
|
const result = await checkDirectory(directory, { writable, allowMissing: writable });
|
|
101
|
-
console.log(
|
|
111
|
+
console.log(`${statusPrefix("ok")}${formatLabel(label)}${directory}${result.exists ? "" : " (will be created on a real backfill)"}`);
|
|
102
112
|
} catch (error) {
|
|
103
113
|
if (label === "Session history" && error.code === "ENOENT") {
|
|
104
|
-
console.warn(
|
|
114
|
+
console.warn(`${statusPrefix("warning")}${formatLabel(label)}${directory} (not found yet). Start a saved Pi session, or choose its location with '${commandName} init'.`);
|
|
105
115
|
warnings++;
|
|
106
116
|
} else {
|
|
107
|
-
console.error(
|
|
117
|
+
console.error(`${statusPrefix("error")}${formatLabel(label)}${directory} (${error.message}). Check folder permissions or choose another folder with '${commandName} init'.`);
|
|
108
118
|
issues++;
|
|
109
119
|
}
|
|
110
120
|
}
|
|
@@ -112,12 +122,14 @@ async function doctor() {
|
|
|
112
122
|
|
|
113
123
|
try {
|
|
114
124
|
const pi = await runPiCommand(settings.piCommand, ["--version", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-context-files", "--offline"]);
|
|
115
|
-
console.log(
|
|
125
|
+
console.log(`${statusPrefix("ok")}${formatLabel("Pi")}${pi.stdout || pi.stderr || "available"}`);
|
|
116
126
|
} catch (error) {
|
|
117
|
-
console.error(
|
|
127
|
+
console.error(`${statusPrefix("error")}${formatLabel("Pi")}${error.message}. Install Pi or set its executable in '${commandName} init' (advanced settings).`);
|
|
118
128
|
issues++;
|
|
119
129
|
}
|
|
120
130
|
console.log(`\nSummary model: ${settings.model || "Pi startup default"}`);
|
|
131
|
+
console.log(`Thinking: ${settings.thinkingLevel || "Pi startup default"}`);
|
|
132
|
+
console.log(`Parallel sessions: ${settings.concurrency}`);
|
|
121
133
|
console.log("Authentication is checked only when you request a summary. If it fails, open Pi and run /login.");
|
|
122
134
|
if (issues) {
|
|
123
135
|
console.error(`\n${issues} thing${issues === 1 ? "" : "s"} to fix. Address the FIX lines above, then run '${commandName} doctor' again.`);
|
|
@@ -131,16 +143,25 @@ async function backfill(args) {
|
|
|
131
143
|
const settings = await loadSettings();
|
|
132
144
|
let daysArgument;
|
|
133
145
|
let dryRun = false;
|
|
146
|
+
let skipToday = false;
|
|
134
147
|
let selectedModel = settings.model;
|
|
148
|
+
let selectedThinking = settings.thinkingLevel;
|
|
135
149
|
for (let index = 0; index < args.length; index++) {
|
|
136
150
|
const arg = args[index];
|
|
137
151
|
if (arg === "--dry-run") {
|
|
138
152
|
dryRun = true;
|
|
153
|
+
} else if (arg === "--skip-today") {
|
|
154
|
+
skipToday = true;
|
|
139
155
|
} else if (arg === "--model" || arg === "-m") {
|
|
140
156
|
selectedModel = args[++index];
|
|
141
157
|
if (!selectedModel || selectedModel.startsWith("-")) throw new Error("--model requires provider/model, or 'default' to use Pi's startup model");
|
|
142
158
|
} else if (arg.startsWith("--model=")) {
|
|
143
159
|
selectedModel = arg.slice("--model=".length);
|
|
160
|
+
} else if (arg === "--thinking") {
|
|
161
|
+
selectedThinking = args[++index];
|
|
162
|
+
if (!selectedThinking || selectedThinking.startsWith("-")) throw new Error("--thinking requires default, off, minimal, low, medium, high, xhigh, or max");
|
|
163
|
+
} else if (arg.startsWith("--thinking=")) {
|
|
164
|
+
selectedThinking = arg.slice("--thinking=".length);
|
|
144
165
|
} else if (!arg.startsWith("-") && daysArgument === undefined) {
|
|
145
166
|
daysArgument = arg;
|
|
146
167
|
} else {
|
|
@@ -148,19 +169,23 @@ async function backfill(args) {
|
|
|
148
169
|
}
|
|
149
170
|
}
|
|
150
171
|
selectedModel = selectedModel?.trim().toLowerCase() === "default" ? undefined : selectedModel;
|
|
151
|
-
|
|
172
|
+
const generationSettings = validateSettings({ ...settings, model: selectedModel, thinkingLevel: selectedThinking });
|
|
173
|
+
selectedModel = generationSettings.model;
|
|
174
|
+
selectedThinking = generationSettings.thinkingLevel;
|
|
152
175
|
const range = parseBackfillArgument(daysArgument);
|
|
153
176
|
requireJournalPaths(settings);
|
|
154
177
|
|
|
155
|
-
console.log(`${dryRun ? "Preview" : "Backfill"}: ${range.all ? "all saved sessions" : `last ${range.days} day${range.days === 1 ? "" : "s"}`} (${settings.timeZone})`);
|
|
178
|
+
console.log(`${dryRun ? "Preview" : "Backfill"}: ${range.all ? "all saved sessions" : `last ${range.days}${skipToday ? " complete" : ""} day${range.days === 1 ? "" : "s"}`} (${settings.timeZone})${skipToday ? " · skipping today" : ""}`);
|
|
156
179
|
console.log(`Daily notes: ${settings.dailyDirectory} (${settings.dailyHeader}, ${settings.dailySummary})`);
|
|
157
180
|
console.log(`Summary cache: ${path.join(settings.cacheDirectory, "Sessions")}`);
|
|
158
181
|
console.log(`Model: ${selectedModel || "Pi startup default"}`);
|
|
182
|
+
console.log(`Thinking: ${selectedThinking || "Pi startup default"}`);
|
|
183
|
+
console.log(`Parallel sessions: ${settings.concurrency}`);
|
|
159
184
|
console.log(dryRun
|
|
160
185
|
? "Dry run: no model requests, no file changes, and no folders created."
|
|
161
|
-
: "Only new or changed
|
|
162
|
-
const found = await collectSessions({ sessionDirectory: settings.sessionDirectory, timeZone: settings.timeZone, days: range.days });
|
|
163
|
-
console.log(`\nFound ${found.sessions.length} saved Pi session${found.sessions.length === 1 ? "" : "s"}${found.firstDate ? ` to check for ${found.firstDate} through ${found.lastDate}` : ""}.`);
|
|
186
|
+
: "Only new or changed work blocks need summarizing. Selected, redacted history is sent to Pi's model; provider charges may apply.");
|
|
187
|
+
const found = await collectSessions({ sessionDirectory: settings.sessionDirectory, timeZone: settings.timeZone, days: range.days, skipToday });
|
|
188
|
+
console.log(`\nFound ${found.sessions.length} saved Pi session${found.sessions.length === 1 ? "" : "s"}${found.firstDate ? ` to check for ${found.firstDate} through ${found.lastDate}` : skipToday ? ` to check through ${found.lastDate}` : ""}.`);
|
|
164
189
|
if (found.sessions.length === 0) {
|
|
165
190
|
console.log(`No saved sessions found. History folder: ${settings.sessionDirectory}`);
|
|
166
191
|
console.log(`If you have saved history elsewhere, choose it in '${commandName} init' (advanced settings). Otherwise, start a Pi session first.`);
|
|
@@ -171,32 +196,45 @@ async function backfill(args) {
|
|
|
171
196
|
return;
|
|
172
197
|
}
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
199
|
+
console.log("Results are shown in session order; processing remains parallel.");
|
|
200
|
+
console.log("One result per work period; resumed sessions may have multiple dated rows.");
|
|
201
|
+
if (!dryRun) console.log("Summarizing new or changed work may take a few minutes; a slow session can delay later rows.");
|
|
202
|
+
console.log();
|
|
203
|
+
const progress = createBackfillProgress({ total: found.sessions.length, concurrency: settings.concurrency, dryRun });
|
|
204
|
+
const interrupt = () => { cleanupProgress(); process.kill(process.pid, "SIGINT"); };
|
|
205
|
+
const terminate = () => { cleanupProgress(); process.kill(process.pid, "SIGTERM"); };
|
|
206
|
+
function cleanupProgress() {
|
|
207
|
+
progress.close();
|
|
208
|
+
process.removeListener("SIGINT", interrupt);
|
|
209
|
+
process.removeListener("SIGTERM", terminate);
|
|
210
|
+
}
|
|
211
|
+
process.once("SIGINT", interrupt);
|
|
212
|
+
process.once("SIGTERM", terminate);
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await writeSessions(createPiModelClient(generationSettings), found.sessions, settings, {
|
|
216
|
+
...found, dryRun, onProgress: progress.onProgress, onSessionComplete: progress.onSessionComplete,
|
|
217
|
+
});
|
|
218
|
+
} finally {
|
|
219
|
+
cleanupProgress();
|
|
220
|
+
}
|
|
187
221
|
const summaryCount = `${result.summariesCreated} ${result.summariesCreated === 1 ? "summary" : "summaries"}`;
|
|
188
222
|
const entryCount = `${result.entriesAppended} daily ${result.entriesAppended === 1 ? "entry" : "entries"}`;
|
|
189
223
|
const updateCount = `${result.entriesUpdated} ${result.entriesUpdated === 1 ? "entry" : "entries"}`;
|
|
224
|
+
const checkedBlocks = result.sessionResults.filter((entry) => entry.blockId);
|
|
225
|
+
const checkedSessions = new Set(checkedBlocks.map((entry) => entry.sessionId)).size;
|
|
226
|
+
console.log(`\nChecked: ${checkedBlocks.length} work block${checkedBlocks.length === 1 ? "" : "s"} across ${checkedSessions} session${checkedSessions === 1 ? "" : "s"}.`);
|
|
190
227
|
console.log(dryRun
|
|
191
|
-
?
|
|
192
|
-
:
|
|
193
|
-
if (result.sessionsSkipped) console.log(`${result.sessionsSkipped} session${result.sessionsSkipped === 1 ? "" : "s"} skipped (outside the date range or without journalable messages).`);
|
|
228
|
+
? `Would create ${summaryCount}, reuse ${result.summariesReused}, append ${entryCount}; ${result.entriesSkipped} already present; update ${updateCount}.`
|
|
229
|
+
: `Saved: ${summaryCount} created, ${result.summariesReused} reused, appended ${entryCount}; ${result.entriesSkipped} already present; updated ${updateCount}.`);
|
|
230
|
+
if (result.sessionsSkipped) console.log(`${result.sessionsSkipped} session${result.sessionsSkipped === 1 ? "" : "s"} skipped (outside the date range or without journalable messages${skipToday ? ", or active today" : ""}).`);
|
|
231
|
+
if (skipToday) console.log(`${result.blocksExcludedToday} work period${result.blocksExcludedToday === 1 ? "" : "s"} excluded because of activity today (--skip-today).`);
|
|
194
232
|
if (result.dates.length) {
|
|
195
233
|
console.log(`Daily-note dates: ${result.dates.join(", ")}`);
|
|
196
|
-
if (dryRun && result.summariesCreated) console.log(`${result.summariesCreated}
|
|
197
|
-
if (!result.summariesCreated && !result.errors.length) console.log("These unchanged
|
|
234
|
+
if (dryRun && result.summariesCreated) console.log(`${result.summariesCreated} work block${result.summariesCreated === 1 ? "" : "s"} would need summarizing. Large blocks may require multiple model requests each.`);
|
|
235
|
+
if (!result.summariesCreated && !result.errors.length) console.log("These unchanged work blocks need no model requests.");
|
|
198
236
|
} else if (!result.errors.length) {
|
|
199
|
-
console.log(`Nothing to journal in this range. Try '${commandName} backfill 7 --dry-run' or '${commandName} backfill all --dry-run'.`);
|
|
237
|
+
console.log(`Nothing to journal in this range. Try '${commandName} backfill 7${skipToday ? " --skip-today" : ""} --dry-run' or '${commandName} backfill all${skipToday ? " --skip-today" : ""} --dry-run'.`);
|
|
200
238
|
}
|
|
201
239
|
const warnings = [...found.warnings, ...result.errors];
|
|
202
240
|
if (warnings.length) {
|
|
@@ -204,39 +242,43 @@ async function backfill(args) {
|
|
|
204
242
|
console.warn(dryRun ? "Preview incomplete. Fix the issues above, then preview again." : "Some sessions could not be saved. Successful summaries are cached. Fix the issues above and rerun the same command to retry.");
|
|
205
243
|
process.exitCode = 1;
|
|
206
244
|
} else if (dryRun && result.dates.length) {
|
|
207
|
-
console.log(`\nWhen you're ready: ${commandName} backfill ${range.all ? "all" : range.days}${selectedModel ? ` --model ${selectedModel}` : settings.model ? " --model default" : ""}`);
|
|
245
|
+
console.log(`\nWhen you're ready: ${commandName} backfill ${range.all ? "all" : range.days}${skipToday ? " --skip-today" : ""}${selectedModel ? ` --model ${selectedModel}` : settings.model ? " --model default" : ""}${selectedThinking ? ` --thinking ${selectedThinking}` : settings.thinkingLevel ? " --thinking default" : ""}`);
|
|
208
246
|
}
|
|
209
247
|
}
|
|
210
248
|
|
|
211
249
|
function parseStatusArguments(args) {
|
|
212
250
|
let rangeArgument;
|
|
213
251
|
let json = false;
|
|
252
|
+
let skipToday = false;
|
|
214
253
|
for (const arg of args) {
|
|
215
254
|
if (arg === "--json") {
|
|
216
255
|
json = true;
|
|
256
|
+
} else if (arg === "--skip-today") {
|
|
257
|
+
skipToday = true;
|
|
217
258
|
} else if (arg.startsWith("-")) {
|
|
218
259
|
throw new Error(`Unknown status argument: ${arg}. Run '${commandName} status --help'.`);
|
|
219
260
|
} else if (rangeArgument !== undefined) {
|
|
220
|
-
throw new Error(`Usage: ${commandName} status [number-of-days|all] [--json]`);
|
|
261
|
+
throw new Error(`Usage: ${commandName} status [number-of-days|all] [--json] [--skip-today]`);
|
|
221
262
|
} else {
|
|
222
263
|
rangeArgument = arg;
|
|
223
264
|
}
|
|
224
265
|
}
|
|
225
|
-
return { range: parseBackfillArgument(rangeArgument, "status"), json };
|
|
266
|
+
return { range: parseBackfillArgument(rangeArgument, "status"), json, skipToday };
|
|
226
267
|
}
|
|
227
268
|
|
|
228
269
|
async function status(args) {
|
|
229
|
-
const { range, json } = parseStatusArguments(args);
|
|
270
|
+
const { range, json, skipToday } = parseStatusArguments(args);
|
|
230
271
|
const settings = await loadSettings();
|
|
231
272
|
requireJournalPaths(settings);
|
|
232
273
|
const found = await collectSessions({
|
|
233
274
|
sessionDirectory: settings.sessionDirectory,
|
|
234
275
|
timeZone: settings.timeZone,
|
|
235
276
|
days: range.days,
|
|
277
|
+
skipToday,
|
|
236
278
|
});
|
|
237
279
|
const previousEntries = await listJournaledSessions(settings.cacheDirectory);
|
|
238
280
|
const inspection = await writeSessions(
|
|
239
|
-
{
|
|
281
|
+
{ cachePolicy: summaryCachePolicy(settings) },
|
|
240
282
|
found.sessions,
|
|
241
283
|
settings,
|
|
242
284
|
{ ...found, dryRun: true, journaledEntries: previousEntries },
|
|
@@ -244,26 +286,31 @@ async function status(args) {
|
|
|
244
286
|
const sessions = inspection.sessionResults
|
|
245
287
|
.filter((session) => !["skipped", "error"].includes(session.status))
|
|
246
288
|
.map((session) => {
|
|
247
|
-
const previous = previousEntries.get(session.sessionId) || [];
|
|
248
|
-
const status = session.entryPresent ? "logged" : previous.length ? "
|
|
249
|
-
const
|
|
250
|
-
const sourceChanged = !hasCurrentSource && previous.some((entry) => entry.sourceFingerprint);
|
|
289
|
+
const previous = (previousEntries.get(session.sessionId) || []).filter((entry) => entry.blockId === session.blockId);
|
|
290
|
+
const status = session.entryPresent ? "logged" : previous.length ? "stale" : "new";
|
|
291
|
+
const summaryChanged = previous.length && !previous.some((entry) => entry.cacheFingerprint === session.cacheFingerprint);
|
|
251
292
|
return {
|
|
252
293
|
sessionId: session.sessionId,
|
|
294
|
+
blockId: session.blockId,
|
|
253
295
|
project: session.project,
|
|
254
296
|
date: session.date,
|
|
255
297
|
time: session.time,
|
|
298
|
+
activityDates: session.activityDates,
|
|
299
|
+
prerequisite: session.prerequisite,
|
|
300
|
+
...(session.continuationOf ? { continuationOf: session.continuationOf } : {}),
|
|
256
301
|
status,
|
|
257
|
-
...(status === "
|
|
258
|
-
reason:
|
|
302
|
+
...(status === "stale" ? {
|
|
303
|
+
reason: session.legacyEntry
|
|
304
|
+
? "previous entry needs work-block migration"
|
|
305
|
+
: summaryChanged ? "work-block evidence or generation policy changed" : "previous journal entry is not current",
|
|
259
306
|
} : {}),
|
|
260
|
-
summaryStatus: session.summaryReused ? "
|
|
307
|
+
summaryStatus: session.summaryReused ? "reusable" : "needs-summarizing",
|
|
261
308
|
dailyPath: session.dailyPath,
|
|
262
309
|
};
|
|
263
310
|
});
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
311
|
+
const stale = sessions.filter((session) => session.status === "stale").length;
|
|
312
|
+
const newCount = sessions.filter((session) => session.status === "new").length;
|
|
313
|
+
const needsSummarizing = sessions.filter((session) => session.summaryStatus === "needs-summarizing").length;
|
|
267
314
|
const warnings = [...found.warnings, ...inspection.errors];
|
|
268
315
|
const report = {
|
|
269
316
|
timeframe: {
|
|
@@ -272,16 +319,18 @@ async function status(args) {
|
|
|
272
319
|
firstDate: found.firstDate || null,
|
|
273
320
|
lastDate: found.lastDate,
|
|
274
321
|
timeZone: settings.timeZone,
|
|
322
|
+
...(skipToday ? { skipToday: true } : {}),
|
|
275
323
|
},
|
|
276
324
|
totals: {
|
|
277
325
|
scanned: found.sessions.length,
|
|
278
326
|
eligible: sessions.length,
|
|
279
327
|
logged: sessions.filter((session) => session.status === "logged").length,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
328
|
+
stale,
|
|
329
|
+
new: newCount,
|
|
330
|
+
needsSummarizing,
|
|
283
331
|
skipped: inspection.sessionsSkipped,
|
|
284
332
|
errors: inspection.errors.length,
|
|
333
|
+
...(skipToday ? { excludedToday: inspection.blocksExcludedToday } : {}),
|
|
285
334
|
},
|
|
286
335
|
sessions,
|
|
287
336
|
warnings,
|
|
@@ -290,26 +339,29 @@ async function status(args) {
|
|
|
290
339
|
if (json) {
|
|
291
340
|
console.log(JSON.stringify(report, null, 2));
|
|
292
341
|
} else {
|
|
293
|
-
const label = range.all ? "all saved sessions" : `last ${range.days} calendar day${range.days === 1 ? "" : "s"}`;
|
|
294
|
-
console.log(`LogDig status: ${label} (${settings.timeZone})${found.firstDate ? ` · ${found.firstDate} through ${found.lastDate}` : ""}`);
|
|
295
|
-
console.log(`${
|
|
296
|
-
console.log(
|
|
297
|
-
|
|
342
|
+
const label = range.all ? "all saved sessions" : `last ${range.days}${skipToday ? " complete" : ""} calendar day${range.days === 1 ? "" : "s"}`;
|
|
343
|
+
console.log(`LogDig status: activity in ${label} (${settings.timeZone})${found.firstDate ? ` · ${found.firstDate} through ${found.lastDate}` : skipToday ? ` · through ${found.lastDate}` : ""}${skipToday ? " · skipping today" : ""}`);
|
|
344
|
+
if (skipToday) console.log(`${inspection.blocksExcludedToday} work period${inspection.blocksExcludedToday === 1 ? "" : "s"} excluded because of activity today (--skip-today).`);
|
|
345
|
+
console.log(`${report.totals.eligible} work block${report.totals.eligible === 1 ? "" : "s"}: ${report.totals.logged} logged, ${stale} stale, ${newCount} new.`);
|
|
346
|
+
console.log(`Summaries: ${report.totals.eligible - needsSummarizing} reusable, ${needsSummarizing} work block${needsSummarizing === 1 ? " needs" : "s need"} summarizing.`);
|
|
347
|
+
const prerequisites = sessions.filter((session) => session.prerequisite).length;
|
|
348
|
+
if (prerequisites) console.log(`Includes ${prerequisites} earlier block${prerequisites === 1 ? "" : "s"} needed for continuation links.`);
|
|
349
|
+
const attention = sessions.filter((session) => session.status !== "logged" || session.summaryStatus !== "reusable");
|
|
298
350
|
if (attention.length) {
|
|
299
351
|
console.log("\nNeeds attention:");
|
|
300
352
|
for (const session of attention) {
|
|
301
|
-
const state = session.status
|
|
302
|
-
const summary = session.summaryStatus === "
|
|
353
|
+
const state = session.status + (session.reason ? `: ${session.reason}` : "");
|
|
354
|
+
const summary = session.summaryStatus === "reusable" ? "summary reusable" : "needs summarizing";
|
|
303
355
|
console.log(`- ${session.date} ${session.time} · ${session.project} · ${state}; ${summary} (${session.sessionId})`);
|
|
304
356
|
console.log(` Daily note: ${session.dailyPath}`);
|
|
305
357
|
}
|
|
306
358
|
} else if (!sessions.length) {
|
|
307
|
-
console.log("No journalable
|
|
359
|
+
console.log("No journalable work blocks found in this timeframe.");
|
|
308
360
|
} else {
|
|
309
361
|
console.log("Everything in this timeframe is current.");
|
|
310
362
|
}
|
|
311
|
-
if (
|
|
312
|
-
console.log(`\nTo update the journal: ${commandName} backfill ${range.all ? "all" : range.days}`);
|
|
363
|
+
if (newCount || stale || needsSummarizing) {
|
|
364
|
+
console.log(`\nTo update the journal: ${commandName} backfill ${range.all ? "all" : range.days}${skipToday ? " --skip-today" : ""}`);
|
|
313
365
|
}
|
|
314
366
|
}
|
|
315
367
|
|
|
@@ -331,19 +383,25 @@ function helpText() {
|
|
|
331
383
|
"Commands (install with 'npm install -g logdig', or use 'npm link' in a checkout):",
|
|
332
384
|
" logdig init guided setup; nothing is summarized",
|
|
333
385
|
" logdig doctor check paths and Pi without a model request",
|
|
334
|
-
" logdig config
|
|
335
|
-
" logdig backfill [N|all] [--dry-run] [--
|
|
386
|
+
" logdig config edit numbered settings; q quits (redirected: read-only)",
|
|
387
|
+
" logdig backfill [N|all] [--dry-run] [--skip-today]",
|
|
388
|
+
" [--model provider/model|default]",
|
|
389
|
+
" [--thinking default|off|minimal|low|medium|high|xhigh|max]",
|
|
336
390
|
" journal saved sessions (default: last 3 days)",
|
|
337
|
-
" logdig status [N|all] [--json]
|
|
391
|
+
" logdig status [N|all] [--json] [--skip-today]",
|
|
392
|
+
" show journal coverage (default: last 3 days; read-only)",
|
|
338
393
|
" logdig pi-install | pi-uninstall add or remove /journal integration",
|
|
339
394
|
" logdig --version show the installed version",
|
|
340
395
|
"",
|
|
341
396
|
"Preview first: --dry-run shows dates, files, and cache hits. It never calls Pi",
|
|
342
397
|
"or writes files. Remove --dry-run when you're ready to summarize.",
|
|
398
|
+
"--skip-today selects N complete calendar days ending yesterday, or all past work.",
|
|
399
|
+
"Work periods with activity today are excluded entirely, including overnight work.",
|
|
343
400
|
"",
|
|
344
401
|
"Real backfill uses Pi's startup model and existing authentication. Tools,",
|
|
345
402
|
"extensions, project context, and session saving are disabled for model requests.",
|
|
346
|
-
"Use --model default to ignore
|
|
403
|
+
"Use --model default or --thinking default to ignore that saved override for one run.",
|
|
404
|
+
"More thinking may improve accuracy, but can increase latency and token cost.",
|
|
347
405
|
"More help: README.md and QUICKSTART.md",
|
|
348
406
|
].join("\n");
|
|
349
407
|
}
|
|
@@ -401,5 +459,5 @@ async function main() {
|
|
|
401
459
|
|
|
402
460
|
main().catch((error) => {
|
|
403
461
|
console.error(`logdig: ${error.message}`);
|
|
404
|
-
process.exitCode = error.code
|
|
462
|
+
process.exitCode = ["LOGDIG_SETUP_CANCELLED", "LOGDIG_CONFIG_CANCELLED"].includes(error.code) ? 130 : 1;
|
|
405
463
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import { progressStatus } from "./cli-status.js";
|
|
3
|
+
|
|
4
|
+
function cleanText(value) {
|
|
5
|
+
return stripVTControlCharacters(String(value ?? "")).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function fitLiveLine(line, columns) {
|
|
9
|
+
const limit = Math.max(0, columns - 1);
|
|
10
|
+
let width = 0;
|
|
11
|
+
let fitted = "";
|
|
12
|
+
for (const token of line.match(/\x1b\[[0-9;]*m|./gu) || []) {
|
|
13
|
+
if (token.startsWith("\x1b[")) {
|
|
14
|
+
fitted += token;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
// Conservatively allow two cells for non-ASCII text so panel rows never wrap.
|
|
18
|
+
const cells = token.codePointAt(0) > 127 ? 2 : 1;
|
|
19
|
+
if (width + cells > limit) break;
|
|
20
|
+
width += cells;
|
|
21
|
+
fitted += token;
|
|
22
|
+
}
|
|
23
|
+
return fitted + (line.includes("\x1b[") ? "\x1b[0m" : "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createBackfillProgress({ total, concurrency, dryRun = false, stream = process.stdout, term = process.env.TERM, icons, noColor }) {
|
|
27
|
+
const live = stream.isTTY && term !== "dumb" && !dryRun;
|
|
28
|
+
const statusOptions = { isTTY: stream.isTTY, term, icons, noColor, pad: true };
|
|
29
|
+
const sessions = Array.from({ length: total }, () => ({ lines: [], complete: false, failed: false }));
|
|
30
|
+
let nextResult = 0;
|
|
31
|
+
let completed = 0;
|
|
32
|
+
let panelRows = 0;
|
|
33
|
+
let closed = false;
|
|
34
|
+
|
|
35
|
+
function clearPanel() {
|
|
36
|
+
if (!panelRows) return;
|
|
37
|
+
stream.write("\r" + "\x1b[1A\x1b[2K\r".repeat(panelRows));
|
|
38
|
+
panelRows = 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sessionLabel(event, status = event.status) {
|
|
42
|
+
const position = `${String(event.index).padStart(String(total).length, "0")}/${total}`;
|
|
43
|
+
const timestamp = `${cleanText(event.date)}${event.time ? ` ${cleanText(event.time)}` : ""}`.padEnd(16);
|
|
44
|
+
return `[${position}] ${timestamp} · ${progressStatus(status, statusOptions)} · ${cleanText(event.project) || "session"} (${cleanText(event.sessionId).slice(-8)})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderPanel() {
|
|
48
|
+
if (!live || closed) return;
|
|
49
|
+
clearPanel();
|
|
50
|
+
const pending = sessions.slice(nextResult).filter((session) => session.event);
|
|
51
|
+
if (!pending.length) return;
|
|
52
|
+
const waiting = pending.filter((session) => session.complete);
|
|
53
|
+
const active = pending.filter((session) => !session.complete);
|
|
54
|
+
const capacity = Math.max(0, Math.min(concurrency, (stream.rows || 24) - 3));
|
|
55
|
+
const lines = [
|
|
56
|
+
`Completed: ${completed}/${total} sessions${waiting.length ? ` · ${waiting.length} awaiting ordered output` : ""}`,
|
|
57
|
+
...[...active, ...waiting].slice(0, capacity).map((session) => sessionLabel(session.event, session.complete ? session.failed ? "FAILED" : "DONE" : session.event.status)),
|
|
58
|
+
];
|
|
59
|
+
stream.write(lines.map((line) => fitLiveLine(line, stream.columns || 80)).join("\n") + "\n");
|
|
60
|
+
panelRows = lines.length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onProgress(event) {
|
|
64
|
+
if (closed) return;
|
|
65
|
+
const session = sessions[event.index - 1];
|
|
66
|
+
session.event = event;
|
|
67
|
+
if (event.phase === "error") session.failed = true;
|
|
68
|
+
if (["complete", "skipped", "error"].includes(event.phase)) {
|
|
69
|
+
const detail = event.error || event.reason;
|
|
70
|
+
session.lines.push(`${sessionLabel(event)}${event.prerequisite ? " · prerequisite" : ""}${detail ? `: ${cleanText(detail)}` : ""}`);
|
|
71
|
+
if (dryRun && event.dailyPath) {
|
|
72
|
+
session.lines.push(` Daily note: ${cleanText(event.dailyPath)}`, ` Full summary: ${cleanText(event.sessionPath)}`);
|
|
73
|
+
if (event.prerequisite) session.lines.push(" Includes this earlier block to establish the continuation link.");
|
|
74
|
+
}
|
|
75
|
+
} else if (!live && event.phase === "summarizing") {
|
|
76
|
+
stream.write(`Active ${sessionLabel(event)}\n`);
|
|
77
|
+
}
|
|
78
|
+
renderPanel();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onSessionComplete({ index }) {
|
|
82
|
+
if (closed) return;
|
|
83
|
+
const session = sessions[index - 1];
|
|
84
|
+
session.complete = true;
|
|
85
|
+
completed++;
|
|
86
|
+
clearPanel();
|
|
87
|
+
if (!live && !dryRun && index - 1 > nextResult) {
|
|
88
|
+
stream.write(`Finished ${sessionLabel(session.event, session.failed ? "FAILED" : "DONE")} (waiting for earlier results)\n`);
|
|
89
|
+
}
|
|
90
|
+
while (sessions[nextResult]?.complete) {
|
|
91
|
+
const { lines } = sessions[nextResult];
|
|
92
|
+
if (lines.length) stream.write(lines.join("\n") + "\n");
|
|
93
|
+
lines.length = 0;
|
|
94
|
+
nextResult++;
|
|
95
|
+
}
|
|
96
|
+
renderPanel();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function close() {
|
|
100
|
+
if (closed) return;
|
|
101
|
+
clearPanel();
|
|
102
|
+
closed = true;
|
|
103
|
+
if (live) stream.removeListener("resize", renderPanel);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (live) stream.on("resize", renderPanel);
|
|
107
|
+
return { onProgress, onSessionComplete, close };
|
|
108
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const STATUS_WIDTH = 6;
|
|
2
|
+
const ICON_STATUS_WIDTH = 2;
|
|
3
|
+
|
|
4
|
+
const statuses = {
|
|
5
|
+
ok: { text: "OK", icon: "\uf00c", color: "32" },
|
|
6
|
+
warning: { text: "WARN", icon: "\uf071", color: "33" },
|
|
7
|
+
error: { text: "FIX", icon: "\uf00d", color: "31" },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const progressStatuses = {
|
|
11
|
+
CHECKING: { icon: "\uf002", color: "36" },
|
|
12
|
+
SUMMARIZING: { icon: "\uf110", color: "36" },
|
|
13
|
+
SAVED: { icon: "\uf00c", color: "32" },
|
|
14
|
+
UPDATED: { icon: "\uf040", color: "36" },
|
|
15
|
+
CURRENT: { icon: "\uf058", color: "32" },
|
|
16
|
+
DONE: { icon: "\uf00c", color: "32" },
|
|
17
|
+
SKIPPED: { icon: "\uf05e", color: "33" },
|
|
18
|
+
FAILED: { icon: "\uf00d", color: "31" },
|
|
19
|
+
PREVIEW: { icon: "\uf06e", color: "36" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PROGRESS_STATUS_WIDTH = Math.max(...Object.keys(progressStatuses).map((status) => status.length));
|
|
23
|
+
|
|
24
|
+
export { STATUS_WIDTH };
|
|
25
|
+
|
|
26
|
+
function useIcons({
|
|
27
|
+
isTTY = process.stdout.isTTY,
|
|
28
|
+
term = process.env.TERM,
|
|
29
|
+
icons = process.env.LOGDIG_ICONS !== "0",
|
|
30
|
+
} = {}) {
|
|
31
|
+
return isTTY && term !== "dumb" && icons;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function statusPrefixWidth(options) {
|
|
35
|
+
return useIcons(options) ? ICON_STATUS_WIDTH : STATUS_WIDTH;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function statusPrefix(status, options = {}) {
|
|
39
|
+
const marker = statuses[status];
|
|
40
|
+
if (!marker) throw new TypeError(`Unknown CLI status: ${status}`);
|
|
41
|
+
|
|
42
|
+
if (!useIcons(options)) {
|
|
43
|
+
return `${marker.text}${" ".repeat(STATUS_WIDTH - marker.text.length)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const noColor = options.noColor ?? process.env.NO_COLOR !== undefined;
|
|
47
|
+
const icon = noColor ? marker.icon : `\u001b[${marker.color}m${marker.icon}\u001b[0m`;
|
|
48
|
+
return `${icon} `;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function progressStatus(status, options = {}) {
|
|
52
|
+
const marker = progressStatuses[status];
|
|
53
|
+
if (!marker) throw new TypeError(`Unknown progress status: ${status}`);
|
|
54
|
+
const text = options.pad ? status.padEnd(PROGRESS_STATUS_WIDTH) : status;
|
|
55
|
+
if (!useIcons(options)) return text;
|
|
56
|
+
|
|
57
|
+
const noColor = options.noColor ?? process.env.NO_COLOR !== undefined;
|
|
58
|
+
const icon = noColor ? marker.icon : `\u001b[${marker.color}m${marker.icon}\u001b[0m`;
|
|
59
|
+
return `${icon} ${text}`;
|
|
60
|
+
}
|