@lapis-notes/search 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE.md +8 -0
  3. package/README.md +20 -0
  4. package/dist/agent-tools.d.ts +1 -0
  5. package/dist/agent-tools.js +1 -0
  6. package/dist/built-in-search-document-providers.d.ts +5 -0
  7. package/dist/built-in-search-document-providers.js +30 -0
  8. package/dist/highlighted-text.svelte +41 -0
  9. package/dist/highlighted-text.svelte.d.ts +11 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +9 -0
  12. package/dist/notes-search-command.d.ts +2 -0
  13. package/dist/notes-search-command.js +8 -0
  14. package/dist/notes-search-tool.d.ts +8 -0
  15. package/dist/notes-search-tool.js +84 -0
  16. package/dist/public-components.d.ts +2 -0
  17. package/dist/public-components.js +2 -0
  18. package/dist/search-manager.d.ts +92 -0
  19. package/dist/search-manager.js +771 -0
  20. package/dist/search-match-context.d.ts +17 -0
  21. package/dist/search-match-context.js +45 -0
  22. package/dist/search-panel.css +460 -0
  23. package/dist/search-panel.svelte +796 -0
  24. package/dist/search-panel.svelte.d.ts +11 -0
  25. package/dist/search-plugin.d.ts +22 -0
  26. package/dist/search-plugin.js +174 -0
  27. package/dist/search-result-model.d.ts +29 -0
  28. package/dist/search-result-model.js +50 -0
  29. package/dist/search-settings-tab.d.ts +12 -0
  30. package/dist/search-settings-tab.js +139 -0
  31. package/dist/search-settings.d.ts +54 -0
  32. package/dist/search-settings.js +107 -0
  33. package/dist/search-sort.d.ts +8 -0
  34. package/dist/search-sort.js +36 -0
  35. package/dist/search-startup.d.ts +3 -0
  36. package/dist/search-startup.js +4 -0
  37. package/dist/search-tool-result.css +70 -0
  38. package/dist/search-tool-result.d.ts +16 -0
  39. package/dist/search-tool-result.js +111 -0
  40. package/dist/search-tool-result.svelte +55 -0
  41. package/dist/search-tool-result.svelte.d.ts +6 -0
  42. package/dist/search-view-type.d.ts +1 -0
  43. package/dist/search-view-type.js +1 -0
  44. package/dist/search-view.d.ts +14 -0
  45. package/dist/search-view.js +48 -0
  46. package/dist/styles.css +2 -0
  47. package/manifest.json +28 -0
  48. package/package.json +80 -0
  49. package/styles.css +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Search changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial independent public package extracted from Lapis Notes without changing
6
+ the runtime plugin ID.
package/LICENSE.md ADDED
@@ -0,0 +1,8 @@
1
+ # License
2
+
3
+ Copyright (c) LapisMD contributors.
4
+
5
+ This software is licensed under the GNU Affero General Public License, version
6
+ 3 or (at your option) any later version (`AGPL-3.0-or-later`). The complete
7
+ license text is available from the
8
+ [GNU Project](https://www.gnu.org/licenses/agpl-3.0.html).
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Search
2
+
3
+ Search files, metadata, tags, and note content.
4
+
5
+ ## Install for static composition
6
+
7
+ ```sh
8
+ pnpm add @lapis-notes/search
9
+ ```
10
+
11
+ Register the exported plugin class in the application's `PluginProfile`. Import
12
+ `@lapis-notes/search/styles.css?inline` and pass the CSS through the static
13
+ registration so the host owns its lifecycle.
14
+
15
+ The runtime plugin ID is `search`. A matching signed
16
+ `search-0.1.0.lapis-plugin` archive is attached to the
17
+ package-scoped GitHub release for manual or registry installation.
18
+
19
+ See the [repository README](https://github.com/lapismd/lapis-plugins#readme) for
20
+ development, validation, and release-gate details.
@@ -0,0 +1 @@
1
+ export { createNotesSearchTool } from "./notes-search-tool";
@@ -0,0 +1 @@
1
+ export { createNotesSearchTool } from "./notes-search-tool";
@@ -0,0 +1,5 @@
1
+ import type { SearchDocumentProvider } from "@lapis-notes/api";
2
+ type LocalSearchDocumentProvider = Omit<SearchDocumentProvider, "id">;
3
+ export declare const MARKDOWN_SEARCH_DOCUMENT_PROVIDER: LocalSearchDocumentProvider;
4
+ export declare const CANVAS_SEARCH_DOCUMENT_PROVIDER: LocalSearchDocumentProvider;
5
+ export {};
@@ -0,0 +1,30 @@
1
+ function canvasText(content) {
2
+ try {
3
+ const canvas = JSON.parse(content);
4
+ return [
5
+ ...(canvas.nodes ?? []).flatMap((node) => [
6
+ node.type,
7
+ node.text,
8
+ node.label,
9
+ node.file,
10
+ node.url,
11
+ ]),
12
+ ...(canvas.edges ?? []).map((edge) => edge.label),
13
+ ]
14
+ .filter((part) => Boolean(part?.trim()))
15
+ .join("\n");
16
+ }
17
+ catch {
18
+ return content;
19
+ }
20
+ }
21
+ export const MARKDOWN_SEARCH_DOCUMENT_PROVIDER = {
22
+ version: "1",
23
+ matches: (file) => ["md", "markdown"].includes(file.extension.toLowerCase()),
24
+ extract: ({ content }) => ({ content }),
25
+ };
26
+ export const CANVAS_SEARCH_DOCUMENT_PROVIDER = {
27
+ version: "1",
28
+ matches: (file) => file.extension.toLowerCase() === "canvas",
29
+ extract: ({ content }) => ({ content: canvasText(content) }),
30
+ };
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ type Range = { start: number; end: number };
3
+
4
+ let {
5
+ text,
6
+ ranges = [],
7
+ }: {
8
+ text: string;
9
+ ranges?: Range[];
10
+ } = $props();
11
+
12
+ const segments = $derived.by(() => {
13
+ const normalized = ranges
14
+ .map((range) => ({
15
+ start: Math.max(0, Math.min(text.length, range.start)),
16
+ end: Math.max(0, Math.min(text.length, range.end)),
17
+ }))
18
+ .filter((range) => range.end > range.start)
19
+ .sort((left, right) => left.start - right.start);
20
+ const output: Array<{ text: string; highlighted: boolean }> = [];
21
+ let offset = 0;
22
+ for (const range of normalized) {
23
+ const start = Math.max(offset, range.start);
24
+ if (start > offset) {
25
+ output.push({ text: text.slice(offset, start), highlighted: false });
26
+ }
27
+ if (range.end > start) {
28
+ output.push({ text: text.slice(start, range.end), highlighted: true });
29
+ }
30
+ offset = Math.max(offset, range.end);
31
+ }
32
+ if (offset < text.length) {
33
+ output.push({ text: text.slice(offset), highlighted: false });
34
+ }
35
+ return output;
36
+ });
37
+ </script>
38
+
39
+ {#each segments as segment, index (`${index}:${segment.text}`)}
40
+ {#if segment.highlighted}<mark>{segment.text}</mark>{:else}{segment.text}{/if}
41
+ {/each}
@@ -0,0 +1,11 @@
1
+ type Range = {
2
+ start: number;
3
+ end: number;
4
+ };
5
+ type $$ComponentProps = {
6
+ text: string;
7
+ ranges?: Range[];
8
+ };
9
+ declare const HighlightedText: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type HighlightedText = ReturnType<typeof HighlightedText>;
11
+ export default HighlightedText;
@@ -0,0 +1,11 @@
1
+ import "./styles.css";
2
+ export { default as manifest } from "../../manifest.json";
3
+ export { SearchManager } from "./search-manager";
4
+ export type { SearchQueryHit, SearchQueryParams, SearchQueryResult, } from "./search-manager";
5
+ export { SearchPanel, SearchToolResult } from "./public-components";
6
+ export { SearchPlugin } from "./search-plugin";
7
+ export { createNotesSearchSlashCommand } from "./notes-search-command";
8
+ export { createNotesSearchTool } from "./notes-search-tool";
9
+ export { DEFAULT_SEARCH_SETTINGS, SEARCH_VIEW_SORT_OPTIONS, SEARCH_EMBEDDING_MODEL_OPTIONS, mergeSearchSettings, patchSearchSettings, resolveSearchEmbeddingProviderConfig, } from "./search-settings";
10
+ export type { SearchPluginSettings, SearchPluginSettingsPatch, SearchResultFacet, SearchRetrievalMode, SearchViewSortMode, } from "./search-settings";
11
+ export { SearchView, SearchViewType } from "./search-view";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import "./styles.css";
2
+ export { default as manifest } from "../../manifest.json";
3
+ export { SearchManager } from "./search-manager";
4
+ export { SearchPanel, SearchToolResult } from "./public-components";
5
+ export { SearchPlugin } from "./search-plugin";
6
+ export { createNotesSearchSlashCommand } from "./notes-search-command";
7
+ export { createNotesSearchTool } from "./notes-search-tool";
8
+ export { DEFAULT_SEARCH_SETTINGS, SEARCH_VIEW_SORT_OPTIONS, SEARCH_EMBEDDING_MODEL_OPTIONS, mergeSearchSettings, patchSearchSettings, resolveSearchEmbeddingProviderConfig, } from "./search-settings";
9
+ export { SearchView, SearchViewType } from "./search-view";
@@ -0,0 +1,2 @@
1
+ import type { AppSlashCommandDefinition } from "@lapis-notes/api/agent-skills";
2
+ export declare function createNotesSearchSlashCommand(): AppSlashCommandDefinition;
@@ -0,0 +1,8 @@
1
+ export function createNotesSearchSlashCommand() {
2
+ return {
3
+ name: "search",
4
+ description: "Search notes in the current conversation scope.",
5
+ argumentHint: "<query>",
6
+ dispatch: { kind: "tool", tool: "notes_search" },
7
+ };
8
+ }
@@ -0,0 +1,8 @@
1
+ import type { AppTool } from "@lapis-notes/api";
2
+ import type { SearchManager } from "./search-manager";
3
+ interface NotesSearchInput {
4
+ query: string;
5
+ limit?: number;
6
+ }
7
+ export declare function createNotesSearchTool(searchManager: Pick<SearchManager, "query">): AppTool<NotesSearchInput>;
8
+ export {};
@@ -0,0 +1,84 @@
1
+ const MARKDOWN_SOURCE_PROVIDER_ID = "search:markdown";
2
+ const HIDDEN_NOTE_SEGMENTS = new Set([".obsidian", ".lapis", ".trash"]);
3
+ const MAX_STRUCTURED_RESULT_BYTES = 48 * 1024;
4
+ export function createNotesSearchTool(searchManager) {
5
+ return {
6
+ name: "notes_search",
7
+ description: `
8
+ Search the user's Lapis Notes using the application's indexed note search.
9
+
10
+ Use this tool whenever the user asks to:
11
+ - find, search, locate, recall, or look up notes
12
+ - find information previously written in their notes
13
+ - discover notes related to a topic
14
+ - search across the current folder/project
15
+
16
+ Prefer this tool over shell commands such as grep, rg, find, or manually
17
+ walking the notes filesystem. This tool uses the application's index and
18
+ respects the current conversation's note scope.
19
+
20
+ Returns lightweight matches. Use read to inspect a selected result.
21
+ `.trim(),
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ query: { type: "string", minLength: 1, maxLength: 1_000 },
26
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
27
+ },
28
+ required: ["query"],
29
+ additionalProperties: false,
30
+ },
31
+ outputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ results: { type: "array" },
35
+ },
36
+ required: ["results"],
37
+ additionalProperties: false,
38
+ },
39
+ effect: "read",
40
+ execute: async (input, context) => executeNotesSearch(searchManager, input, context),
41
+ };
42
+ }
43
+ async function executeNotesSearch(searchManager, input, context) {
44
+ context.signal.throwIfAborted();
45
+ const limit = input.limit ?? 10;
46
+ const result = await searchManager.query({
47
+ term: input.query,
48
+ limit,
49
+ pathPrefix: context.scope.directory,
50
+ sourceProviderIds: [MARKDOWN_SOURCE_PROVIDER_ID],
51
+ });
52
+ context.signal.throwIfAborted();
53
+ const results = [];
54
+ for (const hit of result.hits) {
55
+ if (!isAllowedMarkdownPath(hit.document.path))
56
+ continue;
57
+ const entry = {
58
+ path: hit.document.path,
59
+ score: Number.isFinite(hit.score) ? hit.score : 0,
60
+ snippets: hit.snippets.slice(0, 3).map((snippet) => ({
61
+ text: snippet.text.slice(0, 500),
62
+ offset: Math.max(0, snippet.offset),
63
+ })),
64
+ };
65
+ if (new TextEncoder().encode(JSON.stringify({ results: [...results, entry] }))
66
+ .byteLength > MAX_STRUCTURED_RESULT_BYTES) {
67
+ break;
68
+ }
69
+ results.push(entry);
70
+ if (results.length >= limit)
71
+ break;
72
+ }
73
+ const structuredContent = { results };
74
+ return {
75
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
76
+ structuredContent,
77
+ };
78
+ }
79
+ function isAllowedMarkdownPath(path) {
80
+ const segments = path.split("/");
81
+ const extension = segments.at(-1)?.split(".").at(-1)?.toLowerCase();
82
+ return ((extension === "md" || extension === "markdown") &&
83
+ !segments.some((segment) => HIDDEN_NOTE_SEGMENTS.has(segment)));
84
+ }
@@ -0,0 +1,2 @@
1
+ export { default as SearchPanel } from "./search-panel.svelte";
2
+ export { default as SearchToolResult } from "./search-tool-result.svelte";
@@ -0,0 +1,2 @@
1
+ export { default as SearchPanel } from "./search-panel.svelte";
2
+ export { default as SearchToolResult } from "./search-tool-result.svelte";
@@ -0,0 +1,92 @@
1
+ import { type App, type AppDatabaseSearchDiagnostics, type AppDatabaseSearchScoreBreakdown, type AppDatabaseSearchSnippet, type SearchEmbeddingProviderConfig, type SearchEmbeddingRuntimeStatus, type CachedMetadata, type SearchDocumentRecord, type TFile } from "@lapis-notes/api";
2
+ import { type SearchPluginSettings } from "./search-settings";
3
+ export interface SearchQueryParams {
4
+ term: string;
5
+ limit?: number;
6
+ pathPrefix?: string;
7
+ snippetLength?: number;
8
+ caseSensitive?: boolean;
9
+ mode?: "auto" | "lexical" | "vector" | "hybrid";
10
+ sourceProviderIds?: string[];
11
+ }
12
+ export interface SearchQueryHit {
13
+ id: string;
14
+ score: number;
15
+ document: SearchDocumentRecord;
16
+ snippets: AppDatabaseSearchSnippet[];
17
+ retrievalMode: "lexical" | "vector" | "hybrid";
18
+ scoreBreakdown: AppDatabaseSearchScoreBreakdown;
19
+ matchedChunkIds: string[];
20
+ diagnostics?: AppDatabaseSearchDiagnostics;
21
+ }
22
+ export interface SearchRuntimeStatus {
23
+ backendKind: string;
24
+ provider: SearchEmbeddingProviderConfig | null;
25
+ runtime: SearchEmbeddingRuntimeStatus | null;
26
+ documentCount: number;
27
+ chunkCount: number;
28
+ readyChunkCount: number;
29
+ pendingChunkCount: number;
30
+ errorChunkCount: number;
31
+ lastError: string | null;
32
+ isRefreshing: boolean;
33
+ refreshReason: string | null;
34
+ refreshProgress: {
35
+ processed: number;
36
+ total: number;
37
+ };
38
+ refreshedAt: number | null;
39
+ }
40
+ export interface SearchQueryResult {
41
+ count: number;
42
+ hits: SearchQueryHit[];
43
+ }
44
+ export declare class SearchManager {
45
+ readonly app: App;
46
+ private readonly getSettings;
47
+ private refreshPromise;
48
+ private queuedRefreshReason;
49
+ private refreshState;
50
+ private readonly queuedChanges;
51
+ private readonly queuedDeletes;
52
+ private readonly flushQueuedChanges;
53
+ private readonly ownedSourceProviderIds;
54
+ private changeTrackingReady;
55
+ private sourceGeneration;
56
+ private reconcileCheckpointReady;
57
+ private reconcileCheckpointDirty;
58
+ private reconcileCheckpointGeneration;
59
+ private activeIncrementalOperations;
60
+ private reconcileCheckpointWrite;
61
+ private queuedProcessing;
62
+ private readonly pendingIncrementalOperations;
63
+ private disposed;
64
+ constructor(app: App, getSettings?: () => SearchPluginSettings);
65
+ processChange(file: TFile, content: string, cache: CachedMetadata): Promise<void>;
66
+ processDelete(file: TFile | string): Promise<void>;
67
+ query(params: SearchQueryParams): Promise<SearchQueryResult>;
68
+ getStatus(): Promise<SearchRuntimeStatus>;
69
+ refreshFromVault(reason?: string): Promise<SearchRuntimeStatus>;
70
+ reconcileStartup(): Promise<SearchRuntimeStatus>;
71
+ private runRefreshQueue;
72
+ trackChanges(): () => void;
73
+ dispose(): Promise<void>;
74
+ private processFile;
75
+ private projectionSignature;
76
+ private currentReconcileFingerprint;
77
+ private readReconcileCheckpoint;
78
+ private markReconcileCheckpointDirty;
79
+ private recordSourceMutation;
80
+ private invalidateReconcileCheckpoint;
81
+ private persistReconcileCheckpoint;
82
+ private trackIncrementalOperation;
83
+ private searchManifestRows;
84
+ private metadataManifestRows;
85
+ private usesMetadataPipeline;
86
+ private fileAtPath;
87
+ private processFileSafely;
88
+ private handleProviderFailure;
89
+ private processQueuedChanges;
90
+ private drainQueuedChanges;
91
+ private runRefresh;
92
+ }