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/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
- console.log(`Settings: ${settings.filePath}${settings.configured ? " (saved)" : " (not saved; using defaults/environment)"}`);
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(`FIX ${label}: not configured. Run '${commandName} init' to choose a folder.`);
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(`OK ${label}: ${directory}${result.exists ? "" : " (will be created on a real backfill)"}`);
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(`WARN ${label}: ${directory} (not found yet). Start a saved Pi session, or choose its location with '${commandName} init'.`);
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(`FIX ${label}: ${directory} (${error.message}). Check folder permissions or choose another folder with '${commandName} init'.`);
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(`OK Pi: ${pi.stdout || pi.stderr || "available"}`);
125
+ console.log(`${statusPrefix("ok")}${formatLabel("Pi")}${pi.stdout || pi.stderr || "available"}`);
116
126
  } catch (error) {
117
- console.error(`FIX Pi: ${error.message}. Install Pi or set its executable in '${commandName} init' (advanced settings).`);
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
- selectedModel = validateSettings({ ...settings, model: selectedModel }).model;
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 sessions need model requests. Selected, redacted history is sent to Pi's model; provider charges may apply.");
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
- const modelClient = createPiModelClient({ ...settings, model: selectedModel });
175
- const result = await writeSessions(modelClient, found.sessions, settings, {
176
- ...found,
177
- dryRun,
178
- onProgress: ({ index, total, sessionId, project, date, time, status, error, phase, sessionPath, dailyPath }) => {
179
- if (["checking", "skipped"].includes(phase)) return;
180
- console.log(`[${index}/${total}] ${project || sessionId}${date ? ` · ${date} ${time}` : ""}: ${error || status}`);
181
- if (dryRun && dailyPath) {
182
- console.log(` Daily note: ${dailyPath}`);
183
- console.log(` Full summary: ${sessionPath}`);
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
- ? `\nWould create ${summaryCount}, reuse ${result.summariesReused}, append ${entryCount}; ${result.entriesSkipped} already present; update ${updateCount}.`
192
- : `\nSaved: ${summaryCount} created, ${result.summariesReused} reused, appended ${entryCount}; ${result.entriesSkipped} already present; updated ${updateCount}.`);
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} session${result.summariesCreated === 1 ? "" : "s"} would need model requests. Large sessions may require multiple requests each.`);
197
- if (!result.summariesCreated && !result.errors.length) console.log("These unchanged sessions need no model requests.");
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
- { cacheKey: settings.model || "Pi default" },
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 ? "needs-update" : "missing";
249
- const hasCurrentSource = previous.some((entry) => entry.sourceFingerprint === session.sourceFingerprint);
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 === "needs-update" ? {
258
- reason: sourceChanged ? "session changed since its previous journal entry" : "previous journal entry is not current",
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 ? "cached" : "needs-generation",
307
+ summaryStatus: session.summaryReused ? "reusable" : "needs-summarizing",
261
308
  dailyPath: session.dailyPath,
262
309
  };
263
310
  });
264
- const needsUpdate = sessions.filter((session) => session.status === "needs-update").length;
265
- const missing = sessions.filter((session) => session.status === "missing").length;
266
- const needsModelRequest = sessions.filter((session) => session.summaryStatus === "needs-generation").length;
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
- needsUpdate,
281
- missing,
282
- needsModelRequest,
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(`${report.totals.eligible} journalable: ${report.totals.logged} current, ${needsUpdate} need updating, ${missing} missing.`);
296
- console.log(`Summaries: ${report.totals.eligible - needsModelRequest} cached, ${needsModelRequest} need model requests.`);
297
- const attention = sessions.filter((session) => session.status !== "logged" || session.summaryStatus !== "cached");
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 === "logged" ? "logged" : session.reason || "not yet logged";
302
- const summary = session.summaryStatus === "cached" ? "summary cached" : "summary needs a model request";
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 sessions found in this timeframe.");
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 (missing || needsUpdate || needsModelRequest) {
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 show settings and active environment overrides",
335
- " logdig backfill [N|all] [--dry-run] [--model provider/model|default]",
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] show journal coverage (default: last 3 days; read-only)",
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 a saved model override for one run.",
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 === "LOGDIG_SETUP_CANCELLED" ? 130 : 1;
462
+ process.exitCode = ["LOGDIG_SETUP_CANCELLED", "LOGDIG_CONFIG_CANCELLED"].includes(error.code) ? 130 : 1;
405
463
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logdig",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Turn saved Pi sessions into a work journal in your Obsidian daily notes.",
5
5
  "license": "MIT",
6
6
  "author": "Soleone",
@@ -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
+ }