@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.
- package/CHANGELOG.md +6 -0
- package/LICENSE.md +8 -0
- package/README.md +20 -0
- package/dist/agent-tools.d.ts +1 -0
- package/dist/agent-tools.js +1 -0
- package/dist/built-in-search-document-providers.d.ts +5 -0
- package/dist/built-in-search-document-providers.js +30 -0
- package/dist/highlighted-text.svelte +41 -0
- package/dist/highlighted-text.svelte.d.ts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/notes-search-command.d.ts +2 -0
- package/dist/notes-search-command.js +8 -0
- package/dist/notes-search-tool.d.ts +8 -0
- package/dist/notes-search-tool.js +84 -0
- package/dist/public-components.d.ts +2 -0
- package/dist/public-components.js +2 -0
- package/dist/search-manager.d.ts +92 -0
- package/dist/search-manager.js +771 -0
- package/dist/search-match-context.d.ts +17 -0
- package/dist/search-match-context.js +45 -0
- package/dist/search-panel.css +460 -0
- package/dist/search-panel.svelte +796 -0
- package/dist/search-panel.svelte.d.ts +11 -0
- package/dist/search-plugin.d.ts +22 -0
- package/dist/search-plugin.js +174 -0
- package/dist/search-result-model.d.ts +29 -0
- package/dist/search-result-model.js +50 -0
- package/dist/search-settings-tab.d.ts +12 -0
- package/dist/search-settings-tab.js +139 -0
- package/dist/search-settings.d.ts +54 -0
- package/dist/search-settings.js +107 -0
- package/dist/search-sort.d.ts +8 -0
- package/dist/search-sort.js +36 -0
- package/dist/search-startup.d.ts +3 -0
- package/dist/search-startup.js +4 -0
- package/dist/search-tool-result.css +70 -0
- package/dist/search-tool-result.d.ts +16 -0
- package/dist/search-tool-result.js +111 -0
- package/dist/search-tool-result.svelte +55 -0
- package/dist/search-tool-result.svelte.d.ts +6 -0
- package/dist/search-view-type.d.ts +1 -0
- package/dist/search-view-type.js +1 -0
- package/dist/search-view.d.ts +14 -0
- package/dist/search-view.js +48 -0
- package/dist/styles.css +2 -0
- package/manifest.json +28 -0
- package/package.json +80 -0
- package/styles.css +1 -0
package/CHANGELOG.md
ADDED
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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,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,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
|
+
}
|