logdig 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/QUICKSTART.md 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
 
@@ -33,9 +33,9 @@ logdig status 7
33
33
  logdig backfill 1 --dry-run
34
34
  ```
35
35
 
36
- `status` shows what's already journaled, what needs updating or is missing, and which summaries would need a model request. It is read-only. The backfill preview shows projects, dates, destination files, cache hits, and which sessions need summarizing. **Neither command makes model requests or changes files.** The preview 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.
37
37
 
38
- `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:
39
39
 
40
40
  ```sh
41
41
  logdig backfill 7 --dry-run
@@ -51,11 +51,15 @@ Once the preview looks right, run the same range without `--dry-run`:
51
51
  logdig backfill 1
52
52
  ```
53
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
+
54
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.
55
57
 
56
- 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.
57
61
 
58
- Repeating an unchanged session reuses its summary and does not insert that entry again. If a session evolves, LogDig updates its existing daily row to the latest summary, preserving manually edited blurbs and keeping older snapshots in `Entries/`. 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.
59
63
 
60
64
  ## Optional: use `/journal` inside Pi
61
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. When a session evolves, LogDig replaces its daily-note row with a link to the latest summary instead of adding another row. Previous linked snapshots remain unchanged in `Entries/`, and manually edited daily summaries are preserved. Changing the default does not override existing saved heading preferences; rerun setup to change them. 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,8 +129,9 @@ 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
116
135
  logdig status show coverage for the last 3 calendar days
117
136
  logdig status 7 show coverage for seven days
118
137
  logdig status all --json output coverage for every saved session as JSON
@@ -122,9 +141,9 @@ logdig pi-uninstall remove it without deleting notes or summarie
122
141
 
123
142
  `--help` works before setup, including `logdig init --help`, `logdig backfill --help`, and `logdig status --help`.
124
143
 
125
- `status` is a read-only coverage check. It reports current journal entries, sessions that need updating, missing entries, and whether each summary is cached or needs a model request. It uses the last user message date in your configured timezone. It never calls a model or writes files; scan warnings make the command exit nonzero so incomplete coverage is clear.
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`.
126
145
 
127
- 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.
128
147
 
129
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.
130
149
 
@@ -157,6 +176,10 @@ CLI backfill uses Pi's normal startup model and existing authentication unless y
157
176
 
158
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.
159
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
+
160
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.
161
184
 
162
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,13 +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 } 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 { statusPrefix, statusPrefixWidth } from "../src/cli-status.js";
12
13
 
13
14
  const packageRoot = fileURLToPath(new URL("../", import.meta.url));
14
15
  const relativeEntry = path.relative(process.cwd(), fileURLToPath(import.meta.url)).split(path.sep).join("/");
@@ -82,29 +83,35 @@ async function doctor() {
82
83
  const settings = await loadSettings();
83
84
  let issues = 0;
84
85
  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 [
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,16 +188,17 @@ 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
  });
@@ -193,8 +211,8 @@ async function backfill(args) {
193
211
  if (result.sessionsSkipped) console.log(`${result.sessionsSkipped} session${result.sessionsSkipped === 1 ? "" : "s"} skipped (outside the date range or without journalable messages).`);
194
212
  if (result.dates.length) {
195
213
  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.");
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.");
198
216
  } else if (!result.errors.length) {
199
217
  console.log(`Nothing to journal in this range. Try '${commandName} backfill 7 --dry-run' or '${commandName} backfill all --dry-run'.`);
200
218
  }
@@ -204,7 +222,7 @@ async function backfill(args) {
204
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.");
205
223
  process.exitCode = 1;
206
224
  } 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" : ""}`);
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" : ""}`);
208
226
  }
209
227
  }
210
228
 
@@ -236,7 +254,7 @@ async function status(args) {
236
254
  });
237
255
  const previousEntries = await listJournaledSessions(settings.cacheDirectory);
238
256
  const inspection = await writeSessions(
239
- { cacheKey: settings.model || "Pi default" },
257
+ { cachePolicy: summaryCachePolicy(settings) },
240
258
  found.sessions,
241
259
  settings,
242
260
  { ...found, dryRun: true, journaledEntries: previousEntries },
@@ -244,26 +262,31 @@ async function status(args) {
244
262
  const sessions = inspection.sessionResults
245
263
  .filter((session) => !["skipped", "error"].includes(session.status))
246
264
  .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);
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);
251
268
  return {
252
269
  sessionId: session.sessionId,
270
+ blockId: session.blockId,
253
271
  project: session.project,
254
272
  date: session.date,
255
273
  time: session.time,
274
+ activityDates: session.activityDates,
275
+ prerequisite: session.prerequisite,
276
+ ...(session.continuationOf ? { continuationOf: session.continuationOf } : {}),
256
277
  status,
257
- ...(status === "needs-update" ? {
258
- reason: sourceChanged ? "session changed since its previous journal entry" : "previous journal entry is not current",
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",
259
282
  } : {}),
260
- summaryStatus: session.summaryReused ? "cached" : "needs-generation",
283
+ summaryStatus: session.summaryReused ? "reusable" : "needs-summarizing",
261
284
  dailyPath: session.dailyPath,
262
285
  };
263
286
  });
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;
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;
267
290
  const warnings = [...found.warnings, ...inspection.errors];
268
291
  const report = {
269
292
  timeframe: {
@@ -277,9 +300,9 @@ async function status(args) {
277
300
  scanned: found.sessions.length,
278
301
  eligible: sessions.length,
279
302
  logged: sessions.filter((session) => session.status === "logged").length,
280
- needsUpdate,
281
- missing,
282
- needsModelRequest,
303
+ stale,
304
+ new: newCount,
305
+ needsSummarizing,
283
306
  skipped: inspection.sessionsSkipped,
284
307
  errors: inspection.errors.length,
285
308
  },
@@ -291,24 +314,26 @@ async function status(args) {
291
314
  console.log(JSON.stringify(report, null, 2));
292
315
  } else {
293
316
  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");
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");
298
323
  if (attention.length) {
299
324
  console.log("\nNeeds attention:");
300
325
  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";
326
+ const state = session.status + (session.reason ? `: ${session.reason}` : "");
327
+ const summary = session.summaryStatus === "reusable" ? "summary reusable" : "needs summarizing";
303
328
  console.log(`- ${session.date} ${session.time} · ${session.project} · ${state}; ${summary} (${session.sessionId})`);
304
329
  console.log(` Daily note: ${session.dailyPath}`);
305
330
  }
306
331
  } else if (!sessions.length) {
307
- console.log("No journalable sessions found in this timeframe.");
332
+ console.log("No journalable work blocks found in this timeframe.");
308
333
  } else {
309
334
  console.log("Everything in this timeframe is current.");
310
335
  }
311
- if (missing || needsUpdate || needsModelRequest) {
336
+ if (newCount || stale || needsSummarizing) {
312
337
  console.log(`\nTo update the journal: ${commandName} backfill ${range.all ? "all" : range.days}`);
313
338
  }
314
339
  }
@@ -333,6 +358,7 @@ function helpText() {
333
358
  " logdig doctor check paths and Pi without a model request",
334
359
  " logdig config show settings and active environment overrides",
335
360
  " logdig backfill [N|all] [--dry-run] [--model provider/model|default]",
361
+ " [--thinking default|off|minimal|low|medium|high|xhigh|max]",
336
362
  " journal saved sessions (default: last 3 days)",
337
363
  " logdig status [N|all] [--json] show journal coverage (default: last 3 days; read-only)",
338
364
  " logdig pi-install | pi-uninstall add or remove /journal integration",
@@ -343,7 +369,8 @@ function helpText() {
343
369
  "",
344
370
  "Real backfill uses Pi's startup model and existing authentication. Tools,",
345
371
  "extensions, project context, and session saving are disabled for model requests.",
346
- "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.",
347
374
  "More help: README.md and QUICKSTART.md",
348
375
  ].join("\n");
349
376
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logdig",
3
- "version": "0.1.1",
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
+ }
package/src/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { JOURNAL_SYSTEM_PROMPT } from "./journal.js";
3
+ import { JOURNAL_SYSTEM_PROMPT, summaryCachePolicy } from "./journal.js";
4
4
  import { collectSessions, parseBackfillArgument, writeSessions } from "./session-runner.js";
5
5
  import { loadSettings } from "./settings.js";
6
- import { fingerprintSession } from "./transcript.js";
7
6
 
8
7
  function requireJournalSettings(settings) {
9
8
  if (!settings.cacheDirectory) throw new Error("Run 'logdig init' or set PI_JOURNAL_DIR to a LogDig cache folder");
@@ -23,7 +22,6 @@ function currentSession(ctx) {
23
22
  entries: ctx.sessionManager.getEntries(),
24
23
  sourcePath: ctx.sessionManager.getSessionFile() || "active Pi session",
25
24
  };
26
- session.fingerprint = fingerprintSession(session);
27
25
  return session;
28
26
  }
29
27
 
@@ -37,26 +35,29 @@ function extensionModelClient(ctx, settings) {
37
35
 
38
36
  return {
39
37
  modelLabel: model ? `${model.provider}/${model.id}` : "Pi current model",
40
- cacheKey: settings.model || "Pi default",
38
+ cachePolicy: summaryCachePolicy(settings),
41
39
  complete: async (prompt) => {
42
40
  if (!model) throw new Error("Pi has no active model for journal generation");
43
41
  if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
44
42
  throw new Error(`No configured authentication for ${model.provider}/${model.id}. Run /login in Pi, then try /journal again`);
45
43
  }
46
- return ctx.modelRegistry.complete(
47
- model,
48
- {
49
- systemPrompt: JOURNAL_SYSTEM_PROMPT,
50
- messages: [
51
- {
52
- role: "user",
53
- content: [{ type: "text", text: prompt }],
54
- timestamp: Date.now(),
55
- },
56
- ],
57
- },
58
- { sessionId: randomUUID(), cacheRetention: "none", signal: ctx.signal },
59
- );
44
+ const context = {
45
+ systemPrompt: JOURNAL_SYSTEM_PROMPT,
46
+ messages: [
47
+ {
48
+ role: "user",
49
+ content: [{ type: "text", text: prompt }],
50
+ timestamp: Date.now(),
51
+ },
52
+ ],
53
+ };
54
+ const options = { sessionId: randomUUID(), cacheRetention: "none", signal: ctx.signal };
55
+ // Preserve existing provider defaults unless LogDig explicitly selects thinking.
56
+ if (settings.thinkingLevel === undefined) return ctx.modelRegistry.complete(model, context, options);
57
+ return ctx.modelRegistry.streamSimple(model, context, {
58
+ ...options,
59
+ reasoning: settings.thinkingLevel === "off" ? undefined : settings.thinkingLevel,
60
+ }).result();
60
61
  },
61
62
  };
62
63
  }
@@ -88,7 +89,7 @@ async function runBackfill(ctx, argument) {
88
89
  days: range.days,
89
90
  currentSession: currentSession(ctx),
90
91
  });
91
- const client = dryRun ? { cacheKey: settings.model || "Pi default" } : extensionModelClient(ctx, settings);
92
+ const client = dryRun ? { cachePolicy: summaryCachePolicy(settings) } : extensionModelClient(ctx, settings);
92
93
  const result = await writeSessions(client, found.sessions, settings, { ...found, dryRun, onProgress: journalProgress(ctx) });
93
94
  const warnings = [...found.warnings, ...result.errors];
94
95
  const summaryCount = `${result.summariesCreated} ${result.summariesCreated === 1 ? "summary" : "summaries"}`;