Maintenance pass: dependency refresh + streaming-citations bug fix
We are maintained. Updated all packages, solved for dependabot flags, and fixed a streaming-citations bug.
Why Care?
This is the first changelog the Perplexed plugin has ever had. The
project went from init (7e6831c) through 47 commits without one,
and the last touched commit before today (a13b4e2 Update styles.css,
2025-05-01) left the working tree in a state where:
pnpm-lock.yamlwas out of sync with whatnode_moduleshad been installed by; runningpnpm outdatedreported every package as "missing".Every direct dependency had drifted at least one minor behind current latest, and three were a full major behind: ESLint (9 → 10), TypeScript (5 → 6), and
@types/node(24 → 25).A latent bug in
handleStreamingResponsewas silently dropping Perplexity'ssearch_results/citationsmetadata on the final SSE chunk, so users saw plenty of streamed answer text but no reference list at the end.
The plugin was "working as is" for the user's day-to-day usage — they weren't blocked. The maintenance window opened because the toolchain drift had reached the point where any future bug fix would have to fight an outdated lockfile and a yellow-triangle-littered npm advisory list before it could even start. So: refresh the toolchain, lock in working configs against the new majors, and fix anything the refresh exposed.
The strategic point: the two tracks are coupled by intent but
independent in scope. Track 1 (deps) has to be safe — every step
verified against pnpm run build so the production bundle survives.
Track 2 (citations bug) was discovered in the same session because
the user reported it after the refresh; the bug pre-dates the refresh
and is unrelated to any version change. Both ship together so future
maintenance has a single, coherent baseline.
What Was Built
Two coordinated tracks: (1) refresh every dependency to current latest, including the two majors that required config migration — ESLint 9 → 10 (flat config) and TypeScript 5 → 6 (deprecated tsconfig options); (2) fix a streaming-response bug where a misnamed 'stale data' cleanup was wiping captured Perplexity citations on the final SSE chunk, so the rendered note never got a Citations reference section. Build verified clean (tsc + esbuild production). One known-flaky behavior — streaming responses occasionally truncate mid-answer with no error in the console — is documented under Open Items but not diagnosed; the user explicitly deferred ('something is funky I don't want to diagnose, or it's an api or credit thing')."
Dependency refresh (Track 1)
| Package | Was | Now | Major? |
@modelcontextprotocol/sdk | 1.15.0 | 1.29.0 | minor |
dotenv | 17.2.0 | 17.4.2 | patch |
fastify | 5.4.0 | 5.8.5 | minor |
zod | 4.0.0 | 4.4.2 | minor |
@types/node | 24.0.12 | 25.6.0 | major |
@typescript-eslint/eslint-plugin | 8.36.0 | 8.59.1 | minor |
@typescript-eslint/parser | 8.36.0 | 8.59.1 | minor |
builtin-modules | 5.0.0 | 5.1.0 | minor |
esbuild | 0.25.6 | 0.28.0 | minor (0.x) |
eslint | 9.30.1 | 10.3.0 | major |
obsidian | 1.8.7 | 1.12.3 | minor |
tslib | 2.8.1 | 2.8.1 | unchanged |
typescript | 5.8.3 | 6.0.3 | major |
typescript-eslint (added) | — | 8.59.1 | new |
@eslint/js (added) | — | 10.0.1 | new |
globals (added) | — | 17.6.0 | new |
Method: install the existing lockfile baseline first
(pnpm install), confirm pnpm run build is green, then bump in
three bands so any breakage is isolated:
All minor / patch bumps in one shot.
@types/node24 → 25 (types-only, lowest blast radius).eslint9 → 10 (config migration required).typescript5 → 6 (tsconfig migration required).
Build re-verified after each band.
ESLint v10 — flat config migration (Track 1, fix-out)
ESLint 10 dropped legacy .eslintrc and .eslintignore support
entirely. Running eslint with the old config produced:
ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
From ESLint v9.0.0, the default configuration file is now eslint.config.js. Migration:
Created
eslint.config.mjs— the new flat-config equivalent of the old.eslintrc. SameparserOptions(ecmaVersion: latest,sourceType: module,project: ./tsconfig.json), same rule set (@typescript-eslint/no-unused-vars,consistent-type-imports,no-explicit-anyas warn, etc.), same ignores (node_modules/,main.js).Adopted the unified
typescript-eslintmeta-package (v8.59.1) and added@eslint/js+globalsbecause the flat-config style composes configs as imported objects rather than string extends.Deleted
.eslintrcand.eslintignore.
Important constraint: the build script does not run ESLint
(tsc -noEmit -skipLibCheck && node esbuild.config.mjs production),
so pnpm run build would have stayed green even if lint were broken.
Verified the new config separately by running npx eslint main.ts
src/ and confirming it parses + emits results. The 42 errors / 33
warnings it emits are pre-existing code-quality nits (mostly
type-only-import refactors and any warnings); not part of this
maintenance pass.
TypeScript 6 — tsconfig modernization (Track 1, fix-out)
TypeScript 6 promoted two tsconfig.json deprecation warnings to
hard errors:
tsconfig.json(4,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0.
tsconfig.json(7,25): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Resolution: modernize rather than silence with
ignoreDeprecations: "6.0".
| Field | Was | Now | Why |
baseUrl | "." | (removed) | Only existed to anchor the paths map below; paths was a no-op (see next row) so neither is needed |
paths | { "*": ["node_modules/*"] } | (removed) | This pattern is a no-op restatement of default Node module resolution; deleting both clears the deprecation cleanly |
moduleResolution | "node" (= node10) | "bundler" | Correct semantic match for esbuild-as-bundler — preserves the same import behavior the project relies on without the deprecation |
Build clean after the change. No source code modifications were
required — none of the imports were depending on the legacy
baseUrl + paths indirection.
Streaming citations bug fix (Track 2)
Bug: in src/services/perplexityService.ts, the
handleStreamingResponse SSE-loop captured Perplexity's response
metadata into a local finalResponseData variable as it streamed,
but then immediately re-checked the same chunk and cleared the
captured value if the chunk was the final one without metadata. Code
as it was:
if (parsed.citations || parsed.images || parsed.search_results) {
finalResponseData = parsed;
}
if (parsed.choices?.[0]?.finish_reason === 'stop' &&
!parsed.citations && !parsed.images && !parsed.search_results) {
if (finalResponseData && !finalResponseData.choices?.[0]?.finish_reason) {
console.log('🧹 Clearing potentially stale finalResponseData');
finalResponseData = null;
}
} Why this is wrong:
finalResponseDatais a local variable created at the top ofhandleStreamingResponse(line 609). Its scope is one request. It cannot ever be "stale from a previous request" — there is no previous request to be stale from.Perplexity's normal SSE flow puts
search_results/citationsin earlier chunks. The final chunk usually carries onlychoices[0].finish_reason: 'stop'with no metadata. That is exactly the condition the cleanup fires on.The cleanup also requires that the captured chunk doesn't have its own
finish_reason— which is normal, because metadata chunks aren't the terminal ones. So the gate is "if metadata came in earlier, clear it now," which is precisely backwards.
Result: every streaming Perplexity request that completed normally
discarded its citations before processStreamingMetadata could
write them to the editor. Symptom the user reported: "I don't get
the citations reference section."
Fix: removed the cleanup branch. Replaced the simple "last-write- wins" capture with a merge so partial metadata across chunks is preserved:
if (parsed.citations || parsed.images || parsed.search_results) {
finalResponseData = {
...(finalResponseData || {}),
...parsed,
};
} Build clean. Bundle written to main.js for testing in the user's
vault.
Verification
pnpm run build(production:tsc -noEmit -skipLibCheck && node esbuild.config.mjs production) green after each of the four dependency bands and after the citations fix.ESLint flat config verified runnable (
npx eslint main.ts src/) — emits warnings on pre-existing code, but loads and parses the config without error.Production bundle dropped from ~492 KB to ~84 KB. This is not a regression: the previous artifact on disk was a dev build with inline source maps (
!isProduction ? 'inline' : falseinesbuild.config.mjs:50), and the new one is a true production minified build with sourcemaps disabled. Bundle contents spot-checked (grep getReader|TextDecoder|reader.read main.js) to confirm streaming primitives are intact.
What Changed in Approach (the meta-lesson)
| Pattern this rejects | Pattern this adopts |
| Bump every dependency in one go and hope the build survives | Bump in bands by blast radius (minors → types-only majors → config-affecting majors), verify build after each band, isolate any breakage to one band |
Silence TypeScript deprecation warnings with ignoreDeprecations | Modernize the offending config — TS deprecations are early-warning for TS7 hard removals, not noise to mute |
Treat .eslintrc as the canonical config until it physically stops working | Migrate to flat config the moment the major bump requires it; flat config is now the only forward-compatible shape |
| Pre-emptive "stale data" cleanup gated on heuristics about chunk shape | Trust local-scope variables to be local; if a guard isn't justified by an actual cross-request risk, it's just a foot-gun — delete it |
| No changelog because the project has never had one | First changelog establishes the habit; even a small maintenance pass gets one so the next pass has a baseline to diff against |
The generalizable point: a maintenance pass is not just pnpm
update --latest. The two majors here both required real config
work (ESLint flat config, TS 6 tsconfig modernization), and the
session also surfaced one runtime bug that was unrelated to the
refresh but worth fixing in the same window. Bundling all of that
into one changelog establishes the baseline; the next maintenance
pass can diff cleanly against this one.
Open Items
Streaming truncates mid-answer, intermittently, with no console error. User reported it during this session; first prompt truncated, second prompt succeeded. No
Streaming Error:notice was raised, no red console output, the loop just stops writing. Working hypothesis (unverified): either Perplexity's server is closing the SSE connection early under some condition (rate limiting, account / credit state, request shape) or Obsidian's Electronfetchis closing the underlying socket on idle. The streaming code inhandleStreamingResponsehas noAbortControllerand no timeout, so the truncation is not client-initiated. User explicitly deferred diagnosis: "something is funky I don't want to diagnose, or it's an api or credit thing." Defensive fix to consider next pass: add anAbortControllerwith a generous timeout (5+ min forsonar-deep-research) so the failure mode at least surfaces a clear error instead of silent truncation.TextDecoder is recreated every chunk in
handleStreamingResponse. Each iteration runsnew TextDecoder().decode(value, { stream: true }). Thestream: trueflag is meant to carry partial multi-byte UTF-8 state across chunks, but a fresh decoder per chunk discards that state every time — so multi-byte characters split across a chunk boundary will be replaced with U+FFFD and the resulting JSON may fail to parse (caught silently in the per-line try/catch and logged as a warning). Pre-existing; not surfaced by this pass. Fix: hoist the decoder outside thewhile (true)loop. Held for a separate pass so this changelog stays scoped.Three runtime dependencies are declared but never imported.
fastify,@modelcontextprotocol/sdk, andzodare all independenciesinpackage.jsonbut not referenced bymain.tsor any file insrc/. Likely legacy from earlier scaffolding. Removing them would shrink the install footprint and the npm advisory surface area; not done here to keep the maintenance pass scoped to "refresh + fix the bug the user noticed." Worth a follow-up pass.Forty-two pre-existing ESLint errors / 33 warnings. Surfaced for the first time because the flat config migration meant we actually ran lint. Mostly
consistent-type-imports(auto- fixable), unused destructured imports insrc/types/obsidian.d.ts, onelet → constinperplexityService.ts:608, and@typescript-eslint/no-explicit-anywarnings throughout. None block the build (tsc + esbuilddoesn't run lint).npx eslint . --fixwould clear roughly half of them mechanically. Held — separate intent from the maintenance pass.Version not bumped.
manifest.jsonandpackage.jsonboth still read0.0.0.1. The plugin's versioning scheme is pre-release / non-semver-conformant; the user did not request a bump in this pass. If the citations fix gets shipped to users, consider bumping then.Commit not created. All changes (config files,
package.json,pnpm-lock.yaml,tsconfig.json,perplexityService.ts,eslint.config.mjs, this changelog) are staged in the working tree only. User has not requested a commit yet.
Files Touched
perplexed/
├── package.json (deps refreshed across 13 packages; added typescript-eslint, @eslint/js, globals)
├── pnpm-lock.yaml (regenerated against the new package.json)
├── tsconfig.json (removed baseUrl + paths; moduleResolution node → bundler)
├── eslint.config.mjs (created — flat-config equivalent of the deleted .eslintrc)
├── .eslintrc (deleted — superseded by eslint.config.mjs)
├── .eslintignore (deleted — superseded by ignores in eslint.config.mjs)
├── src/
│ └── services/
│ └── perplexityService.ts (handleStreamingResponse: removed citations-eating "stale data" cleanup; replaced last-write-wins capture with merge across metadata chunks)
└── changelog/
└── 2026-05-02_01.md (created — this file, first changelog for this plugin) main.js and styles.css are build artifacts; rebuilt by
pnpm run build and not part of the source-of-truth diff.
Reference
Predecessor changelogs: None — this is the first.
Pattern reference for changelog format:
/Users/mpstaton/code/lossless-monorepo/cite-wide/context-v/changelogs/2026-05-02_01.md(cite-wide v0.2.0 release notes — frontmatter shape, section layout, "What Changed in Approach" / "Open Items" / "Files Touched" conventions).The bug-bearing function:
src/services/perplexityService.ts→handleStreamingResponse(line 594) andprocessStreamingMetadata(line 713).Build pipeline:
package.jsonscripts.build(tsc -noEmit -skipLibCheck && node esbuild.config.mjs production);esbuild.config.mjsfor bundle config (targetes2022, formatcjs, externals includeobsidian,electron, all@codemirror/*, all@lezer/*, plusbuiltin-modules).ESLint flat-config docs: https://eslint.org/docs/latest/use/configure/migration-guide (referenced during the v10 migration).
TypeScript 6 deprecations notice: https://aka.ms/ts6 (referenced during the tsconfig modernization).
Recent commits on
development(pre-this-session):a13b4e2 Update styles.css,f75afbf Add WARP.md documentation for development workflow,153894f improve: Deep Research Streaming— this maintenance pass starts froma13b4e2.