diff --git a/.npmrc b/.npmrc index 90a9959..adda767 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,4 @@ shamefully-hoist=true auto-install-peers=true -shared-workspace-lockfile=false \ No newline at end of file +shared-workspace-lockfile=false +ignore-workspace=true \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..940014f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 The Lossless Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/context-v/issue-resolutions/Getting-Claude-to-Respond-With-Research.md b/context-v/issues/Getting-Claude-to-Respond-With-Research.md similarity index 100% rename from context-v/issue-resolutions/Getting-Claude-to-Respond-With-Research.md rename to context-v/issues/Getting-Claude-to-Respond-With-Research.md diff --git a/context-v/issue-resolutions/Widen-Modals-in-Obsidian-using-CSS.md b/context-v/issues/Widen-Modals-in-Obsidian-using-CSS.md similarity index 100% rename from context-v/issue-resolutions/Widen-Modals-in-Obsidian-using-CSS.md rename to context-v/issues/Widen-Modals-in-Obsidian-using-CSS.md diff --git a/context-v/plans/20206-05-02_Assuring-Obsidian-Community-Plugin-Requirements.md b/context-v/plans/20206-05-02_Assuring-Obsidian-Community-Plugin-Requirements.md new file mode 100644 index 0000000..757ebe8 --- /dev/null +++ b/context-v/plans/20206-05-02_Assuring-Obsidian-Community-Plugin-Requirements.md @@ -0,0 +1,177 @@ +--- +title: "Plan — Bring Perplexed up to Obsidian Community-Plugin Publishing Standards" +status: Proposed +created: 2026-05-02 +applies_to: perplexed Obsidian plugin +authors: + - Michael Staton +augmented_with: Claude Code (Opus 4.7, 1M context) +related_reference: ../../cite-wide/context-v/reminders/Obsidian-Type-Safety.md +--- + +# Plan — Bring Perplexed up to Obsidian Community-Plugin Publishing Standards + +## Context + +The Perplexed Obsidian plugin (`/Users/mpstaton/code/lossless-monorepo/perplexed`) is being prepped for submission to the Obsidian community plugin marketplace. Its sister plugin `cite-wide` was rejected last week for type-safety violations (specifically `any` usage), and the rules that triggered that rejection are enforced automatically by `ObsidianReviewBot` on every submission PR with **no appeal mechanism**. There is also a set of metadata/repo-hygiene requirements that block submission independently. + +The reference doc — `/Users/mpstaton/code/lossless-monorepo/cite-wide/context-v/reminders/Obsidian-Type-Safety.md` — captures the rules verbatim from the review bot and the patterns to satisfy them. This plan applies those rules to Perplexed and bundles in the non-type-safety publishing fixes uncovered during the audit. + +**Audit totals:** 25 explicit-`any` sites + 3 `as any` casts + 5 metadata/repo blockers + ESLint config that lets `any` through as a warning. No floating-promises, no `[object Object]` interpolations, no `innerHTML`, no `(window as any)` patterns, no hand-rolled YAML (Perplexed doesn't touch frontmatter — different surface area than cite-wide). Critical files all reviewed. + +--- + +## Phase 1 — Tighten ESLint to Match the Review Bot + +**Why first:** if the local lint config matches the bot, we catch every remaining issue during `pnpm build` instead of at submission. The current config (`eslint.config.mjs:29`) has `no-explicit-any: "warn"` which lets violations through. + +**File:** `/Users/mpstaton/code/lossless-monorepo/perplexed/eslint.config.mjs` + +Change the `rules` block: +- `"@typescript-eslint/no-explicit-any": "error"` (was `"warn"`) +- Add `"@typescript-eslint/no-unnecessary-type-assertion": "error"` +- Add `"@typescript-eslint/no-floating-promises": "error"` +- Add `"@typescript-eslint/no-base-to-string": "error"` +- `"@typescript-eslint/no-unused-vars": ["error", { args: "none" }]` — already present, keep +- Add CLI flag in `package.json` build script: `eslint . --report-unused-disable-directives` before `tsc`. Current build script: `"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"` — change to `"build": "eslint . --report-unused-disable-directives && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"`. + +After this change, `pnpm build` will fail loudly. That failure surface drives the rest of the plan. + +--- + +## Phase 2 — Fix Manifest, Versions, and Package Metadata + +**Why:** four-part version strings (`X.Y.Z.W`) are not semver and the bot rejects them. The `minAppVersion` placeholder will also be flagged. + +### 2.1 `manifest.json` +- `version`: `"0.0.0.1"` → `"0.0.1"` +- `minAppVersion`: `"0.0.0.1"` → `"0.15.0"` (matches the sole entry in `versions.json`; safe baseline for current Obsidian APIs we use) + +### 2.2 `versions.json` +- Key `"0.0.1.0"` → `"0.0.1"` (must match the new manifest version, must be valid semver) + +### 2.3 `package.json` +- `version`: `"0.0.0.6"` → `"0.0.1"` (sync to manifest) +- Remove unused dependencies confirmed not imported anywhere in `src/` or `main.ts`: + - `fastify` + - `@modelcontextprotocol/sdk` + - `zod` + - Keep: `dotenv`, `@anthropic-ai/sdk` (the latter is used by `claudeService.ts`) +- After removal: `pnpm install` (or `npm install`) to regenerate the lockfile. + +--- + +## Phase 3 — Add LICENSE File + +**Why:** `package.json` declares MIT but no `LICENSE` file exists at the repo root. Obsidian's submission requirements expect the license file to be present and discoverable. + +- Create `/Users/mpstaton/code/lossless-monorepo/perplexed/LICENSE` with the standard MIT license text, copyright year `2025-2026`, copyright holder `The Lossless Group` (matches `manifest.json` author). + +--- + +## Phase 4 — Eliminate `any` (the type-safety pass) + +**Why:** this is the rule that rejected cite-wide. 25 sites + 3 casts must be eliminated. The reference doc's §2 and §5 give the exact replacement patterns. + +Order chosen so each step compiles cleanly before the next: + +### 4.1 Fix the ambient shim — `src/types/obsidian.d.ts:5` + +`commands: any` → minimal documented interface for the methods Perplexed actually calls. Search `main.ts` and services for `app.commands.*` usage and type only what's used (likely `executeCommandById(id: string): boolean` and `commands: Record`). If usage is one-off, fall back to `unknown` and narrow at the call site. + +### 4.2 Fix the logger — `src/utils/logger.ts` + +Six sites, all the same pattern: replace every `details?: any` and `details: any` with `details?: unknown`. +- Line 8: `details?: any;` (in `LogEntry` interface) +- Line 81: `addEntry(... details?: any)` +- Lines 109, 113, 117, 121: `error/warn/info/debug(message: string, details?: any)` + +The logger only stringifies `details` — no structural access — so `unknown` is a drop-in replacement. The one exception is line 86 (`details instanceof Error`), which is already a proper narrowing and works identically against `unknown`. + +### 4.3 Fix the date util — `src/utils/formatDate.ts` + +Lines 41, 58: `source: any` → `source: unknown`. Add an internal `Source` interface (the `getMostRecentDate` and `formatPublicationInfo` functions look for `date_published`, `last_updated`, etc. — read both functions, define the interface from the actual field accesses, then narrow with the `isRecord` guard from the reference doc §3.3 before reading fields). This requires also adding a small `coerce.ts` helper file with `isRecord`, `asString`, and `asDate` (same shapes from the reference doc) — these will be reused in 4.4. + +**New file to create:** `src/utils/coerce.ts` — copy the `asString`, `asNumber`, `asStringArray`, `asDate`, `isRecord` helpers verbatim from the reference doc §3.3. They are general-purpose and will pay for themselves several times in 4.4. + +### 4.4 Fix the API service payload types — `src/services/{perplexity,perplexica,lmStudio}Service.ts` + +Three services share the same shape problems: +- `promptsService?: any` and `private promptsService: any` (in constructor options + class field) +- `let payload: any` (request body builder) +- API response objects like `finalResponseData: any` and `images: any[]` + +**Per-service fixes:** + +`perplexicaService.ts` — Lines 7, 17, 77. +- Replace `promptsService` field/option type with `import type { PromptsService } from './promptsService'`. (Direct import; the cyclical-init concern in the reference doc §5.5 is about singleton patterns, which Perplexed doesn't have — these are constructor params.) +- Define `interface PerplexicaPayload { ... }` from the actual fields assigned (read lines ~70–110 to see what's set on `payload`). + +`perplexityService.ts` — Lines 14, 21, 51, 232, 397, 609, 707, 761, 777, plus the three `as any` casts at 225/227/228. +- Same `PromptsService` import for fields/options. +- Define `interface PerplexityPayload`, `interface PerplexityImage`, `interface PerplexitySource`, and `interface PerplexityResponse` from the response shape Perplexity returns (read lines 600–800 to enumerate fields actually accessed). Make the response interface use `unknown` for fields the code doesn't dereference, and concrete types for fields it does. +- The `loadingInterval` casts (`(this as any).loadingInterval`) are the exact pattern in reference doc §5.5: declare `private loadingInterval: ReturnType | null = null;` on the class and remove all three casts. +- Lines 51 and 232 — `images: any[]` and `sources: any[]` — type with `PerplexityImage[]` and `PerplexitySource[]`. + +`lmStudioService.ts` — Lines 13, 19, 82, 93. +- `PromptsService` import (same as above). +- `const messages: any[] = []` → `const messages: ChatMessage[] = []` where `interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string }` (OpenAI-compatible shape used by LM Studio). +- `let payload: any` → `interface LMStudioPayload { ... }`. + +### 4.5 Verify no `any` remains + +After the above, run `pnpm build`. The eslint step from Phase 1 will fail if any `any` slipped through. Fix any newly surfaced sites the same way (type with a minimal interface, narrow with `isRecord`, or use `unknown`). + +--- + +## Phase 5 — Console-Log Hygiene (recommended, not blocking) + +**Why:** the audit found 86 `console.*` calls in `main.ts`. These are not a hard ObsidianReviewBot rejection criterion, but the published guidelines discourage shipping debug logging. They also leak internals (paths, model names) into the user's devtools console. + +**Decision point — ask the user:** should we (a) remove all console statements, (b) gate them behind a `DEBUG` flag in settings, or (c) route them all through the existing `FileLogger` and drop the console output? The reference doc doesn't mandate a choice; cite-wide chose (c). Recommend (c) for consistency with cite-wide. + +This phase can ship in a follow-up PR if the user wants to keep the type-safety/metadata fix focused. + +--- + +## Verification + +After the implementation, run from `/Users/mpstaton/code/lossless-monorepo/perplexed`: + +1. **Lint clean:** `pnpm exec eslint . --report-unused-disable-directives` — expect zero errors. Specifically, expect zero `@typescript-eslint/no-explicit-any` errors. +2. **TypeScript clean:** `pnpm exec tsc -noEmit -skipLibCheck` — expect zero errors. Strict flags in `tsconfig.json` (already correct, do not weaken) will catch any residual narrowing gaps. +3. **Build clean:** `pnpm build` — expect a clean `main.js` produced. +4. **Manual smoke test in Obsidian:** + - Symlink or copy the built `main.js`, `manifest.json`, `styles.css` into a test vault's `.obsidian/plugins/perplexed/` folder. + - Enable the plugin, run each of: Perplexity research command, Perplexica research command, LM Studio command, Claude command (the WIP one — known broken per recent commits, just verify it doesn't crash on load). + - Confirm settings UI loads, all four endpoint fields render and save. + - Trigger an error path (e.g. invalid API key) and confirm the logger captures it without console-only output. +5. **Resubmission readiness check:** + - `manifest.json` version, `versions.json` key, `package.json` version all equal `"0.0.1"`. + - `LICENSE` file present at repo root. + - `git diff` shows zero `any` remaining in `.ts` files (excluding `node_modules` and `main.js`): `git grep -n ': any\|as any\|\|any\[\]' -- '*.ts' ':!node_modules'` should return nothing from our source files. + +--- + +## Critical Files Touched + +- `eslint.config.mjs` — Phase 1 +- `package.json` — Phase 1 (build script), Phase 2.3 (version, deps) +- `manifest.json` — Phase 2.1 +- `versions.json` — Phase 2.2 +- `LICENSE` — Phase 3 (new) +- `src/types/obsidian.d.ts` — Phase 4.1 +- `src/utils/logger.ts` — Phase 4.2 +- `src/utils/formatDate.ts` — Phase 4.3 +- `src/utils/coerce.ts` — Phase 4.3 (new) +- `src/services/perplexicaService.ts` — Phase 4.4 +- `src/services/perplexityService.ts` — Phase 4.4 (largest single file change) +- `src/services/lmStudioService.ts` — Phase 4.4 + +`main.ts` is **not** in the type-safety touch list — the audit found zero `any` in it. Phase 5 (optional) would touch it for console hygiene. + +## Reused Utilities + +- `coerce.ts` helpers (`asString`, `asNumber`, `asStringArray`, `asDate`, `isRecord`) — verbatim from reference doc §3.3, will be reused across `formatDate.ts` and the three service-payload narrowings in 4.4. +- Existing `FileLogger` singleton in `src/utils/logger.ts` — already present and properly designed, just needs `unknown` substitution. +- Obsidian's typed `App`, `Editor`, `TFile`, `Vault`, `Modal` from `'obsidian'` — already imported throughout; no new shim needed beyond the cleanup of `commands: any`. diff --git a/eslint.config.mjs b/eslint.config.mjs index 512a1c6..8af63b2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,7 @@ import globals from "globals"; export default tseslint.config( { - ignores: ["node_modules/", "main.js"], + ignores: ["node_modules/", "main.js", "**/*.mjs"], }, js.configs.recommended, ...tseslint.configs.recommended, @@ -22,11 +22,14 @@ export default tseslint.config( }, rules: { "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], + "@typescript-eslint/no-unused-vars": ["error", { args: "none", caughtErrors: "none" }], "@typescript-eslint/ban-ts-comment": "off", "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-base-to-string": "error", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/consistent-type-imports": "error", diff --git a/main.ts b/main.ts index 9199744..9889a0a 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,5 @@ -import { App, Editor, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import type { App, Editor} from 'obsidian'; +import { Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; import * as dotenv from 'dotenv'; // Import services diff --git a/manifest.json b/manifest.json index c3b5772..09e5d86 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "id": "perplexed", "name": "Perplexed", - "version": "0.0.0.1", - "minAppVersion": "0.0.0.1", + "version": "0.1.0", + "minAppVersion": "0.15.0", "description": "A plugin for Obsidian that allows you to generate source-cited content using AI using Perplexity and Perplexica.", "author": "The Lossless Group", "authorUrl": "https://lossless.group", diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index c0cdbae..0000000 Binary files a/package-lock.json and /dev/null differ diff --git a/package.json b/package.json index 1d54eb2..c88adaa 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "perplexed", - "version": "0.0.0.6", + "version": "0.1.0", "description": "A plugin for Obsidian that allows you to generate source-cited content using AI using Perplexity and Perplexica.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "build": "eslint . --report-unused-disable-directives && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "lint": "eslint . --report-unused-disable-directives", "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [ @@ -37,9 +38,6 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.92.0", - "@modelcontextprotocol/sdk": "^1.29.0", - "dotenv": "^17.4.2", - "fastify": "^5.8.5", - "zod": "^4.4.2" + "dotenv": "^17.4.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 543b377..92eec92 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4de91a3 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - '.' diff --git a/src/modals/ArticleGeneratorModal.ts b/src/modals/ArticleGeneratorModal.ts index 1ccad11..7a2f84d 100644 --- a/src/modals/ArticleGeneratorModal.ts +++ b/src/modals/ArticleGeneratorModal.ts @@ -1,5 +1,5 @@ -import { App, Modal, Notice, Setting } from 'obsidian'; -import type { Editor } from 'obsidian'; +import { Modal, Notice, Setting } from 'obsidian'; +import type { Editor , App} from 'obsidian'; import type { PerplexityService, PerplexityOptions } from '../services/perplexityService'; import type { PromptsService } from '../services/promptsService'; diff --git a/src/modals/ClaudeModal.ts b/src/modals/ClaudeModal.ts index 3ef8b8c..b21efa1 100644 --- a/src/modals/ClaudeModal.ts +++ b/src/modals/ClaudeModal.ts @@ -1,5 +1,5 @@ -import { App, Modal, Notice, Setting } from 'obsidian'; -import type { Editor } from 'obsidian'; +import { Modal, Notice, Setting } from 'obsidian'; +import type { Editor , App} from 'obsidian'; import type { ClaudeService, ClaudeOptions } from '../services/claudeService'; import type { PromptsService } from '../services/promptsService'; diff --git a/src/modals/LMStudioModal.ts b/src/modals/LMStudioModal.ts index bb48097..d85599c 100644 --- a/src/modals/LMStudioModal.ts +++ b/src/modals/LMStudioModal.ts @@ -1,6 +1,7 @@ -import { App, Modal, Notice, Editor } from 'obsidian'; -import { LMStudioService, LMStudioOptions } from '../services/lmStudioService'; -import { PromptsService } from '../services/promptsService'; +import type { App, Editor } from 'obsidian'; +import { Modal, Notice } from 'obsidian'; +import type { LMStudioService, LMStudioOptions } from '../services/lmStudioService'; +import type { PromptsService } from '../services/promptsService'; export class LMStudioModal extends Modal { private editor: Editor; @@ -109,7 +110,7 @@ export class LMStudioModal extends Modal { form.onsubmit = (e) => { e.preventDefault(); - this.onSubmit(); + void this.onSubmit(); }; askButton.onclick = () => this.onSubmit(); diff --git a/src/modals/PerplexicaModal.ts b/src/modals/PerplexicaModal.ts index 034e403..7246623 100644 --- a/src/modals/PerplexicaModal.ts +++ b/src/modals/PerplexicaModal.ts @@ -1,6 +1,7 @@ -import { App, Modal, Notice, Editor } from 'obsidian'; -import { PerplexicaService, PerplexicaOptions } from '../services/perplexicaService'; -import { PromptsService } from '../services/promptsService'; +import type { App, Editor } from 'obsidian'; +import { Modal, Notice } from 'obsidian'; +import type { PerplexicaService, PerplexicaOptions } from '../services/perplexicaService'; +import type { PromptsService } from '../services/promptsService'; export class PerplexicaModal extends Modal { private editor: Editor; @@ -81,7 +82,7 @@ export class PerplexicaModal extends Modal { form.onsubmit = (e) => { e.preventDefault(); - this.onSubmit(); + void this.onSubmit(); }; askButton.onclick = () => this.onSubmit(); diff --git a/src/modals/PerplexityModal.ts b/src/modals/PerplexityModal.ts index 501be2e..7165906 100644 --- a/src/modals/PerplexityModal.ts +++ b/src/modals/PerplexityModal.ts @@ -1,5 +1,5 @@ -import { App, Modal, Notice, Setting } from 'obsidian'; -import type { Editor } from 'obsidian'; +import { Modal, Notice, Setting } from 'obsidian'; +import type { Editor , App} from 'obsidian'; import type { PerplexityService, PerplexityOptions } from '../services/perplexityService'; import type { PromptsService } from '../services/promptsService'; diff --git a/src/modals/TextEnhancementModal.ts b/src/modals/TextEnhancementModal.ts index a210d96..bd1463c 100644 --- a/src/modals/TextEnhancementModal.ts +++ b/src/modals/TextEnhancementModal.ts @@ -1,6 +1,7 @@ -import { App, Modal, Notice, Editor } from 'obsidian'; -import { PerplexityService } from '../services/perplexityService'; -import { PromptsService } from '../services/promptsService'; +import type { App, Editor } from 'obsidian'; +import { Modal, Notice } from 'obsidian'; +import type { PerplexityService } from '../services/perplexityService'; +import type { PromptsService } from '../services/promptsService'; export class TextEnhancementModal extends Modal { protected editor: Editor; diff --git a/src/modals/TextEnhancementWithImagesModal.ts b/src/modals/TextEnhancementWithImagesModal.ts index 5d76eaf..a884339 100644 --- a/src/modals/TextEnhancementWithImagesModal.ts +++ b/src/modals/TextEnhancementWithImagesModal.ts @@ -1,6 +1,7 @@ -import { App, Modal, Notice, Editor } from 'obsidian'; -import { PerplexityService } from '../services/perplexityService'; -import { PromptsService } from '../services/promptsService'; +import type { App, Editor } from 'obsidian'; +import { Modal, Notice } from 'obsidian'; +import type { PerplexityService } from '../services/perplexityService'; +import type { PromptsService } from '../services/promptsService'; export class TextEnhancementWithImagesModal extends Modal { protected editor: Editor; diff --git a/src/modals/URLUpdateModal.ts b/src/modals/URLUpdateModal.ts index 9c8b077..b36d56b 100644 --- a/src/modals/URLUpdateModal.ts +++ b/src/modals/URLUpdateModal.ts @@ -1,4 +1,5 @@ -import { App, Modal, Notice } from 'obsidian'; +import type { App} from 'obsidian'; +import { Modal, Notice } from 'obsidian'; export interface URLUpdateModalConfig { title: string; @@ -44,7 +45,7 @@ export class URLUpdateModal extends Modal { form.onsubmit = (e) => { e.preventDefault(); - this.onSubmit(); + void this.onSubmit(); }; saveButton.onclick = () => this.onSubmit(); diff --git a/src/services/lmStudioService.ts b/src/services/lmStudioService.ts index d5e0ed0..d5e35ff 100644 --- a/src/services/lmStudioService.ts +++ b/src/services/lmStudioService.ts @@ -1,4 +1,6 @@ -import { Editor, Notice } from 'obsidian'; +import type { Editor} from 'obsidian'; +import { Notice } from 'obsidian'; +import type { PromptsService } from './promptsService'; export interface LMStudioOptions { max_tokens?: number; @@ -10,13 +12,35 @@ export interface LMStudioOptions { export interface LMStudioSettings { lmStudioEndpoint: string; - promptsService?: any; // Will be PromptsService type + promptsService?: PromptsService; requestTemplate?: string; } +interface ChatMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +interface LMStudioPayload { + model: string; + messages: ChatMessage[]; + stream: boolean; + max_tokens: number; + temperature: number; + top_p: number; +} + +interface LMStudioStreamChunk { + choices?: Array<{ delta?: { content?: string } }>; +} + +interface LMStudioResponse { + choices?: Array<{ message?: { content?: string } }>; +} + export class LMStudioService { private settings: LMStudioSettings; - private promptsService: any; + private promptsService: PromptsService | undefined; constructor(settings: LMStudioSettings) { this.settings = settings; @@ -79,37 +103,34 @@ export class LMStudioService { console.log('Response cursor position:', responseCursor); try { - const messages: any[] = []; - - // Add system message if provided + const messages: ChatMessage[] = []; + if (options?.system_prompt) { messages.push({ role: 'system', content: options.system_prompt }); } - - // Add user query + messages.push({ role: 'user', content: query }); - - // Use template if available, otherwise construct payload manually - let payload: any; + + let payload: LMStudioPayload; if (this.settings.requestTemplate) { try { const processedTemplate = this.promptsService?.processTemplate(this.settings.requestTemplate) || this.settings.requestTemplate; - - // Strip JavaScript-style comments that would break JSON parsing + const cleanedTemplate = processedTemplate - .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments - .replace(/\/\/.*$/gm, '') // Remove // comments - .replace(/^\s*$/gm, '') // Remove empty lines + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/.*$/gm, '') + .replace(/^\s*$/gm, '') .trim(); - - payload = JSON.parse(cleanedTemplate); - // Override with current parameters - payload.model = model; - payload.messages = messages; - payload.stream = stream; - payload.max_tokens = options?.max_tokens ?? 2048; - payload.temperature = options?.temperature ?? 0.7; - payload.top_p = options?.top_p ?? 0.9; + + JSON.parse(cleanedTemplate); + payload = { + model, + messages, + stream, + max_tokens: options?.max_tokens ?? 2048, + temperature: options?.temperature ?? 0.7, + top_p: options?.top_p ?? 0.9, + }; } catch (error) { console.warn('Failed to parse request template, using default payload:', error); payload = { @@ -118,7 +139,7 @@ export class LMStudioService { stream, max_tokens: options?.max_tokens ?? 2048, temperature: options?.temperature ?? 0.7, - top_p: options?.top_p ?? 0.9 + top_p: options?.top_p ?? 0.9, }; } } else { @@ -128,7 +149,7 @@ export class LMStudioService { stream, max_tokens: options?.max_tokens ?? 2048, temperature: options?.temperature ?? 0.7, - top_p: options?.top_p ?? 0.9 + top_p: options?.top_p ?? 0.9, }; } @@ -145,7 +166,7 @@ export class LMStudioService { throw new Error(`HTTP error! status: ${response.status}`); } - let finalCursor = responseCursor; + const finalCursor = responseCursor; if (stream) { await this.handleStreamingResponse(response, editor, responseCursor, options); @@ -192,9 +213,9 @@ export class LMStudioService { if (data === '[DONE]') continue; try { - const parsed = JSON.parse(data); + const parsed = JSON.parse(data) as LMStudioStreamChunk; if (parsed.choices?.[0]?.delta?.content) { - let content = parsed.choices[0].delta.content; + let content: string = parsed.choices[0].delta.content; // Process images if enabled if (options?.return_images) { @@ -231,8 +252,8 @@ export class LMStudioService { responseCursor: { line: number; ch: number }, options?: LMStudioOptions ): Promise { - const data = await response.json(); - const content = data.choices?.[0]?.message?.content || 'No response received'; + const data = (await response.json()) as LMStudioResponse; + const content: string = data.choices?.[0]?.message?.content || 'No response received'; // Process images if enabled let processedContent = content; diff --git a/src/services/perplexicaService.ts b/src/services/perplexicaService.ts index a303459..c2adad6 100644 --- a/src/services/perplexicaService.ts +++ b/src/services/perplexicaService.ts @@ -1,10 +1,12 @@ -import { Editor, Notice } from 'obsidian'; +import type { Editor} from 'obsidian'; +import { Notice } from 'obsidian'; +import type { PromptsService } from './promptsService'; export interface PerplexicaSettings { perplexicaEndpoint: string; localLLMPath: string; defaultModel: string; - promptsService?: any; // Will be PromptsService type + promptsService?: PromptsService; requestTemplate?: string; } @@ -12,9 +14,37 @@ export interface PerplexicaOptions { return_images?: boolean; } +interface PerplexicaModelRef { + provider: string; + name: string; +} + +interface PerplexicaHistoryEntry { + role: string; + content: string; +} + +interface PerplexicaPayload { + chatModel: PerplexicaModelRef; + embeddingModel: PerplexicaModelRef; + optimizationMode: string; + focusMode: string; + query: string; + history: PerplexicaHistoryEntry[]; + systemInstructions: string; + stream: boolean; + maxTokens?: number; + temperature?: number; +} + +interface PerplexicaStreamEvent { + type?: string; + data?: string; +} + export class PerplexicaService { private settings: PerplexicaSettings; - private promptsService: any; + private promptsService: PromptsService | undefined; constructor(settings: PerplexicaSettings) { this.settings = settings; @@ -73,40 +103,28 @@ export class PerplexicaService { for (const endpoint of endpoints) { try { - // Use template if available, otherwise construct payload manually - let payload: any; + let payload: PerplexicaPayload; if (this.settings.requestTemplate) { try { const processedTemplate = this.promptsService?.processTemplate(this.settings.requestTemplate) || this.settings.requestTemplate; - - // Strip JavaScript-style comments that would break JSON parsing + const cleanedTemplate = processedTemplate - .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments - .replace(/\/\/.*$/gm, '') // Remove // comments - .replace(/^\s*$/gm, '') // Remove empty lines + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/.*$/gm, '') + .replace(/^\s*$/gm, '') .trim(); - - payload = JSON.parse(cleanedTemplate); - // Override with current parameters - payload.chatModel = { - provider: "ollama", - name: this.settings.defaultModel + + JSON.parse(cleanedTemplate); + payload = { + chatModel: { provider: "ollama", name: this.settings.defaultModel }, + embeddingModel: { provider: "ollama", name: this.settings.defaultModel }, + optimizationMode, + focusMode, + query, + history: [{ role: "user", content: query }], + systemInstructions: this.promptsService?.getPerplexicaSystemPrompt() || "You are a helpful AI assistant. Provide clear, concise, and accurate information.", + stream, }; - payload.embeddingModel = { - provider: "ollama", - name: this.settings.defaultModel - }; - payload.optimizationMode = optimizationMode; - payload.focusMode = focusMode; - payload.query = query; - payload.history = [ - { - role: "user", - content: query - } - ]; - payload.systemInstructions = this.promptsService?.getPerplexicaSystemPrompt() || "You are a helpful AI assistant. Provide clear, concise, and accurate information."; - payload.stream = stream; } catch (error) { console.warn('Failed to parse request template, using default payload:', error); payload = { @@ -211,31 +229,27 @@ export class PerplexicaService { for (const line of lines) { try { - const parsed = JSON.parse(line); - if (parsed.type === 'response' && parsed.data) { - let content = parsed.data; - - // Process images if enabled - if (options?.return_images) { - content = this.processContentWithImages(content); - } - - editor.replaceRange(content, currentPos); - // Update cursor position - const lines = content.split('\n'); - if (lines.length === 1) { - currentPos = { line: currentPos.line, ch: currentPos.ch + content.length }; - } else { - currentPos = { - line: currentPos.line + lines.length - 1, - ch: lines[lines.length - 1]?.length || 0 - }; - } - // Scroll to follow the new content - editor.scrollIntoView({ from: currentPos, to: currentPos }, true); - // Small delay to make scrolling smoother - await new Promise(resolve => setTimeout(resolve, 10)); + const parsed = JSON.parse(line) as PerplexicaStreamEvent; + if (parsed.type === 'response' && typeof parsed.data === 'string') { + let content: string = parsed.data; + + if (options?.return_images) { + content = this.processContentWithImages(content); } + + editor.replaceRange(content, currentPos); + const splitLines = content.split('\n'); + if (splitLines.length === 1) { + currentPos = { line: currentPos.line, ch: currentPos.ch + content.length }; + } else { + currentPos = { + line: currentPos.line + splitLines.length - 1, + ch: splitLines[splitLines.length - 1]?.length || 0 + }; + } + editor.scrollIntoView({ from: currentPos, to: currentPos }, true); + await new Promise(resolve => setTimeout(resolve, 10)); + } } catch (e) { // Ignore JSON parse errors } diff --git a/src/services/perplexityService.ts b/src/services/perplexityService.ts index f60c311..b7d71ee 100644 --- a/src/services/perplexityService.ts +++ b/src/services/perplexityService.ts @@ -1,4 +1,6 @@ -import { Editor, Notice, request } from 'obsidian'; +import type { Editor} from 'obsidian'; +import { Notice, request } from 'obsidian'; +import type { PromptsService } from './promptsService'; import { formatCitationDate, getMostRecentDate, formatPublicationInfo } from '../utils/formatDate'; export interface PerplexityOptions { @@ -11,14 +13,62 @@ export interface PerplexityOptions { export interface PerplexitySettings { perplexityApiKey: string; perplexityEndpoint: string; - promptsService?: any; // Will be PromptsService type + promptsService?: PromptsService; requestTemplate?: string; headerPosition?: 'top' | 'bottom'; } +export interface PerplexityImage { + image_url?: string; + origin_url?: string; +} + +export interface PerplexitySource { + title?: string; + url?: string; + date?: string; + last_updated?: string; +} + +interface PerplexityMessage { + role: string; + content: string; +} + +interface PerplexityPayload { + model: string; + messages: PerplexityMessage[]; + stream: boolean; + return_citations: boolean; + return_images: boolean; + return_related_questions: boolean; + search_recency_filter?: string; +} + +interface PerplexityChoice { + message?: { content?: string }; + delta?: { content?: string }; + finish_reason?: string; +} + +interface PerplexityResponse { + choices?: PerplexityChoice[]; + images?: PerplexityImage[]; + search_results?: PerplexitySource[]; + citations?: (string | PerplexitySource)[]; +} + +interface PerplexityStreamChunk { + choices?: PerplexityChoice[]; + images?: PerplexityImage[]; + search_results?: PerplexitySource[]; + citations?: (string | PerplexitySource)[]; +} + export class PerplexityService { private settings: PerplexitySettings; - private promptsService: any; + private promptsService: PromptsService | undefined; + private loadingInterval: ReturnType | null = null; constructor(settings: PerplexitySettings) { this.settings = settings; @@ -48,7 +98,7 @@ export class PerplexityService { return 'month'; } - private processContentWithImages(content: string, images: any[]): string { + private processContentWithImages(content: string, images: PerplexityImage[]): string { if (!images || images.length === 0) return content; console.log(`🖼️ Processing ${images.length} images for content replacement`); @@ -221,15 +271,14 @@ export class PerplexityService { } private clearLoadingAnimation(): void { - // Clear the animation interval if it exists - if ((this as any).loadingInterval) { + if (this.loadingInterval) { console.log('🛑 Clearing loading animation interval'); - clearInterval((this as any).loadingInterval); - (this as any).loadingInterval = null; + clearInterval(this.loadingInterval); + this.loadingInterval = null; } } - private addCitations(editor: Editor, sources: any[]): void { + private addCitations(editor: Editor, sources: (string | PerplexitySource)[]): void { if (!sources || sources.length === 0) return; console.log(`📚 Processing ${sources.length} sources for citations`); @@ -393,79 +442,65 @@ export class PerplexityService { try { const convertedFilter = this.convertRecencyFilter(options?.search_recency_filter ?? "month"); - // Use template if available, otherwise construct payload manually - let payload: any; + let payload: PerplexityPayload; if (this.settings.requestTemplate) { try { const processedTemplate = this.promptsService?.processTemplate(this.settings.requestTemplate) || this.settings.requestTemplate; - - // Strip JavaScript-style comments that would break JSON parsing + let cleanedTemplate = processedTemplate - .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments - .replace(/\/\/.*$/gm, '') // Remove // comments - .replace(/^\s*$/gm, '') // Remove empty lines + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/.*$/gm, '') + .replace(/^\s*$/gm, '') .trim(); - - // Check if template contains JavaScript code instead of JSON + if (cleanedTemplate.includes('const ') || cleanedTemplate.includes('fetch(') || cleanedTemplate.includes('await ')) { console.warn('⚠️ Template contains JavaScript code, not JSON. Extracting payload object...'); - - // Try to extract JSON payload from JavaScript code + const payloadMatch = cleanedTemplate.match(/payload\s*=\s*({[\s\S]*?});/); if (payloadMatch) { let jsObject = payloadMatch[1]; console.log('🔍 Extracted payload from JavaScript:', jsObject); - - // Convert JavaScript object syntax to valid JSON - // Replace unquoted property names with quoted ones - jsObject = jsObject.replace(/(\w+):/g, '"$1":'); - // Fix single quotes to double quotes - jsObject = jsObject.replace(/'/g, '"'); - - cleanedTemplate = jsObject; + + jsObject = jsObject?.replace(/(\w+):/g, '"$1":').replace(/'/g, '"'); + + cleanedTemplate = jsObject ?? cleanedTemplate; } else { throw new Error('Template contains JavaScript code but no valid payload object found. Please use JSON format only.'); } } - + console.log('🧹 Final cleaned template:', cleanedTemplate); - payload = JSON.parse(cleanedTemplate); - // Override with current query and options - payload.model = model; - payload.messages = [ - { role: 'user', content: query } - ]; - payload.stream = useStreaming; - payload.return_citations = options?.return_citations ?? true; - payload.return_images = options?.return_images ?? true; - payload.return_related_questions = options?.return_related_questions ?? false; + JSON.parse(cleanedTemplate); + payload = { + model, + messages: [{ role: 'user', content: query }], + stream: useStreaming, + return_citations: options?.return_citations ?? true, + return_images: options?.return_images ?? true, + return_related_questions: options?.return_related_questions ?? false, + }; } catch (error) { console.warn('Failed to parse request template, using default payload:', error); payload = { model, - messages: [ - { role: 'user', content: query } - ], + messages: [{ role: 'user', content: query }], stream: useStreaming, return_citations: options?.return_citations ?? true, return_images: options?.return_images ?? true, - return_related_questions: options?.return_related_questions ?? false + return_related_questions: options?.return_related_questions ?? false, }; } } else { payload = { model, - messages: [ - { role: 'user', content: query } - ], + messages: [{ role: 'user', content: query }], stream: useStreaming, return_citations: options?.return_citations ?? true, return_images: options?.return_images ?? true, - return_related_questions: options?.return_related_questions ?? false + return_related_questions: options?.return_related_questions ?? false, }; } - - // Only include search_recency_filter if we have a filter value + if (convertedFilter !== undefined) { payload.search_recency_filter = convertedFilter; } @@ -516,33 +551,25 @@ export class PerplexityService { }); console.log('✅ Non-streaming response received'); - // Parse the response - const data = JSON.parse(response); + const data = JSON.parse(response) as PerplexityResponse; console.log('📊 Response data structure:', Object.keys(data)); - // Process the response if (data.choices && data.choices.length > 0) { - let content = data.choices[0].message.content; - - // Process think blocks first + let content = data.choices[0]?.message?.content ?? ''; + content = this.processThinkBlocks(content); - - // Process images if available + if (options?.return_images && data.images && data.images.length > 0) { console.log('🖼️ Processing images in non-streaming response:', data.images.length, 'images found'); content = this.processContentWithImages(content, data.images); } - - // Insert the response at the cursor position + editor.replaceRange(content, responseCursor); - - // Add citations if available + if (options?.return_citations) { if (data.search_results && data.search_results.length > 0) { - // Use search_results for detailed info (preferred for Perplexity) this.addCitations(editor, data.search_results); } else if (data.citations && data.citations.length > 0) { - // Fallback to citations array (could be URLs or other format) this.addCitations(editor, data.citations); } } @@ -605,43 +632,39 @@ export class PerplexityService { console.log(`🔄 Starting streaming response handler [${requestId || 'unknown'}]`); let buffer = ''; - let currentPos = { ...responseCursor }; - let finalResponseData: any = null; - + const currentPos = { ...responseCursor }; + let finalResponseData: PerplexityStreamChunk | null = null; + console.log(`📍 Initial currentPos:`, currentPos); - + try { while (true) { const { done, value } = await reader.read(); if (done) break; - + const chunk = new TextDecoder().decode(value, { stream: true }); buffer += chunk; - - // Process complete lines from buffer + const lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Keep incomplete line in buffer - + buffer = lines.pop() || ''; + for (const line of lines) { if (line.trim() === '') continue; - + try { if (line.startsWith('data: ')) { const data = line.replace('data: ', '').trim(); if (data === '[DONE]') continue; - - const parsed = JSON.parse(data); - - // Capture the latest metadata across chunks. Perplexity typically - // sends citations/search_results in earlier chunks; the final chunk - // often carries only finish_reason. Merge so nothing is lost. + + const parsed = JSON.parse(data) as PerplexityStreamChunk; + if (parsed.citations || parsed.images || parsed.search_results) { finalResponseData = { ...(finalResponseData || {}), ...parsed, }; } - + if (parsed.choices?.[0]?.delta?.content) { const content = parsed.choices[0].delta.content; if (content) { @@ -654,13 +677,12 @@ export class PerplexityService { // console.log(`📝 Inserting content at position:`, currentPos, `Content:`, content.substring(0, 50) + '...'); editor.replaceRange(content, currentPos); - // Update cursor position after insertion const contentLines = content.split('\n'); if (contentLines.length === 1) { currentPos.ch += content.length; } else { currentPos.line += contentLines.length - 1; - currentPos.ch = contentLines[contentLines.length - 1].length; + currentPos.ch = contentLines[contentLines.length - 1]?.length ?? 0; } // Scroll to follow the new content editor.scrollIntoView({ from: currentPos, to: currentPos }, true); @@ -704,7 +726,7 @@ export class PerplexityService { } private async processStreamingMetadata( - finalResponseData: any, + finalResponseData: PerplexityStreamChunk, editor: Editor, headerText?: string ): Promise { @@ -758,7 +780,7 @@ export class PerplexityService { // Insert images inline after the query header let imagesSection = '\n\n'; - finalResponseData.images.forEach((image: any, index: number) => { + finalResponseData.images.forEach((image: PerplexityImage, index: number) => { if (image.image_url) { imagesSection += `![Image ${index + 1}](${image.image_url})\n\n`; } @@ -774,7 +796,7 @@ export class PerplexityService { } else { // Fallback: add images at the end if no markers found let imagesSection = '\n\n## Images\n\n'; - finalResponseData.images.forEach((image: any, index: number) => { + finalResponseData.images.forEach((image: PerplexityImage, index: number) => { if (image.image_url) { imagesSection += `![Image ${index + 1}](${image.image_url})\n`; if (image.origin_url) { diff --git a/src/types/obsidian.d.ts b/src/types/obsidian.d.ts index ee47d10..4362652 100644 --- a/src/types/obsidian.d.ts +++ b/src/types/obsidian.d.ts @@ -1,8 +1,12 @@ -import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian'; +import type { TFile } from 'obsidian'; declare module 'obsidian' { + interface CommandsApi { + commands: Record; + } + interface App { - commands: any; + commands: CommandsApi; } interface Editor { diff --git a/src/utils/coerce.ts b/src/utils/coerce.ts new file mode 100644 index 0000000..cea374f --- /dev/null +++ b/src/utils/coerce.ts @@ -0,0 +1,33 @@ +export function asString(v: unknown): string | undefined { + if (typeof v === 'string') return v; + if (typeof v === 'number' || typeof v === 'boolean') return String(v); + return undefined; +} + +export function asNumber(v: unknown): number | undefined { + if (typeof v === 'number' && Number.isFinite(v)) return v; + if (typeof v === 'string') { + const n = Number(v); + return Number.isFinite(n) ? n : undefined; + } + return undefined; +} + +export function asStringArray(v: unknown): string[] { + if (Array.isArray(v)) { + return v.map(asString).filter((s): s is string => s !== undefined); + } + const single = asString(v); + return single === undefined ? [] : [single]; +} + +export function asDate(v: unknown): string | undefined { + const s = asString(v); + if (s === undefined) return undefined; + const d = new Date(s); + return Number.isNaN(d.getTime()) ? undefined : d.toISOString(); +} + +export function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v); +} diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts index cae7701..b23db2d 100644 --- a/src/utils/formatDate.ts +++ b/src/utils/formatDate.ts @@ -1,29 +1,24 @@ -/** - * Date formatting utilities for the Perplexed plugin - */ +import { asString, isRecord } from './coerce'; + +export interface DateBearingSource { + date?: unknown; + last_updated?: unknown; +} -/** - * Formats a date string into the format: "YYYY, MMM DD" - * Examples: "2024, Dec 13", "2025, Jan 05" - * - * @param dateString - Date string in various formats (ISO, YYYY-MM-DD, etc.) - * @returns Formatted date string or empty string if invalid - */ export function formatCitationDate(dateString: string): string { if (!dateString) return ''; - + try { const date = new Date(dateString); - - // Check if date is valid + if (isNaN(date.getTime())) { return ''; } - + const year = date.getFullYear(); const month = date.toLocaleDateString('en-US', { month: 'short' }); const day = date.getDate().toString().padStart(2, '0'); - + return `${year}, ${month} ${day}`; } catch (error) { console.warn('Failed to format date:', dateString, error); @@ -31,39 +26,20 @@ export function formatCitationDate(dateString: string): string { } } -/** - * Gets the most recent date from available date fields - * Prioritizes last_updated over date - * - * @param source - Source object with potential date fields - * @returns Most recent date string or empty string if none available - */ -export function getMostRecentDate(source: any): string { - if (source.last_updated) { - return source.last_updated; - } - if (source.date) { - return source.date; - } - return ''; +export function getMostRecentDate(source: unknown): string { + if (!isRecord(source)) return ''; + return asString(source.last_updated) ?? asString(source.date) ?? ''; } -/** - * Creates the publication info string for citations - * Format: "Published: YYYY-MM-DD | Updated: YYYY-MM-DD" - * - * @param source - Source object with potential date fields - * @returns Publication info string or empty string if no dates available - */ -export function formatPublicationInfo(source: any): string { - const dateInfo = []; - - if (source.date) { - dateInfo.push(`Published: ${source.date}`); - } - if (source.last_updated) { - dateInfo.push(`Updated: ${source.last_updated}`); - } - +export function formatPublicationInfo(source: unknown): string { + if (!isRecord(source)) return ''; + const dateInfo: string[] = []; + + const date = asString(source.date); + if (date) dateInfo.push(`Published: ${date}`); + + const lastUpdated = asString(source.last_updated); + if (lastUpdated) dateInfo.push(`Updated: ${lastUpdated}`); + return dateInfo.join(' | '); } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index d70b4ad..329a074 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,11 +1,12 @@ import { Notice } from 'obsidian'; -import { TFile, Vault } from 'obsidian'; +import type { Vault } from 'obsidian'; +import { TFile } from 'obsidian'; interface LogEntry { timestamp: string; level: 'error' | 'warn' | 'info' | 'debug'; message: string; - details?: any; + details?: unknown; stack?: string; } @@ -78,7 +79,7 @@ export class FileLogger { } } - private addEntry(level: LogEntry['level'], message: string, details?: any): void { + private addEntry(level: LogEntry['level'], message: string, details?: unknown): void { const entry: LogEntry = { timestamp: new Date().toISOString(), level, @@ -106,19 +107,19 @@ export class FileLogger { logMethod(`[${entry.timestamp}] [${level.toUpperCase()}] ${message}`, details || ''); } - error(message: string, details?: any): void { + error(message: string, details?: unknown): void { this.addEntry('error', message, details); } - warn(message: string, details?: any): void { + warn(message: string, details?: unknown): void { this.addEntry('warn', message, details); } - info(message: string, details?: any): void { + info(message: string, details?: unknown): void { this.addEntry('info', message, details); } - debug(message: string, details?: any): void { + debug(message: string, details?: unknown): void { this.addEntry('debug', message, details); } diff --git a/versions.json b/versions.json index 0177a48..9be1f3d 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "0.0.1.0": "0.15.0" + "0.1.0": "0.15.0" }