logdig 0.1.0 → 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 CHANGED
@@ -19,7 +19,7 @@ In the wizard:
19
19
  - **Summary cache:** choose `My Vault/LogDig` to keep the full summaries visible in Obsidian.
20
20
  - **Heading and length:** `# Projects` and `small` are a good first try. Your personal `# Log` stays separate.
21
21
  - **Timezone:** check that this is the timezone you journal in.
22
- - **Advanced settings:** leave them alone unless you use a custom Pi history folder, executable, or model.
22
+ - **Advanced settings:** choose a custom Pi history folder, executable, model, or thinking level here. Thinking defaults to no LogDig override; choose an explicit level such as `max` for a cost-first reasoning model if you are willing to wait longer.
23
23
  - **Automatic capture:** choose **no** for now.
24
24
  - **Pi extension:** optional. The CLI works without it.
25
25
 
@@ -29,12 +29,13 @@ Press Enter to accept defaults. A typo only re-asks that question. Review the ch
29
29
 
30
30
  ```sh
31
31
  logdig doctor
32
+ logdig status 7
32
33
  logdig backfill 1 --dry-run
33
34
  ```
34
35
 
35
- The preview shows projects, dates, destination files, cache hits, and which sessions need summarizing. **No model requests, file changes, or new folders.** It does not display transcript excerpts.
36
+ `status` counts **logged**, **stale**, and **new** work blocks, with reusable summaries counted separately from blocks needing summarization. It is read-only. The backfill preview shows projects, dates, destination files, cache hits, and which blocks need summarizing. **Neither command makes model requests or changes files.** The preview does not display transcript excerpts.
36
37
 
37
- `1` means today in your chosen timezone. If nothing happened today:
38
+ `1` means conversation activity today in your chosen timezone. Continuous overnight work stays on its starting day, so today's backfill may update yesterday's note. Resuming on a later date after at least four hours without conversation activity starts a linked continuation entry. If its preceding snapshot is missing, preview explicitly includes the earlier block needed for that link. If nothing happened today:
38
39
 
39
40
  ```sh
40
41
  logdig backfill 7 --dry-run
@@ -50,11 +51,15 @@ Once the preview looks right, run the same range without `--dry-run`:
50
51
  logdig backfill 1
51
52
  ```
52
53
 
54
+ To override the model and thinking level for one run, use `logdig backfill 1 --model provider/model --thinking max`. Use `--thinking default` to ignore a saved thinking override. More thinking may improve factual reconstruction, but can increase latency and token cost; available effort depends on the model. Saved thinking preferences also apply to `/journal` and automatic capture.
55
+
53
56
  This may send selected, redacted history to your Pi model and incur provider charges. Pi uses its existing authentication. If authentication fails, open Pi, run `/login`, then retry. Common secrets are redacted, but redaction is not a complete secret scanner.
54
57
 
55
- Open the daily-note date printed by the command. You will find project subheadings under `# Projects`, with timestamped summaries grouped beneath each one. Your existing text stays in place. Click a timestamp to open that entry's saved snapshot of all three summary lengths, or browse `<cache folder>/Sessions/<session-id>.md` for the latest session summary.
58
+ Open the daily-note date printed by the command. You will find project subheadings under `# Projects`, with timestamped summaries grouped beneath each one. Your existing text stays in place. Click a timestamp to open that entry's saved snapshot of all three summary lengths, or browse `<cache folder>/Sessions/` for the latest work-block summaries.
59
+
60
+ Repeating unchanged work reuses its summary and does not insert that entry again. Session renames and other bookkeeping do not trigger regeneration. New work in the same block updates that block's row; continuation blocks get separate linked entries. Manually edited blurbs and older snapshots in `Entries/` are preserved. Run only one save at a time against your journal, and let vault sync finish first.
56
61
 
57
- Repeating an unchanged session reuses its summary and does not insert that entry again. Updated sessions or changed summary settings can append a new version. Run only one save at a time against your journal, and let vault sync finish first.
62
+ If you used the older whole-session journal format, the first real save regenerates summaries for selected work blocks and migrates the old rows while keeping their linked snapshots. Preview before saving to check the dates and model work.
58
63
 
59
64
  ## Optional: use `/journal` inside Pi
60
65
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Remember what you worked on, without writing another status report.
4
4
 
5
- LogDig turns saved [Pi](https://pi.dev) sessions into short entries in your Obsidian daily notes. Your handwritten content stays in place. Each session also gets a cached Markdown note with **Small**, **Medium**, and **Large** summaries, so the details are there when you want them.
5
+ LogDig turns saved [Pi](https://pi.dev) sessions into short entries in your Obsidian daily notes. Your handwritten content stays in place. Each work block also gets a cached Markdown note with **Small**, **Medium**, and **Large** summaries, so the details are there when you want them.
6
6
 
7
7
  No build step, runtime dependencies, or separate model credentials. Try a safe preview before sending any history to a model.
8
8
 
@@ -33,7 +33,7 @@ For a comfortable first try:
33
33
  - Check the timezone. It determines the journal date and time.
34
34
  - Leave automatic capture **off** until you have tried a manual run. Pi integration is optional.
35
35
 
36
- `doctor` checks folder access and Pi availability without requesting a summary. It reports broken paths and a missing Pi executable together, with a next step for each.
36
+ `doctor` checks folder access and Pi availability without requesting a summary. It reports broken paths and a missing Pi executable together, with a next step for each. Interactive terminals use colored Nerd Font status icons; set `LOGDIG_ICONS=0` for text markers, and `NO_COLOR` to disable color.
37
37
 
38
38
  **`--dry-run` never calls Pi, writes files, or creates folders.** It shows the projects, dates, destination files, matching cached summaries, and sessions that would need model requests. The preview does not print transcript excerpts.
39
39
 
@@ -43,7 +43,7 @@ When the preview looks right:
43
43
  logdig backfill 1
44
44
  ```
45
45
 
46
- This journals sessions whose last user message was **today in your configured timezone**, not a rolling 24-hour window. If today is quiet, preview seven days instead:
46
+ This journals work blocks with conversation activity **today in your configured timezone**, not a rolling 24-hour window. Overnight work can update yesterday's note without moving it to today. If today is quiet, preview seven days instead:
47
47
 
48
48
  ```sh
49
49
  logdig backfill 7 --dry-run
@@ -61,7 +61,8 @@ My Vault/
61
61
  │ └── YYYY-MM-DD.md your writing, plus project-grouped summaries under # Projects
62
62
  └── LogDig/
63
63
  ├── Sessions/
64
- │ └── <session-id>.md latest cached summary and provenance
64
+ │ ├── <session-id>.md latest summary of the first work block
65
+ │ └── <session-id>-<block-id>.md latest summary of each continuation
65
66
  └── Entries/
66
67
  └── <entry-id>.md snapshot of all three layers for a journal entry
67
68
  ```
@@ -88,13 +89,30 @@ More work on the same project.
88
89
  **[[<single-entry-id>|23:13]]**: One entry stays compact.
89
90
  ```
90
91
 
91
- A project with one entry puts the timestamp inline at the start of its summary. When a second entry arrives, both timestamps move to separate lines. Summary wording is preserved, including any edits you made.
92
+ Each entry keeps its timestamp inline at the start of its summary, even when a project has multiple entries. Summary wording is preserved, including any edits you made.
92
93
 
93
- Obsidian displays each link as just the timestamp; clicking it opens that entry's detailed summary. The detailed note has two compact frontmatter properties when available: `sessionUsage` for recorded Pi-session usage and `logUsage` for LogDig's summary-generation requests, including intermediate chunks. Each shows cost, cached input, uncached input, output, and elapsed time, for example `"$3.73 ⚡12.2M ↑747k ↓62k · 1h 55m"`. Session duration is wall-clock time, including idle periods; LogDig duration is the time spent generating that summary. The original session and LogDig calls are counted separately. Historical summaries made before usage tracking have no `logUsage`; their cost cannot be recovered without making new requests. Unknown cost or token counts are omitted, not shown as zero. Keep the summary cache inside your vault so Obsidian can resolve these links. Daily summaries do not create headings or code fences; structured detail stays in the linked note. Custom section headings are supported, with project subheadings one level deeper (or bold project labels beneath a level-six heading).
94
+ Obsidian displays each link as just the timestamp; clicking it opens that entry's detailed summary. The detailed note has two compact frontmatter properties when available: `sessionUsage` for recorded Pi usage within that work block and `logUsage` for LogDig's summary-generation requests, including intermediate chunks. Each shows cost, cached input, uncached input, output, and elapsed time, for example `"$3.73 ⚡12.2M ↑747k ↓62k · 1h 55m"`. Work-block duration is wall-clock time, including idle periods within the block; LogDig duration is the time spent generating that summary. The original session and LogDig calls are counted separately. Historical summaries made before usage tracking have no `logUsage`; their cost cannot be recovered without making new requests. Unknown cost or token counts are omitted, not shown as zero. Keep the summary cache inside your vault so Obsidian can resolve these links. Daily summaries do not create headings or code fences; structured detail stays in the linked note. Custom section headings are supported, with project subheadings one level deeper (or bold project labels beneath a level-six heading).
94
95
 
95
- The **last user message** supplies the date and time, even if the assistant finishes after midnight. Alternate session branches are included as explorations, not assumed to be the final result.
96
+ ### Overnight work and continuations
96
97
 
97
- Unchanged sessions reuse their summaries. The identifier in each timestamp link prevents duplicate entries, without HTML comments. Older project-name links and comment-wrapped entries are still recognized. Changing the default does not override existing saved heading preferences; rerun setup to change them. Changing the session, timezone, explicit model, summary level, or heading can append a **new version** rather than replace an earlier journal entry. Linked entry notes keep their original summary even when the latest session cache changes. Handwritten content and summary wording are preserved; an inline timestamp can be expanded when its project gains another entry. Headings inside frontmatter or fenced code are not insertion targets.
98
+ A saved Pi session can contain several **work blocks**. A new block starts only at a new user message when both conditions hold:
99
+
100
+ - Its local date is later than the current block's starting date.
101
+ - At least **four hours** have passed since the preceding conversation activity. Assistant messages and tool results count as activity; session names, model switches, labels, usage records, and extension bookkeeping do not.
102
+
103
+ The block's **starting date and time** supply its journal timestamp. Continuous work from 10pm to 2am stays one entry on the starting day. Returning at 11am after a long break creates a separate entry on the new day, with a **Continues** link to the preceding block's saved snapshot. Repeated saves are checkpoints, not boundaries. Alternate session branches are included as explorations, not assumed to be the final result.
104
+
105
+ Date ranges select actual conversation activity, not just assigned journal dates. Today's backfill therefore catches assistant completion or continued work after midnight and updates yesterday's entry. If a selected continuation has no preceding snapshot, LogDig includes the missing earlier block(s) as prerequisites. Status and preview explicitly show these additions, including their summary-generation cost implications.
106
+
107
+ ### Summary reuse and existing journals
108
+
109
+ Summary freshness is based on the selected, redacted evidence and bounded earlier context, plus summary-processing version, timezone, and configured generation policy. Earlier context is drawn from the preceding block's evidence, not its generated summary, and is marked as background rather than work to repeat. Metadata-only changes do not trigger model requests; usage totals can refresh separately. Explicit LogDig model or thinking-level changes invalidate summaries, while changes to Pi's defaults do not force regeneration under the default policy.
110
+
111
+ The identifier in each timestamp link prevents duplicate entries, without HTML comments. Older project-name links and comment-wrapped entries are still recognized. When a block evolves, LogDig replaces only that block's daily-note row with a link to the latest summary. Earlier blocks stay in place. Previous linked summary snapshots remain in `Entries/`, and manually edited daily summaries are preserved; usage metadata may be refreshed without regenerating the prose.
112
+
113
+ **Upgrading from whole-session journaling:** old summaries use an incompatible cache key and need one regeneration per selected work block. A real save assigns legacy entries to their work blocks using their recorded journal timestamp, updates or relocates their rows as needed, and keeps the original linked snapshots. Run `backfill N --dry-run` first to see the scope and model work. Status and preview never migrate files.
114
+
115
+ Saved heading preferences are not overridden by new defaults; rerun setup to change them. New entries keep their timestamps inline even when their project already has entries. Headings inside frontmatter or fenced code are not insertion targets.
98
116
 
99
117
  Missing daily-note and cache folders are created only by a real save. Raw Pi history stays in Pi's storage.
100
118
 
@@ -111,15 +129,21 @@ logdig backfill journal the last 3 calendar days
111
129
  logdig backfill 7 --dry-run preview seven days without changing anything
112
130
  logdig backfill all --dry-run preview every discoverable saved session
113
131
  logdig backfill 7 journal seven days
114
- logdig backfill 7 --model provider/model
132
+ logdig backfill 7 --model provider/model --thinking max
115
133
  logdig backfill 7 --model default ignore a saved model override for this run
134
+ logdig backfill 7 --thinking default ignore a saved thinking override for this run
135
+ logdig status show coverage for the last 3 calendar days
136
+ logdig status 7 show coverage for seven days
137
+ logdig status all --json output coverage for every saved session as JSON
116
138
  logdig pi-install install the /journal extension
117
139
  logdig pi-uninstall remove it without deleting notes or summaries
118
140
  ```
119
141
 
120
- `--help` works before setup, including `logdig init --help` and `logdig backfill --help`.
142
+ `--help` works before setup, including `logdig init --help`, `logdig backfill --help`, and `logdig status --help`.
143
+
144
+ `status` is a read-only coverage check. It counts work blocks: **logged** means the expected entry is present, **stale** means a previous snapshot exists but the entry needs updating, and **new** means no previous block snapshot was found. Summaries are reported separately as reusable or needing summarization. These are block counts, not exact request counts. It selects blocks by conversation activity in your configured timezone, including overnight updates and any missing continuation prerequisites. It never calls a model or writes files; scan warnings make the command exit nonzero so incomplete coverage is clear. JSON retains `sessions` as the result array, with one row per work block and both `sessionId` and `blockId`; totals use `logged`, `stale`, `new`, and `needsSummarizing`.
121
145
 
122
- New summaries may incur provider charges. Large sessions are summarized in chunks and may need several model requests each. Backfill shows which project it is working on before the model completes. Failed sessions are reported, other sessions continue, and the command exits nonzero if anything needs attention. Fix the issue and rerun the same command; completed summaries are reused, even if a previous attempt failed to write a daily note.
146
+ New summaries may incur provider charges. Large work blocks are summarized in chunks and may need several model requests each. Backfill shows which project it is working on before the model completes. Failed sessions are reported, other sessions continue, and the command exits nonzero if anything needs attention. Fix the issue and rerun the same command; completed summaries are reused, even if a previous attempt failed to write a daily note.
123
147
 
124
148
  Run one backfill at a time against a given journal. The extension prevents overlapping saves within one Pi process, but separate CLI/Pi processes and external note editors are not coordinated. Let an existing save or vault sync finish first.
125
149
 
@@ -152,6 +176,10 @@ CLI backfill uses Pi's normal startup model and existing authentication unless y
152
176
 
153
177
  `/journal` uses the current Pi model, including registered providers, unless a LogDig override is set. The preview needs no available model or authentication. `doctor` checks the executable but does not validate model authentication; open Pi and run `/login` if a real save reports an authentication problem.
154
178
 
179
+ Choose a **thinking level** alongside the model in setup's advanced settings, or use `--thinking` for one CLI backfill. Supported values are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`; `default` removes the LogDig override. Saved settings use `thinkingLevel`, and `PI_JOURNAL_THINKING` overrides it. This applies to intermediate timeline extraction and final summaries, including `/journal` and automatic capture. Support and effort mapping depend on the model and Pi provider, so `max` is not necessarily a distinct supported tier on every model.
180
+
181
+ More thinking can help separate proposals, failed attempts, and verified outcomes, but can increase latency and token cost. Explicitly enabled thinking allows CLI requests up to 30 minutes each, rather than the usual five; providers may impose their own timeouts. With no LogDig override, CLI backfill keeps Pi's startup thinking policy, while `/journal` keeps its existing provider-default behavior and does **not** inherit the active session's thinking level. Select an explicit level for consistent control in both paths. Changing it causes selected cached summaries to need regeneration; preview first to see the scope.
182
+
155
183
  Selected user prompts, assistant conclusions, tool actions, test results, and error excerpts are sent to the chosen model after common credential redaction. System prompts, hidden reasoning, and image payloads are excluded. **Redaction is not a comprehensive secret scanner.** Review your provider's data handling before processing sensitive sessions or enabling automatic capture. Summaries can also contain private project details, so treat your cache and vault accordingly.
156
184
 
157
185
  Model summaries are aids to memory, not proof of completed work. Keep Pi history as the source of truth.
package/bin/logdig.js CHANGED
@@ -2,12 +2,14 @@
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, summaryCachePolicy } from "../src/journal.js";
5
6
  import { collectSessions, parseBackfillArgument, writeSessions } from "../src/session-runner.js";
6
7
  import { createPiModelClient } from "../src/pi-client.js";
7
8
  import { spawnPiProcess } from "../src/pi-process.js";
8
9
  import { environmentOverrides, loadSettings, validateSettings } from "../src/settings.js";
9
10
  import { checkDirectory } from "../src/directories.js";
10
11
  import { configureLogDig } from "../src/setup.js";
12
+ import { statusPrefix, statusPrefixWidth } from "../src/cli-status.js";
11
13
 
12
14
  const packageRoot = fileURLToPath(new URL("../", import.meta.url));
13
15
  const relativeEntry = path.relative(process.cwd(), fileURLToPath(import.meta.url)).split(path.sep).join("/");
@@ -81,30 +83,35 @@ async function doctor() {
81
83
  const settings = await loadSettings();
82
84
  let issues = 0;
83
85
  let warnings = 0;
84
- console.log("LogDig checkup (no model requests or file changes)\n");
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 [
86
+ const checks = [
90
87
  ["Session history", settings.sessionDirectory, false],
91
88
  ["Summary cache", settings.cacheDirectory, true],
92
89
  ["Daily notes", settings.dailyDirectory, true],
93
- ]) {
90
+ ];
91
+ const statusWidth = statusPrefixWidth();
92
+ const labelWidth = Math.max(...checks.map(([label]) => label.length), "Pi".length);
93
+ const valueColumn = statusWidth + labelWidth + 2;
94
+ const formatLabel = (label, prefixWidth = statusWidth) => `${label}:${" ".repeat(valueColumn - prefixWidth - label.length - 1)}`;
95
+
96
+ console.log(`${statusPrefix("ok")}${formatLabel("Settings")}${settings.filePath}${settings.configured ? " (saved)" : " (not saved; using defaults/environment)"}`);
97
+ const overrides = environmentOverrides();
98
+ if (overrides.length) console.log(`Environment overrides: ${overrides.join(", ")}`);
99
+
100
+ for (const [label, directory, writable] of checks) {
94
101
  if (!directory) {
95
- console.error(`FIX ${label}: not configured. Run '${commandName} init' to choose a folder.`);
102
+ console.error(`${statusPrefix("error")}${formatLabel(label)}not configured. Run '${commandName} init' to choose a folder.`);
96
103
  issues++;
97
104
  continue;
98
105
  }
99
106
  try {
100
107
  const result = await checkDirectory(directory, { writable, allowMissing: writable });
101
- console.log(`OK ${label}: ${directory}${result.exists ? "" : " (will be created on a real backfill)"}`);
108
+ console.log(`${statusPrefix("ok")}${formatLabel(label)}${directory}${result.exists ? "" : " (will be created on a real backfill)"}`);
102
109
  } catch (error) {
103
110
  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'.`);
111
+ console.warn(`${statusPrefix("warning")}${formatLabel(label)}${directory} (not found yet). Start a saved Pi session, or choose its location with '${commandName} init'.`);
105
112
  warnings++;
106
113
  } else {
107
- console.error(`FIX ${label}: ${directory} (${error.message}). Check folder permissions or choose another folder with '${commandName} init'.`);
114
+ console.error(`${statusPrefix("error")}${formatLabel(label)}${directory} (${error.message}). Check folder permissions or choose another folder with '${commandName} init'.`);
108
115
  issues++;
109
116
  }
110
117
  }
@@ -112,12 +119,13 @@ async function doctor() {
112
119
 
113
120
  try {
114
121
  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"}`);
122
+ console.log(`${statusPrefix("ok")}${formatLabel("Pi")}${pi.stdout || pi.stderr || "available"}`);
116
123
  } catch (error) {
117
- console.error(`FIX Pi: ${error.message}. Install Pi or set its executable in '${commandName} init' (advanced settings).`);
124
+ console.error(`${statusPrefix("error")}${formatLabel("Pi")}${error.message}. Install Pi or set its executable in '${commandName} init' (advanced settings).`);
118
125
  issues++;
119
126
  }
120
127
  console.log(`\nSummary model: ${settings.model || "Pi startup default"}`);
128
+ console.log(`Thinking: ${settings.thinkingLevel || "Pi startup default"}`);
121
129
  console.log("Authentication is checked only when you request a summary. If it fails, open Pi and run /login.");
122
130
  if (issues) {
123
131
  console.error(`\n${issues} thing${issues === 1 ? "" : "s"} to fix. Address the FIX lines above, then run '${commandName} doctor' again.`);
@@ -132,6 +140,7 @@ async function backfill(args) {
132
140
  let daysArgument;
133
141
  let dryRun = false;
134
142
  let selectedModel = settings.model;
143
+ let selectedThinking = settings.thinkingLevel;
135
144
  for (let index = 0; index < args.length; index++) {
136
145
  const arg = args[index];
137
146
  if (arg === "--dry-run") {
@@ -141,6 +150,11 @@ async function backfill(args) {
141
150
  if (!selectedModel || selectedModel.startsWith("-")) throw new Error("--model requires provider/model, or 'default' to use Pi's startup model");
142
151
  } else if (arg.startsWith("--model=")) {
143
152
  selectedModel = arg.slice("--model=".length);
153
+ } else if (arg === "--thinking") {
154
+ selectedThinking = args[++index];
155
+ if (!selectedThinking || selectedThinking.startsWith("-")) throw new Error("--thinking requires default, off, minimal, low, medium, high, xhigh, or max");
156
+ } else if (arg.startsWith("--thinking=")) {
157
+ selectedThinking = arg.slice("--thinking=".length);
144
158
  } else if (!arg.startsWith("-") && daysArgument === undefined) {
145
159
  daysArgument = arg;
146
160
  } else {
@@ -148,7 +162,9 @@ async function backfill(args) {
148
162
  }
149
163
  }
150
164
  selectedModel = selectedModel?.trim().toLowerCase() === "default" ? undefined : selectedModel;
151
- selectedModel = validateSettings({ ...settings, model: selectedModel }).model;
165
+ const generationSettings = validateSettings({ ...settings, model: selectedModel, thinkingLevel: selectedThinking });
166
+ selectedModel = generationSettings.model;
167
+ selectedThinking = generationSettings.thinkingLevel;
152
168
  const range = parseBackfillArgument(daysArgument);
153
169
  requireJournalPaths(settings);
154
170
 
@@ -156,9 +172,10 @@ async function backfill(args) {
156
172
  console.log(`Daily notes: ${settings.dailyDirectory} (${settings.dailyHeader}, ${settings.dailySummary})`);
157
173
  console.log(`Summary cache: ${path.join(settings.cacheDirectory, "Sessions")}`);
158
174
  console.log(`Model: ${selectedModel || "Pi startup default"}`);
175
+ console.log(`Thinking: ${selectedThinking || "Pi startup default"}`);
159
176
  console.log(dryRun
160
177
  ? "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.");
178
+ : "Only new or changed work blocks need summarizing. Selected, redacted history is sent to Pi's model; provider charges may apply.");
162
179
  const found = await collectSessions({ sessionDirectory: settings.sessionDirectory, timeZone: settings.timeZone, days: range.days });
163
180
  console.log(`\nFound ${found.sessions.length} saved Pi session${found.sessions.length === 1 ? "" : "s"}${found.firstDate ? ` to check for ${found.firstDate} through ${found.lastDate}` : ""}.`);
164
181
  if (found.sessions.length === 0) {
@@ -171,29 +188,31 @@ async function backfill(args) {
171
188
  return;
172
189
  }
173
190
 
174
- const modelClient = createPiModelClient({ ...settings, model: selectedModel });
191
+ const modelClient = createPiModelClient(generationSettings);
175
192
  const result = await writeSessions(modelClient, found.sessions, settings, {
176
193
  ...found,
177
194
  dryRun,
178
- onProgress: ({ index, total, sessionId, project, date, time, status, error, phase, sessionPath, dailyPath }) => {
195
+ onProgress: ({ index, total, sessionId, project, date, time, status, error, phase, sessionPath, dailyPath, prerequisite }) => {
179
196
  if (["checking", "skipped"].includes(phase)) return;
180
197
  console.log(`[${index}/${total}] ${project || sessionId}${date ? ` · ${date} ${time}` : ""}: ${error || status}`);
181
198
  if (dryRun && dailyPath) {
182
199
  console.log(` Daily note: ${dailyPath}`);
183
200
  console.log(` Full summary: ${sessionPath}`);
201
+ if (prerequisite) console.log(" Includes this earlier block to establish the continuation link.");
184
202
  }
185
203
  },
186
204
  });
187
205
  const summaryCount = `${result.summariesCreated} ${result.summariesCreated === 1 ? "summary" : "summaries"}`;
188
206
  const entryCount = `${result.entriesAppended} daily ${result.entriesAppended === 1 ? "entry" : "entries"}`;
207
+ const updateCount = `${result.entriesUpdated} ${result.entriesUpdated === 1 ? "entry" : "entries"}`;
189
208
  console.log(dryRun
190
- ? `\nWould create ${summaryCount}, reuse ${result.summariesReused}, append ${entryCount}; ${result.entriesSkipped} already present.`
191
- : `\nSaved: ${summaryCount} created, ${result.summariesReused} reused, ${entryCount} appended; ${result.entriesSkipped} already present.`);
209
+ ? `\nWould create ${summaryCount}, reuse ${result.summariesReused}, append ${entryCount}; ${result.entriesSkipped} already present; update ${updateCount}.`
210
+ : `\nSaved: ${summaryCount} created, ${result.summariesReused} reused, appended ${entryCount}; ${result.entriesSkipped} already present; updated ${updateCount}.`);
192
211
  if (result.sessionsSkipped) console.log(`${result.sessionsSkipped} session${result.sessionsSkipped === 1 ? "" : "s"} skipped (outside the date range or without journalable messages).`);
193
212
  if (result.dates.length) {
194
213
  console.log(`Daily-note dates: ${result.dates.join(", ")}`);
195
- if (dryRun && result.summariesCreated) console.log(`${result.summariesCreated} session${result.summariesCreated === 1 ? "" : "s"} would need model requests. Large sessions may require multiple requests each.`);
196
- if (!result.summariesCreated && !result.errors.length) console.log("These unchanged sessions need no model requests.");
214
+ 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.`);
215
+ if (!result.summariesCreated && !result.errors.length) console.log("These unchanged work blocks need no model requests.");
197
216
  } else if (!result.errors.length) {
198
217
  console.log(`Nothing to journal in this range. Try '${commandName} backfill 7 --dry-run' or '${commandName} backfill all --dry-run'.`);
199
218
  }
@@ -203,7 +222,125 @@ async function backfill(args) {
203
222
  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.");
204
223
  process.exitCode = 1;
205
224
  } else if (dryRun && result.dates.length) {
206
- console.log(`\nWhen you're ready: ${commandName} backfill ${range.all ? "all" : range.days}${selectedModel ? ` --model ${selectedModel}` : settings.model ? " --model default" : ""}`);
225
+ console.log(`\nWhen you're ready: ${commandName} backfill ${range.all ? "all" : range.days}${selectedModel ? ` --model ${selectedModel}` : settings.model ? " --model default" : ""}${selectedThinking ? ` --thinking ${selectedThinking}` : settings.thinkingLevel ? " --thinking default" : ""}`);
226
+ }
227
+ }
228
+
229
+ function parseStatusArguments(args) {
230
+ let rangeArgument;
231
+ let json = false;
232
+ for (const arg of args) {
233
+ if (arg === "--json") {
234
+ json = true;
235
+ } else if (arg.startsWith("-")) {
236
+ throw new Error(`Unknown status argument: ${arg}. Run '${commandName} status --help'.`);
237
+ } else if (rangeArgument !== undefined) {
238
+ throw new Error(`Usage: ${commandName} status [number-of-days|all] [--json]`);
239
+ } else {
240
+ rangeArgument = arg;
241
+ }
242
+ }
243
+ return { range: parseBackfillArgument(rangeArgument, "status"), json };
244
+ }
245
+
246
+ async function status(args) {
247
+ const { range, json } = parseStatusArguments(args);
248
+ const settings = await loadSettings();
249
+ requireJournalPaths(settings);
250
+ const found = await collectSessions({
251
+ sessionDirectory: settings.sessionDirectory,
252
+ timeZone: settings.timeZone,
253
+ days: range.days,
254
+ });
255
+ const previousEntries = await listJournaledSessions(settings.cacheDirectory);
256
+ const inspection = await writeSessions(
257
+ { cachePolicy: summaryCachePolicy(settings) },
258
+ found.sessions,
259
+ settings,
260
+ { ...found, dryRun: true, journaledEntries: previousEntries },
261
+ );
262
+ const sessions = inspection.sessionResults
263
+ .filter((session) => !["skipped", "error"].includes(session.status))
264
+ .map((session) => {
265
+ const previous = (previousEntries.get(session.sessionId) || []).filter((entry) => entry.blockId === session.blockId);
266
+ const status = session.entryPresent ? "logged" : previous.length ? "stale" : "new";
267
+ const summaryChanged = previous.length && !previous.some((entry) => entry.cacheFingerprint === session.cacheFingerprint);
268
+ return {
269
+ sessionId: session.sessionId,
270
+ blockId: session.blockId,
271
+ project: session.project,
272
+ date: session.date,
273
+ time: session.time,
274
+ activityDates: session.activityDates,
275
+ prerequisite: session.prerequisite,
276
+ ...(session.continuationOf ? { continuationOf: session.continuationOf } : {}),
277
+ status,
278
+ ...(status === "stale" ? {
279
+ reason: session.legacyEntry
280
+ ? "previous entry needs work-block migration"
281
+ : summaryChanged ? "work-block evidence or generation policy changed" : "previous journal entry is not current",
282
+ } : {}),
283
+ summaryStatus: session.summaryReused ? "reusable" : "needs-summarizing",
284
+ dailyPath: session.dailyPath,
285
+ };
286
+ });
287
+ const stale = sessions.filter((session) => session.status === "stale").length;
288
+ const newCount = sessions.filter((session) => session.status === "new").length;
289
+ const needsSummarizing = sessions.filter((session) => session.summaryStatus === "needs-summarizing").length;
290
+ const warnings = [...found.warnings, ...inspection.errors];
291
+ const report = {
292
+ timeframe: {
293
+ kind: range.all ? "all" : "days",
294
+ ...(range.days ? { days: range.days } : {}),
295
+ firstDate: found.firstDate || null,
296
+ lastDate: found.lastDate,
297
+ timeZone: settings.timeZone,
298
+ },
299
+ totals: {
300
+ scanned: found.sessions.length,
301
+ eligible: sessions.length,
302
+ logged: sessions.filter((session) => session.status === "logged").length,
303
+ stale,
304
+ new: newCount,
305
+ needsSummarizing,
306
+ skipped: inspection.sessionsSkipped,
307
+ errors: inspection.errors.length,
308
+ },
309
+ sessions,
310
+ warnings,
311
+ };
312
+
313
+ if (json) {
314
+ console.log(JSON.stringify(report, null, 2));
315
+ } else {
316
+ const label = range.all ? "all saved sessions" : `last ${range.days} calendar day${range.days === 1 ? "" : "s"}`;
317
+ console.log(`LogDig status: activity in ${label} (${settings.timeZone})${found.firstDate ? ` · ${found.firstDate} through ${found.lastDate}` : ""}`);
318
+ console.log(`${report.totals.eligible} work block${report.totals.eligible === 1 ? "" : "s"}: ${report.totals.logged} logged, ${stale} stale, ${newCount} new.`);
319
+ console.log(`Summaries: ${report.totals.eligible - needsSummarizing} reusable, ${needsSummarizing} work block${needsSummarizing === 1 ? " needs" : "s need"} summarizing.`);
320
+ const prerequisites = sessions.filter((session) => session.prerequisite).length;
321
+ if (prerequisites) console.log(`Includes ${prerequisites} earlier block${prerequisites === 1 ? "" : "s"} needed for continuation links.`);
322
+ const attention = sessions.filter((session) => session.status !== "logged" || session.summaryStatus !== "reusable");
323
+ if (attention.length) {
324
+ console.log("\nNeeds attention:");
325
+ for (const session of attention) {
326
+ const state = session.status + (session.reason ? `: ${session.reason}` : "");
327
+ const summary = session.summaryStatus === "reusable" ? "summary reusable" : "needs summarizing";
328
+ console.log(`- ${session.date} ${session.time} · ${session.project} · ${state}; ${summary} (${session.sessionId})`);
329
+ console.log(` Daily note: ${session.dailyPath}`);
330
+ }
331
+ } else if (!sessions.length) {
332
+ console.log("No journalable work blocks found in this timeframe.");
333
+ } else {
334
+ console.log("Everything in this timeframe is current.");
335
+ }
336
+ if (newCount || stale || needsSummarizing) {
337
+ console.log(`\nTo update the journal: ${commandName} backfill ${range.all ? "all" : range.days}`);
338
+ }
339
+ }
340
+
341
+ if (warnings.length) {
342
+ if (!json) console.error(`Status may be incomplete:\n${warnings.join("\n")}`);
343
+ process.exitCode = 1;
207
344
  }
208
345
  }
209
346
 
@@ -221,7 +358,9 @@ function helpText() {
221
358
  " logdig doctor check paths and Pi without a model request",
222
359
  " logdig config show settings and active environment overrides",
223
360
  " logdig backfill [N|all] [--dry-run] [--model provider/model|default]",
361
+ " [--thinking default|off|minimal|low|medium|high|xhigh|max]",
224
362
  " journal saved sessions (default: last 3 days)",
363
+ " logdig status [N|all] [--json] show journal coverage (default: last 3 days; read-only)",
225
364
  " logdig pi-install | pi-uninstall add or remove /journal integration",
226
365
  " logdig --version show the installed version",
227
366
  "",
@@ -230,20 +369,21 @@ function helpText() {
230
369
  "",
231
370
  "Real backfill uses Pi's startup model and existing authentication. Tools,",
232
371
  "extensions, project context, and session saving are disabled for model requests.",
233
- "Use --model default to ignore a saved model override for one run.",
372
+ "Use --model default or --thinking default to ignore that saved override for one run.",
373
+ "More thinking may improve accuracy, but can increase latency and token cost.",
234
374
  "More help: README.md and QUICKSTART.md",
235
375
  ].join("\n");
236
376
  }
237
377
 
238
378
  async function main() {
239
379
  const [command = "help", ...args] = process.argv.slice(2);
240
- const commands = ["init", "config", "doctor", "backfill", "pi-install", "pi-uninstall", "help", "--help", "-h", "--version", "-v"];
380
+ const commands = ["init", "config", "doctor", "backfill", "status", "pi-install", "pi-uninstall", "help", "--help", "-h", "--version", "-v"];
241
381
  if (!commands.includes(command)) throw new Error(`Unknown command: ${command}. Run '${commandName} help'.`);
242
382
  if (args.includes("--help") || args.includes("-h")) {
243
383
  console.log(helpText());
244
384
  return;
245
385
  }
246
- if (command !== "backfill" && args.length) throw new Error(`'${command}' does not accept arguments. Run '${commandName} help'.`);
386
+ if (!["backfill", "status"].includes(command) && args.length) throw new Error(`'${command}' does not accept arguments. Run '${commandName} help'.`);
247
387
  switch (command) {
248
388
  case "--version":
249
389
  case "-v": {
@@ -263,6 +403,9 @@ async function main() {
263
403
  case "backfill":
264
404
  await backfill(args);
265
405
  break;
406
+ case "status":
407
+ await status(args);
408
+ break;
266
409
  case "pi-install": {
267
410
  const settings = await loadSettings();
268
411
  await runPiCommand(settings.piCommand, ["install", packageRoot], { inherit: true, timeoutMs: 120_000 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logdig",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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,35 @@
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
+ export { STATUS_WIDTH };
11
+
12
+ function useIcons({
13
+ isTTY = process.stdout.isTTY,
14
+ term = process.env.TERM,
15
+ icons = process.env.LOGDIG_ICONS !== "0",
16
+ } = {}) {
17
+ return isTTY && term !== "dumb" && icons;
18
+ }
19
+
20
+ export function statusPrefixWidth(options) {
21
+ return useIcons(options) ? ICON_STATUS_WIDTH : STATUS_WIDTH;
22
+ }
23
+
24
+ export function statusPrefix(status, options = {}) {
25
+ const marker = statuses[status];
26
+ if (!marker) throw new TypeError(`Unknown CLI status: ${status}`);
27
+
28
+ if (!useIcons(options)) {
29
+ return `${marker.text}${" ".repeat(STATUS_WIDTH - marker.text.length)}`;
30
+ }
31
+
32
+ const noColor = options.noColor ?? process.env.NO_COLOR !== undefined;
33
+ const icon = noColor ? marker.icon : `\u001b[${marker.color}m${marker.icon}\u001b[0m`;
34
+ return `${icon} `;
35
+ }