Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a09c3d7c
Submodule agent-a09c3d7c added at a1e75e
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a1c56452
Submodule agent-a1c56452 added at 5f3520
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a327b0c2
Submodule agent-a327b0c2 added at b61747
Submodule agent-a5e65331 added at 061b96
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a51ac752
Submodule agent-a51ac752 added at 13c9a4
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a662428a
Submodule agent-a662428a added at edf380
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a802499a
Submodule agent-a802499a added at 3f27bb
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a886725f
Submodule agent-a886725f added at bf6e1b
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a8d7741c
Submodule agent-a8d7741c added at 7f4c33
1 change: 1 addition & 0 deletions .claude/worktrees/agent-a8f0cb8d
Submodule agent-a8f0cb8d added at 49f9da
1 change: 1 addition & 0 deletions .claude/worktrees/agent-ab455ec1
Submodule agent-ab455ec1 added at 5f3520
1 change: 1 addition & 0 deletions .claude/worktrees/agent-abffe11f
Submodule agent-abffe11f added at bf6e1b
1 change: 1 addition & 0 deletions .claude/worktrees/agent-aca555c0
Submodule agent-aca555c0 added at f8ae7c
1 change: 1 addition & 0 deletions .claude/worktrees/agent-af29ee1c
Submodule agent-af29ee1c added at 015b53
1 change: 1 addition & 0 deletions .claude/worktrees/agent-af8e138d
Submodule agent-af8e138d added at bf6e1b
1 change: 1 addition & 0 deletions .claude/worktrees/feat-integration-tests
Submodule feat-integration-tests added at 3c5364
1 change: 1 addition & 0 deletions .claude/worktrees/feat-provider-tests
Submodule feat-provider-tests added at 35faeb
1 change: 1 addition & 0 deletions .claude/worktrees/feat-spider
Submodule feat-spider added at 71b1ca
1 change: 1 addition & 0 deletions .claude/worktrees/fix-error-handling
Submodule fix-error-handling added at f1c1b1
1 change: 1 addition & 0 deletions .claude/worktrees/fix-performance
Submodule fix-performance added at 9a3341
1 change: 1 addition & 0 deletions .claude/worktrees/fix-quick-wins
Submodule fix-quick-wins added at 9ac126
1 change: 1 addition & 0 deletions .claude/worktrees/fix-security
Submodule fix-security added at cc2df0
1 change: 1 addition & 0 deletions .claude/worktrees/fix-type-safety
Submodule fix-type-safety added at 26f2c4
1 change: 1 addition & 0 deletions .claude/worktrees/issue-330-cli-logging-pack-perf
Submodule issue-330-cli-logging-pack-perf added at d5dae0
1 change: 1 addition & 0 deletions .claude/worktrees/passthrough-mode
Submodule passthrough-mode added at b3f696
1 change: 1 addition & 0 deletions .claude/worktrees/refactor-cli-decompose
Submodule refactor-cli-decompose added at bb0b1e
1 change: 1 addition & 0 deletions .claude/worktrees/refactor-mcp-decompose
Submodule refactor-mcp-decompose added at 989260
9 changes: 5 additions & 4 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,16 @@ import {
syncOneNote,
disconnectOneNote,
} from "../connectors/onenote.js";
import { loadConnectorConfig, saveConnectorConfig } from "../connectors/index.js";
import { syncNotion, disconnectNotion } from "../connectors/notion.js";
import type { NotionConfig } from "../connectors/notion.js";
import { syncSlack, disconnectSlack, type SlackConfig } from "../connectors/slack.js";
import {
loadConnectorConfig,
saveConnectorConfig,
saveNamedConnectorConfig,
loadNamedConnectorConfig,
hasNamedConnectorConfig,
} from "../connectors/index.js";
import { syncNotion, disconnectNotion } from "../connectors/notion.js";
import type { NotionConfig } from "../connectors/notion.js";
import { syncSlack, disconnectSlack, type SlackConfig } from "../connectors/slack.js";
import {
createSavedSearch,
listSavedSearches,
Expand Down
2 changes: 1 addition & 1 deletion src/connectors/obsidian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function transformObsidianBody(body: string, fileMap: Map<string, string>): stri
/(?<!!)\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g,
(_match, link: string, display?: string) => {
const displayText = display ?? link;
const slug = link.toLowerCase().replace(/\s+/g, "-");
const slug = link.toLowerCase().replaceAll(/\s+/g, "-");
return `[${displayText}](${slug})`;
},
);
Expand Down
8 changes: 4 additions & 4 deletions src/connectors/onenote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,10 @@ export function convertOneNoteHtml(html: string): string {
let md = nhm.translate(processed).trim();

// Post-process: replace tokens with final markdown
md = md.replace(/CHECKDONE7X9Z\s*/g, "- [x] ");
md = md.replace(/CHECKTODO7X9Z\s*/g, "- [ ] ");
md = md.replace(/INKPLACEHOLDER7X9Z/g, "[handwritten content]");
md = md.replace(/IMGPLACEHOLDER7X9Z/g, "[image]");
md = md.replaceAll(/CHECKDONE7X9Z\s*/g, "- [x] ");
md = md.replaceAll(/CHECKTODO7X9Z\s*/g, "- [ ] ");
md = md.replaceAll("INKPLACEHOLDER7X9Z", "[handwritten content]");
md = md.replaceAll("IMGPLACEHOLDER7X9Z", "[image]");
md = md.replace(/FILEATTACH7X9Z([^\s]+?)ENDATTACH7X9Z/g, "[attached: $1]");

return md;
Expand Down
2 changes: 1 addition & 1 deletion src/connectors/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ function formatTimestamp(ts: string): string {
}

function truncateTitle(text: string, maxLen: number = 80): string {
const cleaned = text.replace(/\n/g, " ").trim();
const cleaned = text.replaceAll("\n", " ").trim();
if (cleaned.length <= maxLen) return cleaned;
return cleaned.slice(0, maxLen - 3) + "...";
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/dedup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export async function checkDuplicate(

if (row) {
log.debug({ existingDocId: row.id }, "Exact duplicate detected via content hash");
return { isDuplicate: true, matchType: "exact", existingDocId: row.id, similarity: 1.0 };
return { isDuplicate: true, matchType: "exact", existingDocId: row.id, similarity: 1 };
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/indexing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function startChunkAtHeading(
line: string,
): { lines: string[]; length: number } {
const level = (headingMatch[1] ?? "").length;
while (headingStack.length > 0 && (headingStack[headingStack.length - 1]?.level ?? 0) >= level) {
while (headingStack.length > 0 && (headingStack.at(-1)?.level ?? 0) >= level) {
headingStack.pop();
}
const breadcrumb = headingStack.map((h) => h.text).join(" > ");
Expand Down Expand Up @@ -223,7 +223,7 @@ function addDeduplicatedChunks(
seenHashes: Set<string>,
): void {
for (const chunk of windowChunks) {
const normalized = chunk.replace(/\s+/g, " ").trim();
const normalized = chunk.replaceAll(/\s+/g, " ").trim();
const hash = createHash("sha256").update(normalized).digest("hex");
if (!seenHashes.has(hash)) {
seenHashes.add(hash);
Expand Down
6 changes: 3 additions & 3 deletions src/core/packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,9 +693,9 @@ function matchesExcludePattern(relativePath: string, pattern: string): boolean {
// Escape regex special chars except * and **
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*\*/g, "\0")
.replace(/\*/g, "[^/]*")
.replace(/\0/g, ".*");
.replaceAll("**", "\0")
.replaceAll("*", "[^/]*")
.replaceAll("\0", ".*");
return new RegExp(`^${escaped}$`).test(relativePath);
}

Expand Down
8 changes: 5 additions & 3 deletions src/core/parsers/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ export class CsvParser implements DocumentParser {
const rows = records.slice(1);

const escapeCell = (cell: string): string =>
cell.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " ");
cell.replaceAll("\\", "\\\\").replaceAll("|", "\\|").replaceAll("\n", " ");

const lines: string[] = [];
lines.push("| " + header.map(escapeCell).join(" | ") + " |");
lines.push("| " + header.map(() => "---").join(" | ") + " |");
lines.push(
"| " + header.map(escapeCell).join(" | ") + " |",
"| " + header.map(() => "---").join(" | ") + " |",
);
for (const row of rows) {
// Normalize row length to match header
const normalized = Array.from({ length: colCount }, (_, i) => row[i] ?? "");
Expand Down
4 changes: 2 additions & 2 deletions src/core/parsers/epub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export class EpubParser implements DocumentParser {
const html: string = await getChapter.call(epub, item.id);
// Strip HTML tags to get plain text
const text = html
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.replaceAll(/<[^>]+>/g, " ")
.replaceAll(/\s+/g, " ")
.trim();
if (text.length > 0) {
chapters.push(text);
Expand Down
2 changes: 1 addition & 1 deletion src/core/parsers/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class HtmlParser implements DocumentParser {
const markdown = nhm.translate(html);

// Collapse excessive blank lines left by ignored elements
return Promise.resolve(markdown.replace(/\n{3,}/g, "\n\n").trimEnd());
return Promise.resolve(markdown.replaceAll(/\n{3,}/g, "\n\n").trimEnd());
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown HTML parsing error";
throw new ValidationError(`Failed to parse HTML: ${message}`);
Expand Down
15 changes: 6 additions & 9 deletions src/core/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function computeRrfScores(map: Map<string, RankedItem>): SearchResult[] {
for (const item of map.values()) {
let rrfScore = 0;
for (const rank of item.ranks) {
rrfScore += 1.0 / (RRF_K + rank);
rrfScore += 1 / (RRF_K + rank);
}
const boostFactors = [...item.result.scoreExplanation.boostFactors];
fused.push({
Expand Down Expand Up @@ -718,8 +718,7 @@ function keywordSearch(
const baseParams = [...params];

sql += " LIMIT ? OFFSET ?";
params.push(limit);
params.push(offset);
params.push(limit, offset);

const KeywordRowSchema = z.object({
chunk_id: z.string(),
Expand Down Expand Up @@ -825,7 +824,7 @@ export function getRelatedChunks(
): RelatedChunksResult {
const { chunkId } = options;
const limit = Math.max(1, Math.min(options.limit ?? 10, 1000));
const minScore = options.minScore ?? 0.0;
const minScore = options.minScore ?? 0;

// Look up the source chunk
const SourceChunkSchema = z.object({
Expand Down Expand Up @@ -1050,7 +1049,7 @@ function fts5Search(

const needsRatingJoin = options.minRating !== undefined;

const ftsQuery = words.map((w) => `"${w.replace(/"/g, '""')}"`).join(" AND ");
const ftsQuery = words.map((w) => `"${w.replaceAll('"', '""')}"`).join(" AND ");
const params: unknown[] = [ftsQuery];

let sql = `
Expand Down Expand Up @@ -1086,8 +1085,7 @@ function fts5Search(
let baseParams = [...params];

sql += " ORDER BY rank LIMIT ? OFFSET ?";
params.push(limit);
params.push(offset);
params.push(limit, offset);

const Fts5RowSchema = z.object({
chunk_id: z.string(),
Expand Down Expand Up @@ -1140,8 +1138,7 @@ function fts5Search(
baseParams = [...orParams];

orSql += " ORDER BY rank LIMIT ? OFFSET ?";
orParams.push(limit);
orParams.push(offset);
orParams.push(limit, offset);

rows = validateRows(Fts5RowSchema, db.prepare(orSql).all(...orParams), "fts5Search.orRows");
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/spider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ function extractTitle(html: string, url: string): string {
const parsed = new URL(url);
const path = parsed.pathname.replace(/\/$/, "");
const last = path.split("/").pop();
if (last) return last.replace(/[-_]/g, " ").replace(/\.\w+$/, "");
if (last) return last.replaceAll(/[-_]/g, " ").replace(/\.\w+$/, "");
return parsed.hostname;
} catch {
return url;
Expand Down
2 changes: 1 addition & 1 deletion src/core/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export function suggestTags(
for (const [term, count] of tf) {
if (existingTags.has(term)) continue;
const normalizedTf = count / maxTf;
const knownBoost = knownTags.has(term) ? 2.0 : 1.0;
const knownBoost = knownTags.has(term) ? 2 : 1;
scored.push({ term, score: normalizedTf * knownBoost });
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/topics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function createTopic(db: Database.Database, input: CreateTopicInput): Top
input.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "") || randomUUID();
.replaceAll(/^-|-$/g, "") || randomUUID();

// Verify parent exists if provided
if (input.parentId) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function validateUrl(url: string): void {
/** Resolve hostname and block private/internal IPs (SSRF protection). */
export async function validateWebhookUrlSsrf(url: string): Promise<void> {
const parsed = new URL(url);
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
const hostname = parsed.hostname.replaceAll(/^\[|\]$/g, "");

const results = await Promise.allSettled([dns.resolve4(hostname), dns.resolve6(hostname)]);
const addresses: string[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/web/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export function getDashboardHtml(): string {

function escAttr(s) {
if (!s) return '';
return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return s.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll("'", '&#39;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}

// Event delegation for cards and delete buttons
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/retrieval-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class TfIdfEmbeddingProvider implements EmbeddingProvider {
private tokenize(text: string): string[] {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.replaceAll(/[^a-z0-9]+/g, " ")
.split(/\s+/)
.filter((w) => w.length > 1);
}
Expand Down
3 changes: 1 addition & 2 deletions tests/unit/batch-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
import type Database from "better-sqlite3";
import { createTestDbWithVec } from "../fixtures/test-db.js";
import { MockEmbeddingProvider } from "../fixtures/mock-provider.js";
import { seedTestDocument } from "../fixtures/helpers.js";
import { insertChunk } from "../fixtures/helpers.js";
import { seedTestDocument, insertChunk } from "../fixtures/helpers.js";
import {
searchBatch,
BATCH_SEARCH_MAX_REQUESTS,
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/dedup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("dedup", () => {

expect(result.isDuplicate).toBe(true);
expect(result.matchType).toBe("exact");
expect(result.similarity).toBe(1.0);
expect(result.similarity).toBe(1);
expect(result.existingDocId).toBeDefined();
});

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/indexing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,15 @@ More content here.`;
// Build content where the same logical text appears with different whitespace
const line = "Hello world this is a test line.";
const variant1 = line + "\n";
const variant2 = line.replace(/ /g, " ") + "\n"; // double spaces
const variant2 = line.replaceAll(" ", " ") + "\n"; // double spaces
// Interleave so overlap might pick up both
const content = (variant1.repeat(50) + variant2.repeat(50)).repeat(2);
const chunks = chunkContentStreaming(content, { windowSize: 512 });

// After whitespace normalization, duplicates should be removed
const seen = new Set<string>();
for (const chunk of chunks) {
const normalized = chunk.replace(/\s+/g, " ").trim();
const normalized = chunk.replaceAll(/\s+/g, " ").trim();
expect(seen.has(normalized)).toBe(false);
seen.add(normalized);
}
Expand Down
7 changes: 5 additions & 2 deletions tests/unit/obsidian.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { parseObsidianMarkdown } from "../../src/connectors/obsidian.js";
import {
parseObsidianMarkdown,
syncObsidianVault,
disconnectVault,
} from "../../src/connectors/obsidian.js";
import { createTestDb, createTestDbWithVec } from "../fixtures/test-db.js";
import { MockEmbeddingProvider } from "../fixtures/mock-provider.js";
import type Database from "better-sqlite3";
Expand Down Expand Up @@ -29,7 +33,6 @@ vi.mock("node:fs/promises", async (importOriginal) => {
});

import { readdirSync, readFileSync, statSync, existsSync, writeFileSync } from "node:fs";
import { syncObsidianVault, disconnectVault } from "../../src/connectors/obsidian.js";
import { initLogger } from "../../src/logger.js";

const mockedReaddirSync = vi.mocked(readdirSync);
Expand Down
Loading