Commit graph

168 commits

Author SHA1 Message Date
Charles Kelsoe
59fea7afe4 Release 0.32.1: deep-link token completes the browser reconnect (#75) 2026-07-18 11:11:24 -04:00
Charles Kelsoe
403edc38db
Complete browser reconnect from the deep link, not just the paste button (#75) (#76)
* Complete browser reconnect from the deep link, not just the paste button (#75)

During a browser reconnect, a token returning via the bookmarklet's
obsidian:// deep link stored correctly but skipped the follow-through the
paste button ran: paused auto-sync stayed paused, the onReconnected
continuation never fired, and the modal stayed open holding the
single-flight gate until manually cancelled.

Both channels now run one completion handler. The active flow is tracked as
an object (modal + continuation + deliveryInFlight); a delivery claims the
flow before its async token store (the other channel no-ops meanwhile), and
completion is keyed to that exact flow object, so a slow store outliving a
cancelled flow can never complete a newer one. The gate release in the
modal's onClose is once-guarded because Obsidian runs onClose on every
close() call and the modal can be closed twice (completion plus the paste
handler's close-on-success backstop); without the guard a stale second
close could release a newer sign-in's gate. onunload closes any waiting
reconnect modal so a stale surface cannot write through a dead plugin
instance. A deep-linked token outside a reconnect now also resumes paused
auto-sync, and an onReconnected failure reports accurately instead of
surfacing as a token-save error.

Three Codex review rounds drove the hardening (delivery race/ABA, unload
leak, double-close gate release); all findings addressed.

* Address CodeRabbit findings: gate release on modal failure, stale-paste store guard

- The single-flight gate now survives modal lifecycle failures: open() is
  wrapped (a modal that never opened can never fire onClose), and completion
  and onunload call an idempotent release exposed on the flow after their
  close() attempt, so a throwing close() cannot wedge every later sign-in.
- pasteTokenFromClipboard takes an optional canStore guard re-checked after
  the clipboard read, right before the store. The reconnect paste passes the
  flow-identity guard, so a paste resolving after its flow was cancelled can
  no longer overwrite a newer sign-in's token. Other callers keep the
  default and are unchanged.
2026-07-18 11:10:51 -04:00
Charles Kelsoe
17352644d3 Release 0.32.0: retire the refresh subsystem (issue #68, release 2 of 2) 2026-07-18 10:07:21 -04:00
Charles Kelsoe
cad39f4a41
Retire the refresh subsystem; route Reconnect by recorded sign-in method (#68) (#74)
The long-lived user token (0.31.0) made the 24h silent-refresh machinery
dead weight: it could never fire against the stored token, and running it
would only mint a 24h WT and reinstate daily expiry. Delete it end to end:
plaud-refresh.ts, plaud-refresh-net.ts, their tests, the proactive timer,
retry backoff, reactive refresh, the 'Refresh session now' command, and the
keepSessionAlive setting (a stale key in existing data.json is scrubbed at
load). Pause + Reconnect and both sign-in flows stay; they are the roughly
yearly re-auth path. The rebuild spec lives in
dev-docs/plaud-importer/2026-07-17-auth-mechanisms-reference.md (workspace).

Reconnect routing (SSO vs email) used to infer the account type from the
stored WRT that this release deletes. Capture now records how the session
was obtained (settings.signInMethod: the email window sets 'window',
storeAccessToken sets 'browser' for paste and deep-link) and Reconnect
routes off that; pre-0.32.0 sessions with no recorded method fall back to
peeking at the legacy WRT secret, which only email-window sign-ins ever
stored. Manually linking a different secret in settings clears the recorded
method so it cannot misdescribe an unknown credential. The decision is a
pure function (reconnect-routing.ts) with unit tests, including the legacy
fallback and reader-failure cases.

Verified by a four-agent adversarial pass (orphans, spec compliance,
behavior walks, prose drift) plus Codex review; all findings addressed.
998 tests green; strict unused-symbol tsc sweep clean.
2026-07-18 10:06:44 -04:00
Charles Kelsoe
d5876e0e12 Release 0.31.0: adopt the long-lived Plaud user token (issue #68, release 1 of 2) 2026-07-18 09:24:12 -04:00
Charles Kelsoe
ca7293edba
Adopt the long-lived Plaud user token; neutralize the 24h refresh (#68) (#71)
* Adopt the long-lived Plaud user token; neutralize the 24h refresh (#68)

Capture the ~300-day user token from web.plaud.ai localStorage in all three
sign-in flows (email window, SSO bookmarklet, paste) instead of the 24h
workspace token. A shared capture guard (plaud-token.ts) accepts a value only
when its decoded payload carries a client_id and a still-future exp, and rejects
the refresh token (WRT) and the neighboring profile JWT with no exp.

Neutralize the silent refresh whenever a non-WT token is stored: a refresh mints
a 24h WT and would clobber the long-lived token, reinstating daily expiry. The
scheduler, the reactive path, and the manual command all gate on the token typ.
Route a dead-token -3900 in-band status to pause + Reconnect. The refresh
subsystem stays in the tree (inert) until 0.32.0 removes it.

Update the sign-in copy and README to reflect the ~yearly re-auth cadence for
both email and SSO accounts.

* Address CodeRabbit/CodeQL findings on the token-capture PR

- Parse JWTs by an exact three-segment split instead of an unanchored search
  regex (removes the polynomial-regex/ReDoS class CodeQL flagged, and refuses a
  prefix.<jwt>.suffix value a search would have accepted and stored).
- Restrict the localStorage token read to a Plaud origin: the email window gates
  on the probe href host, and the SSO bookmarklet checks location.hostname.
- Update the two auto-sync toggle descriptions that still said the ~24h token
  pauses sync roughly daily.
- Clarify in the README that Clear sign-in does not sign you out of Plaud in
  your normal browser.
- Add regression tests for exact-segment JWT parsing.

* Guard the refresh write with an optimistic-concurrency check

A silent refresh does not hold a lock across its network round-trip, and the
capture paths (storeAccessToken, clearSignIn) do not take the refresh gate, so
the stored credential can change while a refresh is in flight. Snapshot the
credential the refresh is renewing and apply the refreshed WT only if that exact
token and secretId are still stored on return. Otherwise keep the current
credential and report whether the session is still healthy, so a concurrent
paste/deep-link (long-lived or another account) or sign-out is never silently
overwritten and the reactive path does not pause a session a capture just fixed.

Addresses CodeRabbit's token-capture race finding on PR #71.

* Address CodeRabbit findings on the merge commit

- Update the keep-session-alive setting description and settings-interface
  comment: the current sign-in stores the long-lived token, so background
  renewal only applies to legacy short-lived sessions. The old text still
  promised a daily 24h renewal.
- Harden token capture: PROBE_JS now refuses to read localStorage on any
  non-Plaud hostname, so a foreign page's token key is never even read. The
  plugin-side isPlaudOrigin gate on the returned href stays authoritative;
  the in-page check is defense in depth on the same page snapshot.

* Require HTTPS in both token-capture origin gates

CodeRabbit finding on PR #71: the gates checked hostname only, and the
capture guard validates JWT claims, not a signature, so a plain-http
plaud.ai page (MITM-able) could plant a crafted token. isPlaudOrigin and
the in-page PROBE_JS gate now both require https:. Adds an isPlaudOrigin
test covering https acceptance, http rejection, lookalike hosts, other
schemes, and malformed input (isPlaudOrigin exported for the test).
2026-07-18 09:23:31 -04:00
Charles Kelsoe
1bb0ae372e Release 0.30.3: keep email sign-in navigation in-window (Obsidian 1.13 reroute) 2026-07-17 19:20:44 -04:00
Charles Kelsoe
9ca14b8a75
Keep email sign-in navigation in-window: strip the host's will-navigate reroute (#73)
Obsidian 1.13 (first seen here as 1.13.1 on 2026-06-22) attaches a
main-process will-navigate listener to every top-level window that cancels
any navigation off Obsidian's own app:// origin and reroutes the URL to
shell.openExternal. In the plugin's sign-in window that turned Plaud's own
login redirect (web.plaud.ai -> /login) into a stray tab in the user's
default browser. The 0.30.2 popup fix was aimed at window.open and could not
help: the leak is page navigation, a path no prior fix addressed.

Fix: remove the listener from the plugin-owned window right after
construction (remote method calls run synchronously in the main process, and
the listener attaches during the BrowserWindow constructor, so this cannot
race), re-strip on each poll tick as defense in depth, and fail closed
(settle null, window closed) if removal is unavailable or incomplete.

Verified live against Obsidian 1.13.2 via CDP: with the listener, a page
navigation logs 'Opening URL:' in the main process and opens the default
browser; with the fix, the same redirect stays in-window, listenerCount
reads 0, and no external open occurs. Root-caused with Codex plus a
five-agent forensic workflow (version bisect: listener absent through
1.12.7, present in 1.13.x).
2026-07-17 19:20:15 -04:00
Charles Kelsoe
d87940935b Release 0.30.2: fix email sign-in popup leak to the system browser 2026-07-17 18:46:10 -04:00
Charles Kelsoe
121d086bc2
Fix email sign-in popup leak: deny must return synchronously over the remote bridge (#72)
The sign-in BrowserWindow's setWindowOpenHandler deny was a plain renderer
callback. @electron/remote transports ordinary callbacks asynchronously, so
the {action: 'deny'} return value never reached Electron's main process and
every popup the Plaud page fired escaped to the system browser as a stray
tab. Wrap the deny in remote.createFunctionWithReturnValue, which serializes
a constant synchronous return into the main process. The plain callback stays
only as a last-resort fallback on builds without that API, with an error note.

Latent since 2d34f5a; masked for a while when c5c87b6 stopped the background
refresh from loading the page in a hidden window. Diagnosed with Codex.
2026-07-17 18:45:38 -04:00
Charles Kelsoe
197d30a368 Release 0.30.1: fix SSO reconnect dead-end and stop futile refresh retries 2026-07-10 11:29:15 -04:00
Charles Kelsoe
4fb688a364
Fix SSO reconnect dead-end and stop futile refresh retries (#64) (#67)
* Fix SSO reconnect dead-end and stop futile refresh retries (#64)

Google and Apple (SSO) accounts sign in through the external browser and a
bookmarklet, so the plugin's Electron partition never holds their Plaud session
cookies. The windowless background refresh authenticates only via those cookies,
so it can never succeed for SSO accounts. On expiry the auth-pause Reconnect
notice opened the embedded email sign-in window, where Google and Apple logins
dead-end.

- Route Reconnect by account type: a session with no stored WRT (the SSO/paste
  signature) opens the browser + bookmarklet + paste flow instead of the embedded
  window. BrowserSignInModal gains an inline Paste button so the reconnect
  completes from the notice. Email sign-ins are unchanged.
- Stop the proactive refresh re-arming for an exhausted, due session that has no
  stored WRT, so an un-refreshable SSO session no longer retries hourly against
  Plaud's rate-limited endpoint. Email sessions (WRT present) keep retrying, since
  their failures may be transient.
- A post-reconnect continuation runs on both reconnect paths so the backfill
  'Reconnect and retry' notice actually retries after an SSO paste.
- Guard the paste handler so a throwing token store cannot surface an unhandled
  rejection.
- Document in the sign-in help and README why SSO accounts reconnect about daily.

Refs #64. Not a full resolution; unattended SSO refresh needs the official OAuth
path, tracked separately.

* Guard the SSO browser-reconnect with the single-flight sign-in gate

CodeRabbit finding on PR #67: openBrowserReconnect opened a BrowserSignInModal
with no concurrency guard, so a second Reconnect (a stacked notice, or the
settings tab) could open a rival modal and race the token write on the shared
partition. Reuse the existing reauthInFlight gate the embedded window and the
silent refresh already share: reject a concurrent open with the same 'Plaud
sign-in is already open.' notice, hold it for the modal's lifetime, and clear it
from the modal's onClose so paste-success, cancel, and dismiss all release it.
2026-07-10 11:02:15 -04:00
Charles Kelsoe
c170382fbd
docs: list plaud-industry in README frontmatter fields (#66)
Adds the plaud-industry property (shipped in 0.30.0) to the enumerated optional frontmatter fields in the feature list; it was omitted when the field was added in #65.
2026-07-09 19:54:47 -04:00
Charles Kelsoe
05f0ea67e2
Release 0.30.0: dedupe plaud-category, add plaud-industry (#65)
Follow-up to #61: suppress plaud-category when it duplicates plaud-template, capture Plaud industry_category as a separate plaud-industry property. Closes #61.
2026-07-09 19:43:46 -04:00
Charles Kelsoe
0e0db57fd4 Release 0.29.0: fix session refresh setTimeout overflow, background refresh never opens a window 2026-07-09 18:46:59 -04:00
Charles Kelsoe
c5c87b6414
Fix session refresh setTimeout overflow; background refresh never opens a window (#63)
* Fix session refresh setTimeout overflow; background refresh never opens a window

Root cause (debug-log proven): the proactive refresh timer computes its delay from the stored token's exp. A long-lived token (a 137-day exp was observed) produced a delay above the 32-bit setTimeout ceiling (2,147,483,647 ms), so the timer fired immediately instead of months out. The spurious refresh failed, retried on the 5/15/30/60-minute backoff, and every retry opened a hidden sign-in window whose login page leaked web.plaud.ai popup tabs into the default browser, roughly hourly.

Fixes:

- Clamp the scheduled delay to REFRESH_MAX_DELAY_MS (20 days), safely under the ceiling. New pure computeRefreshDelay in plaud-refresh.ts, unit tested.

- Skip the proactive refresh while the stored token still has life beyond the 5-minute lead (new isRefreshDue guard in runScheduledRefresh). An early clamped fire re-arms without refreshing and resets the failure streak.

- The background refresh is windowless-only: tryRefreshSession no longer falls back to a hidden openPlaudLogin window. On failure the plugin pauses and shows the one-click Reconnect notice. The dead headless machinery in plaud-login.ts is removed; only a user click can open the sign-in window.

Closes #41.

* Bound the windowless refresh POSTs with a 30s abort timeout

CodeRabbit finding on PR #63: session.fetch had no timeout, so a stalled request would hold refreshInFlight/reauthInFlight forever and block every later refresh plus the manual Sign in until restart. Now each POST carries AbortSignal.timeout(30s); an abort rejects the post, performNetRefresh catches it and returns null, and the refresh fails cleanly. Matters more now that the windowless path is the only background refresh.
2026-07-09 18:43:02 -04:00
Charles Kelsoe
7c30dfa019 Release 0.28.0: restore summary metadata, preserve unknown frontmatter 2026-07-09 11:04:50 -04:00
Charles Kelsoe
4160d07b06
Fix #61: restore summary metadata on the newer summary path (#62)
Reads summary metadata (model, template, headline, category, summary-id) from the /file/detail response on the newer auto_sum_note path via a scoped by-name resolver, restoring the plaud-* frontmatter fields. Adds a drift debug-signal and a golden-fixture regression test.
2026-07-09 11:02:16 -04:00
Charles Kelsoe
c3e1d2dd84
Preserve unknown frontmatter on re-import (#58) (#60)
Adds a global setting (default on) so an overwrite re-import keeps any frontmatter property the user or their downstream automation added that the plugin does not manage. Before this, formatFrontmatter rebuilt the block from reserved plugin keys plus declared extra-frontmatter rows, so an undeclared foreign key was dropped on overwrite. The 0.25.0 per-property preserve (#53) only covered declared keys (an allowlist); this closes the gap for keys never entered in settings.

Mechanism: a passthrough in formatFrontmatter after the custom-rows loop carries through any existing key that is neither reserved nor already emitted. Fires only on the overwrite path (existingValues present); reserved keys still refresh; a declared row still controls its key. To let the plugin manage a specific key instead, declare it as an extra-frontmatter row with preserve off.

Also fixes a pre-existing gap the passthrough surfaced: plaud-placeholder (the stub marker) was missing from RESERVED_FRONTMATTER_KEYS, so it was wrongly preserved onto real content. Now reserved.

Threaded settings -> buildImportRuntimeOptions -> NoteWriterOptions -> NoteWriter ctor -> FormatMarkdownOptions -> formatFrontmatter; toggle in both the imperative (1.12) and declarative (1.13+) settings paths. lint + build + 983 tests green (7 new). Codex pre-commit: no findings.
2026-07-09 10:00:23 -04:00
Charles Kelsoe
b4c247f556
Salvage #43 durability: shared duplicate-handling const + regression test (#57)
* Salvage #43 durability: shared duplicate-handling const, override marker, regression test

Non-functional follow-up to 0.25.2, which shipped the user-facing #43 copy fix. Adds the two durability pieces PR #51 had that 0.25.2 dropped, using 0.25.2's current wording (no copy revert):

- Extract DUPLICATE_HANDLING_NAME/DESC consts so the declarative (1.13+) and imperative (1.12) settings paths share one literal and cannot drift.

- Comment the makeWriter skip/overwrite override as the #43 safe fallback to preserve across refactors.

- Regression test asserting NoteWriter throws on onDuplicate='prompt' without a callback, so a future refactor routing 'prompt' to a headless writer fails loud instead of stalling.

No behavior change. lint + build + 976 tests green (1 new). Codex pre-commit: no findings.

* Fix code-comment path reference to __tests__/note-writer.test.ts

Addresses Copilot review nit on #57: the makeWriter comment referenced note-writer.test.ts but the file lives under __tests__/. Comment-only, no logic change.
2026-07-09 08:47:56 -04:00
Charles Kelsoe
1581181fc3 docs: document import-list filters, ignore, and trashed badge (#54)
README was not updated when the filter bar, per-recording ignore, empty-state, and Trashed badge shipped in 0.26.0-0.27.2. Document them in the features list, the Import dialog and trashed sections, and the Using it steps.
2026-07-08 19:28:53 -04:00
Charles Kelsoe
e23d749802 Release 0.27.2: recognize Plaud trash flag in numeric/string form
Plaud's per-record is_trash can arrive as 1/"1" (the list is queried with is_trash=2, a numeric enum), so the strict === true parse left every recording flagged not-trashed and the Trashed badge + Hide-trashed filter inert. Normalize the flag across boolean/number/string encodings.
2026-07-08 19:09:03 -04:00
Charles Kelsoe
6cb85478c9 Release 0.27.1: fix stuck-empty import list, compact filter group, trash badge
Auto-advance past fully-hidden pages so the dialog is not left blank when auto-sync imported the newest recordings; honest empty-state message replacing the misleading scroll hint. Filter toggles collapse to one compact Hide: group. Trashed recordings show a Trashed badge when shown.
2026-07-08 18:56:47 -04:00
Charles Kelsoe
3b055ff3c7 Release 0.27.0: hide-updates filter and consistent Hide-* toggle labels
Adds a 'Hide updates available' toggle to the import dialog (default off; update-available rows are actionable so shown by default). Relabels 'Show trashed' to 'Hide trashed' so all four filter toggles share one polarity (checked = hidden).
2026-07-08 18:30:14 -04:00
Charles Kelsoe
2eeefbcc94 Release 0.26.0: import-list filtering and per-recording ignore (#54)
Import dialog gains a filter bar (hide already-processed, hide ignored, show trashed) and a per-row ignore button. Ignored recording ids persist in settings and are excluded from background auto-sync. Closes #54.
2026-07-08 18:13:40 -04:00
Charles Kelsoe
b8a82aff41 Release 0.25.2: clarify duplicate handling is manual-import only (issue #43)
Rename the Duplicate handling setting to 'Duplicate handling for manual imports' and state in both its description and the automatic sync description that background sync never prompts and ignores this setting. Copy-only change; auto-sync already runs headless with version-marker skip/update. No logic change.
2026-07-08 14:47:31 -04:00
Charles Kelsoe
6e81917f13 Release 0.25.1: fix #52 card image (repoint on import + one-time repair command) 2026-07-08 11:21:20 -04:00
Charles Kelsoe
bc11b9754c
Fix #52 card image: repoint on import + one-time repair for older notes (#56)
* Fix card image showing 'could not be found' by repointing Plaud's inline embed (#52)

Plaud's AI summary markdown carries an inline embed authored by Plaud's
server (![PLAUD NOTE](permanent/.../summary_poster/card_...)) that only
resolves inside Plaud. The importer already downloads that asset into the
note's -assets folder and links it in the managed section, but copied the
summary body verbatim, leaving the broken inline embed alongside the good one.

- New exported pure fn rewriteInlineSummaryEmbeds(content, urlToLocalPath)
  repoints an inline markdown image embed at its downloaded local copy as an
  ![[...]] wikilink on a map hit; unmapped embeds and existing wikilinks are
  left alone.
- A summaryEmbedRewrites map (asset.url -> local path) is populated at the two
  image-save sites and applied in the existing vault.process pass before the
  managed section is rebuilt.
- Tests: the pure fn plus key-match (exact, query-suffix) and markdown-only
  scope cases. The map-population + vault.process wiring is validated by a
  hands-on smoke test in a real vault (the obsidian mock is intentionally
  inert), NOT jest.

* Add one-time command to repair card links in older imports (#52)

The import-time fix only repoints card embeds on (re)import, so notes
imported before it keep the broken inline embed. Adds a user-invoked (never
automatic) command 'Repair card image links from older imports (one-time)'
that scans this plugin's notes and repoints each broken card poster embed at
the card image already in the note's -assets folder.

- attachment-importer.ts: pure isLocalCardImage() and repairLegacyCardEmbeds()
  (conservative: only summary_poster embeds, only when exactly one local card
  exists; idempotent). Clearly banner-marked for removal in a future version.
- main.ts: the command + a scan handler scoped to plaud-id notes; reports a
  count and flags notes whose card was never downloaded for a re-import.
- Tests for both pure helpers (match boundaries, no-card, ambiguous, idempotent).

Vault scan is validated by hands-on smoke test (obsidian mock is inert).

* Address CodeRabbit on repair command: recompute inside vault.process, add disposed + single-flight guards

- The repair now recomputes the rewrite inside the vault.process callback on
  the FRESH content, so a concurrent edit between read and write is never
  clobbered; the read is used only to gate whether a write is needed.
- Added a this.disposed check at the top and inside the scan loop so an
  unloaded plugin stops writing, and a repairInFlight single-flight guard so a
  double-invoke cannot run two bulk scans at once.
2026-07-08 11:14:23 -04:00
Charles Kelsoe
403cb952ac Release 0.25.0: silent session refresh (fixed), extra frontmatter, dated subfolder default 2026-07-08 09:52:13 -04:00
Charles Kelsoe
4baa7b21e1
Fix silent session refresh spawning hourly browser windows (beta #5) (#55)
* Fix and instrument the windowless session refresh (beta #5)

The silent-refresh net path failed silently and always fell back to a
hidden sign-in window; that window leaked a visible browser window and the
failure backoff reopened it hourly (10+ overnight).

- buildPartitionPost now sends credentials:'include' on session.fetch. The
  two refresh POSTs authenticate only with the partition's httpOnly Plaud
  cookies, and fetch's default same-origin credentials mode sent none, so
  every refresh 401'd and fell back. Most-likely root cause.
- performNetRefresh takes an optional log sink and reports the exact failure
  (which step, HTTP status, envelope status, body snippet), so one debug run
  pinpoints the cause instead of failing silently. Never logs token values.
- tryNetRefresh logs when no session.fetch transport exists and passes the
  logger through.

Not yet hands-on validated: needs a 'Refresh session now' run with debug on
to confirm the credentials fix works or surface the real cause.

* Address CodeRabbit: redact tokens in logged bodies, make log sink non-throwing

- bodySnippet redacts JWT-shaped substrings before logging, so a stray
  token in a failing auth-endpoint body can never reach the debug log.
- performNetRefresh wraps the injected log sink in try/catch so a throwing
  logger cannot break its documented never-throws contract (it runs in the
  background refresh timer). Tests for both.
2026-07-08 09:48:44 -04:00
Charles Kelsoe
fb6b8bbe4e
Add extra frontmatter with token mapping and per-property preserve (#53)
* Add extra frontmatter with token mapping and per-property preserve

New Extra frontmatter setting: an expanding rowset of key / value /
preserve properties written to each imported note. Values expand the
same {{ }} tokens as the other fields (date set, {{title}},
{{plaud-folder}}) plus content tokens from the recording and its AI
summary ({{category}}, {{headline}}, {{duration}}, {{language}},
{{template}}, {{model}}).

Per-property preserve: on a re-import, a preserve-on property keeps the
note's existing value; preserve-off refreshes it. plaud-id is locked out
of the system as the note's immutable identity.

- note-writer.ts: CustomFrontmatterRow/Context types,
  expandCustomFrontmatterValue, extractFrontmatterValues, keyed-map
  formatFrontmatter with merge, renderCustomFrontmatterPreview; reorder
  NoteWriter.writeNote to read the existing note before building markdown
  so the merge sees current values (shared buildNote helper).
- main.ts: customFrontmatter setting + default, renderCustomFrontmatterControl
  rowset UI with token palette and live preview, registered in both
  display paths, threaded through buildImportRuntimeOptions so manual and
  auto-sync behave identically.
- styles.css: rowset layout; pre-wrap on the multi-line preview.
- Tests for the expander, frontmatter reader, merge/preserve, and preview.

Concept proposed by @jtsmith2 in PR #50 (closed unmerged); rebuilt with
tests, content tokens, and preserve.

* Address Codex review and rework the Extra frontmatter settings UI

Codex pre-PR review fixes (note-writer.ts):
- yamlScalar the frontmatter key so a custom name needing quoting (a
  colon, a leading digit) cannot break the block; no-op for built-in keys.
- expandCustomFrontmatterValue now resolves tokens in a single pass, so a
  substituted value that itself contains braces is not re-interpreted.
- extractFrontmatterValues captures block scalar (| / >) bodies so a
  preserved multi-line value round-trips instead of collapsing to the
  indicator. Tests added for all three.

Settings UI (main.ts, styles.css): the control defaulted to zero rows with
the add button buried among token buttons, so there was no visible way to
create a property. Now it always shows one editable row, adds column
headings, a prominent Add property button on its own line, and a labeled
token palette below. Blank rows are not persisted.

* Address CodeRabbit review: reserve plugin keys, parse quoted keys, clear focus ref

- note-writer.ts: a custom row can no longer override a plugin-managed
  frontmatter field. RESERVED_FRONTMATTER_KEYS lists every key the plugin
  emits; a matching custom row is skipped, so extra properties only add,
  never override identity, the auto-sync cursor, or the built-in fields.
- extractFrontmatterValues now parses quoted keys (a name with a colon or
  spaces, which yamlScalar emits) and unquoted keys containing spaces, so
  preserve round-trips for those keys instead of silently regenerating.
  Added parseLeadingQuotedScalar.
- main.ts: clear lastFocusedValue when the rows re-render, so a token
  button never inserts into a detached input after add/remove.
- Tests for reserved-key skip, quoted/space key parsing, and preserve
  round-trip for those keys.

* Polish the Extra frontmatter settings UI

- Seed a real, editable default row (Recording Source: Plaud Importer) so
  the setting is self-documenting on a fresh install.
- Drop placeholder text; column headings (Property / Value / Preserve)
  label the fields, so nothing reads as a fake default.
- Lay the header and every row's inputs out as direct cells of one CSS
  grid, so each heading is left-aligned over its column (earlier separate
  grids drifted). Preserve is a checkbox under its heading; blank rows are
  never persisted.
2026-07-08 09:06:51 -04:00
Charles Kelsoe
9306e291f9
Default the subfolder template to {{YYYY}}/{{MM}} (issue #45) (#49)
The subfolder template defaulted to empty, filing every note flat into the output folder and surprising users who expected a dated layout. Default fresh installs to a year/month tree instead. Only new installs are affected: an existing saved value (including a deliberately cleared one) is untouched, and the note writer still supports a flat layout when the template is empty.
2026-07-07 11:19:51 -04:00
Charles Kelsoe
07d2080433
Document that new recordings need Plaud to finalize before import (issue #47) (#48)
A recording is not importable until Plaud web/app has opened and its cloud has finalized it. Add a Troubleshooting bullet so users who hit the sync delay know it is a Plaud-side step, not a plugin fault.
2026-07-07 10:09:28 -04:00
Charles Kelsoe
9608006695 Release 0.25.0-beta.1: silent Plaud session refresh (beta)
First beta of Release A (one-click reconnect) + Release B (silent session refresh). Ships to BRAT testers before any production tag; the refresh path needs live Plaud cookies and is validated hands-on, not in CI.
2026-07-07 08:13:57 -04:00
Charles Kelsoe
2d34f5a1ed
Add silent Plaud session refresh (issue #5) (#40)
Release B: silent Plaud session refresh. Direct windowless refresh (POST /auth/refresh-user-token then the workspace/token mint over the persist:plaud-importer partition) as primary, hidden-window re-capture as fallback. Shared fail-safe replaces the stored token only on a fresh typ-WT. 906 tests green. Not yet hands-on validated; ships to BETA first.
2026-07-07 08:09:45 -04:00
Charles Kelsoe
ee47d87212
Add one-click reconnect on auth-pause and backfill failure (issue #5) (#39)
* Add one-click reconnect on auth-pause and backfill failure (issue #5)

When background auto-sync pauses on an expired or missing token, the pause notice now carries a Reconnect action that runs sign-in and resumes auto-sync on success, instead of pointing the user at settings. The Backfill version markers command does the same on an auth failure (Reconnect and retry).

showActionNotice builds a sticky, keyboard-activatable notice; a reauthInFlight guard blocks concurrent sign-in windows; onunload hides any open action notice so its handler cannot run after unload; reconnectFromNotice owns its result messaging for every outcome (success, closed, error).

* Address review: reconnect messaging, notice cleanup, focus style

CodeRabbit/Copilot findings on #39: suppress the misleading 'sign-in closed' notice when reauthenticate short-circuits on the concurrent-signin guard; drop notices from the tracking Set on manual dismiss (messageEl click); add a :focus-visible ring for the keyboard-focusable action span; correct the reauthenticate doc comment to note the single 'already open' notice it now shows.
2026-07-06 16:39:23 -04:00
Charles Kelsoe
da9550d4e7
Add CI guard validating ribbon-icon ids against Lucide (follows up #34) (#38)
scripts/check-icons.mjs validates every hardcoded Lucide icon id in main.ts (the RIBBON_ICON_CHOICES list, DEFAULT_RIBBON_ICON, and literal setIcon ids) against the offline lucide-static package, chained into npm run lint. An unknown id (like the shipped #34 bug 'tape' vs 'cassette-tape') now fails CI instead of rendering a blank icon. Strips comments before extraction and preflights missing main.ts / lucide-static with clear messages. Caveat noted in-script: catches ids that exist in no Lucide, not per-version drift vs Obsidian's bundled set.
2026-07-06 11:59:55 -04:00
Charles Kelsoe
f230f3730d Release 0.24.0: {{plaud-folder}} subfolder token
Adds a {{plaud-folder}} token to the subfolder template that mirrors Plaud folders into the vault; unfiled recordings file under _unfiled (issue #16 follow-up).
2026-07-06 11:37:38 -04:00
Charles Kelsoe
886539ea34
Add {{plaud-folder}} subfolder token (issue #16 follow-up) (#37)
* Add {{plaud-folder}} subfolder token (issue #16 follow-up)

The subfolder template supports a {{plaud-folder}} token that expands to the recording's Plaud folder name, so {{plaud-folder}}/{{YYYY}} mirrors Plaud folders into the vault tree. Plaud folders are flat, so a slash in a name is flattened to the replacement char, not treated as nesting; an unfiled recording files under _unfiled; a multi-folder recording uses the first.

Threads the resolved folder names (already on formatOptions.folders) into NoteWriter.resolveTargetPath. The placeholder path resolves no folders and lands in _unfiled; cross-folder dedup (existingPathForPlaudId + migrateExistingNote) migrates such a stub to the real folder on the later successful import (covered by a new integration test).

Build, lint, and 880 tests green. Codex review clean.

* Address PR #37 review: make {{plaud-folder}} expand to _unfiled inline

CodeRabbit/Copilot: the docs promised {{plaud-folder}} files an unfiled recording under _unfiled, but the old code only bucketed when the WHOLE subfolder resolved empty; {{plaud-folder}}/{{YYYY}} silently dropped the empty segment and mixed unfiled recordings into the date folder. An unfiled recording now expands the token to a literal _unfiled segment inline, so it is always visibly grouped (_unfiled, _unfiled/2026, 2026/_unfiled), matching the docs. Removed the now-redundant _unfiled result-empty bucket; the _untitled title bucket is unchanged.

Also: added a multi-folder writeNote test asserting folders[0] is used (CodeRabbit nitpick), and corrected two stale comments (resolveTargetPath same-path, note-name token vocabulary). Build, lint, 881 tests green. Codex review clean.
2026-07-06 11:36:48 -04:00
Charles Kelsoe
0c1bc19161 Release 0.23.0: {{title}} subfolder token and configurable forbidden-char replacement
Adds {{title}} in the subfolder template for folder-note layouts and a configurable forbidden-character replacement setting (issue #30 follow-up).
2026-07-06 10:51:14 -04:00
Charles Kelsoe
16917409df
Add {{title}} subfolder token and configurable forbidden-char replacement (issue #30 follow-up) (#35)
* Add {{title}} subfolder token and configurable forbidden-char replacement (issue #30 follow-up)

The subfolder template now supports {{title}} for folder-note layouts. The title has any leading date stripped (parity with the note name) and is sanitized into a single legal folder segment: path separators are flattened, forbidden characters replaced, reserved device names prefixed, trailing dot/space stripped, and length clamped, so an arbitrary recording title can never abort folder resolution. A title that reduces to empty files under _untitled instead of collapsing into the output root.

New forbidden-character replacement setting (default '-') chooses the stand-in for a character a file name or folder cannot hold, applied to both note names and folders. Validated to a safe single char via isValidReplacementChar, enforced in the settings UI, coerced on load (so a hand-edited data.json cannot feed an unsafe char to the rename command or imports), and guarded again in the NoteWriter constructor.

sanitizeFilename and sanitizeFolderSegment take a replacement param (function replacer, so a '$' is literal) plus an emptyFallback param. Build, lint, and 867 tests green. Codex review clean after fixes for title-segment assertion aborts, the rename-path validation gap, and preview/import char desync.

* Address PR #35 review: harden sanitizer replacement, align Notice text

Copilot: sanitizeFilename (exported) and sanitizeFolderSegment now coerce an unsafe replacement to '-' via isValidReplacementChar before use, so a caller that bypasses settings/NoteWriter validation cannot reintroduce a forbidden character. Production callers already pass a validated char; this is defense-in-depth. Adds a test.

Copilot: the invalid-replacement Notice now lists backslash and control characters, matching isValidReplacementChar's actual rejection set.
2026-07-06 10:50:18 -04:00
Charles Kelsoe
94d8aca247 Release 0.22.1: fix Cassette tape ribbon icon id
Corrects the invalid Lucide id 'tape' to 'cassette-tape' (issue #34) so the ribbon icon renders instead of disappearing.
2026-07-06 10:43:14 -04:00
Charles Kelsoe
5ee620ba62
Fix Cassette tape ribbon icon id (issue #34) (#36)
The ribbon icon choice labeled Cassette tape used the Lucide id 'tape', which does not exist in Lucide, so selecting it left the ribbon with no icon. Corrected to 'cassette-tape'.

Verified all 12 ids in RIBBON_ICON_CHOICES against the Lucide registry over the network: 'tape' returns 404, 'cassette-tape' and the other 11 all exist. Only this one id was wrong.
2026-07-06 10:42:01 -04:00
Charles Kelsoe
2985690ae9 Release 0.22.0: datetime frontmatter property and time tokens
Adds a configurable datetime: frontmatter property (issue #32) and surfaces time tokens on every template field. date: stays frozen at YYYY-MM-DD.
2026-07-06 09:52:08 -04:00
Charles Kelsoe
c913414daf
Add configurable datetime frontmatter property (issue #32) (#33)
* Add configurable datetime frontmatter property (issue #32)

A new setting writes a datetime: property to each imported note, formatted with the same {{ }} Moment tokens as the folder and note-name fields. Empty by default (no property emitted); a non-empty template turns it on, mirroring the subfolder field's on/off model.

date: stays frozen at YYYY-MM-DD for Dataview stability; datetime: is a separate additive property so the time can be recorded in any format (24h, 12h, ISO 8601 with UTC offset via {{Z}}). Value is machine-local time, the same basis as date:.

Time tokens (Hour, Minute, Second, AM/PM, Offset) are added to the shared insert-token buttons, so they work in the subfolder and note-name templates too. New formatDatetime export is shared by the frontmatter builder and the settings live preview. The datetime line is also emitted in the placeholder-note path so stubs match real notes.

Build, lint (eslint + obsidianmd + check-submission), and 839 tests green. Codex review clean.

* Address PR #33 review: fix misleading ISO example, add placeholder datetime tests

Copilot: the ISO datetime example hard-coded a +00:00 offset, misleading for users outside UTC. Show it as a local-offset placeholder (±hh:mm) instead; the live preview renders the real offset.

CodeRabbit: add formatPlaceholderMarkdown datetime coverage (emit when set, quoted; omit when empty) so the placeholder path cannot silently desync from formatFrontmatter.
2026-07-06 09:30:29 -04:00
Charles Kelsoe
323126bdd5 Release 0.21.1: fix marketplace type-check warnings on Moment usage
The developer-dashboard scan resolves Obsidian's re-exported moment as the
error/any type, tripping @typescript-eslint/no-unsafe-* on the date formatter.
Pin the factory to moment's own module type (typeof import('moment')) at that
one boundary, add moment as an explicit devDependency so its types resolve, and
turn off no-unnecessary-type-assertion in config (the cast is redundant only in
environments where moment already resolves). Also bump eslint-plugin-obsidianmd
to 0.4.1 (was stuck at 0.3.0 despite the ^0.4.0 pin). No behavior change; 834
tests pass. Verified the cast clears no-unsafe under a simulated any-moment.
2026-07-05 19:29:09 -04:00
Charles Kelsoe
3dd3918c77 docs: note-name README lists all rejection reasons, matching the app
The note-name section described rejection as forbidden characters only; align
it with isValidNoteNameTemplate and the in-app failure messages, which also
reject reserved device names, leading/trailing dot or space, and over-200-char
results. Docs-only.
2026-07-05 18:39:41 -04:00
Charles Kelsoe
932cd07b39 Release 0.21.0: real Moment date formats and token parity for both templates 2026-07-05 18:34:45 -04:00
Charles Kelsoe
9e04382d64
Real Moment date formats and token parity for subfolder and note-name templates (#31)
Issue #30: both the subfolder and note-name templates now use real Moment.js date formatting via the externalized obsidian moment, with one shared vocabulary so every token works in both fields. Existing templates migrate output-preserving under a settingsVersion gate. Adds live preview + insert-token buttons, and hardens Windows path-component safety (forbidden chars, reserved device names including with extensions, control chars, trailing dot/space, and length) symmetrically across the note-name and subfolder paths.
2026-07-05 18:26:26 -04:00
Charles Kelsoe
8db46bc20b Release 0.20.0: note-name template, rename cascade, and Plaud title write-back
Bundles the four merged features that had accumulated in [Unreleased]:
- Date-prefix for dateless recording titles.
- Configurable note-name template ({{...}} tokens + {{title}}).
- Rename cascade: rename a note and its -assets folder together (command,
  context menu, file-explorer listener), and auto-migrate a note when the
  recording is renamed in Plaud.
- Optional Plaud title write-back on rename (autoUpdatePlaudTitle, default off,
  confirm-first), the plugin's only cloud write, with sign-in-and-retry on an
  expired token.

README updated for all four, including the new opt-in write to Plaud.
2026-07-04 19:27:37 -04:00