dsh-locator 0.6.3 → 0.7.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 +8 -0
- package/README.en.md +2 -2
- package/README.md +6 -3
- package/bin/darwin-arm64/indexd +0 -0
- package/bin/darwin-x64/indexd +0 -0
- package/bin/linux-arm64/indexd +0 -0
- package/bin/linux-x64/indexd +0 -0
- package/bin/win32-arm64/indexd.exe +0 -0
- package/bin/win32-x64/indexd.exe +0 -0
- package/docs/assets/panel-en.png +0 -0
- package/docs/assets/panel-zh.png +0 -0
- package/lib/config.d.ts +2 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +1 -0
- package/lib/config.js.map +1 -1
- package/lib/content.d.ts +39 -24
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +249 -148
- package/lib/content.js.map +1 -1
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +9 -0
- package/lib/http.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +8 -4
- package/lib/index.js.map +1 -1
- package/lib/reindex.d.ts +3 -0
- package/lib/reindex.d.ts.map +1 -0
- package/lib/reindex.js +39 -0
- package/lib/reindex.js.map +1 -0
- package/package.json +1 -1
- package/src/config.ts +3 -0
- package/src/content.ts +225 -118
- package/src/http.ts +9 -0
- package/src/index.ts +9 -4
- package/src/reindex.ts +39 -0
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Config as ConfigSchema } from "./config.js";
|
|
|
7
7
|
import { ContentIndex } from "./content.js";
|
|
8
8
|
import { makeExportTool } from "./export.js";
|
|
9
9
|
import { makeFindTool } from "./find.js";
|
|
10
|
+
import { makeReindexTool } from "./reindex.js";
|
|
10
11
|
import { registerHttp } from "./http.js";
|
|
11
12
|
import { resolveBinary, resolveRoots, resolveStateDir } from "./paths.js";
|
|
12
13
|
import { LocatorRuntime } from "./runtime.js";
|
|
@@ -33,7 +34,7 @@ export function apply(ctx, config) {
|
|
|
33
34
|
const content = config.contentIndex
|
|
34
35
|
? new ContentIndex(rt, {
|
|
35
36
|
stateDir,
|
|
36
|
-
sessionDirs: config.contentSessionDirs,
|
|
37
|
+
sessionDirs: config.contentSessionDirs.length > 0 ? config.contentSessionDirs : undefined,
|
|
37
38
|
maxFileBytes: config.contentMaxFileBytes,
|
|
38
39
|
maxTotalBytes: config.contentMaxTotalBytes,
|
|
39
40
|
maxFiles: config.contentMaxFiles,
|
|
@@ -46,7 +47,7 @@ export function apply(ctx, config) {
|
|
|
46
47
|
rt.start();
|
|
47
48
|
logger.info(`index roots: ${roots.join(', ')} (state ${stateDir})`);
|
|
48
49
|
if (content !== undefined)
|
|
49
|
-
void startContent(rt, content, logger);
|
|
50
|
+
void startContent(rt, content, logger, config.contentRefreshMs);
|
|
50
51
|
}, 1500);
|
|
51
52
|
return () => {
|
|
52
53
|
clearTimeout(timer);
|
|
@@ -58,13 +59,15 @@ export function apply(ctx, config) {
|
|
|
58
59
|
ctx.tools.register(makeStatusTool(rt));
|
|
59
60
|
ctx.tools.register(makeRebuildTool(rt));
|
|
60
61
|
ctx.tools.register(makeExportTool(rt));
|
|
61
|
-
if (content !== undefined)
|
|
62
|
+
if (content !== undefined) {
|
|
62
63
|
ctx.tools.register(makeFindTool(content));
|
|
64
|
+
ctx.tools.register(makeReindexTool(content));
|
|
65
|
+
}
|
|
63
66
|
// The Web GUI panel is optional: profiles without a Web server keep the
|
|
64
67
|
// three tools and never mount an HTTP route.
|
|
65
68
|
registerHttp(ctx, rt, content);
|
|
66
69
|
}
|
|
67
|
-
async function startContent(rt, content, logger) {
|
|
70
|
+
async function startContent(rt, content, logger, refreshMs) {
|
|
68
71
|
try {
|
|
69
72
|
await rt.waitReady();
|
|
70
73
|
for (let i = 0; i < 900; i++) {
|
|
@@ -74,6 +77,7 @@ async function startContent(rt, content, logger) {
|
|
|
74
77
|
await new Promise(resolve => setTimeout(resolve, 2_000));
|
|
75
78
|
}
|
|
76
79
|
content.start();
|
|
80
|
+
content.startAutoRefresh(refreshMs);
|
|
77
81
|
logger.info('content index started');
|
|
78
82
|
}
|
|
79
83
|
catch (error) {
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE5E,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,CAAA;AAEjC,0BAA0B;AAC1B,MAAM,CAAC,MAAM,IAAI,GAAG,aAAa,CAAA;AAEjC,2DAA2D;AAC3D,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,CAAA;AAE/B;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,MAAc;IAChD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAM;IAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IACzB,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACvC,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACzC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IACxC,MAAM,EAAE,GAAG,IAAI,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IACnE,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY;QACjC,CAAC,CAAC,IAAI,YAAY,CAAC,EAAE,EAAE;YACnB,QAAQ;YACR,WAAW,EAAE,MAAM,CAAC,kBAAkB;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/reindex.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../src/reindex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,mDAkCpD"}
|
package/lib/reindex.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** locator_reindex — full or incremental content-index maintenance. */
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
+
export function makeReindexTool(content) {
|
|
4
|
+
return defineTool({
|
|
5
|
+
name: 'locator_reindex',
|
|
6
|
+
description: [
|
|
7
|
+
'Rebuild or refresh the dsh-locator content index (file bodies + DSH session messages).',
|
|
8
|
+
'',
|
|
9
|
+
'`full: true` drops the content database and re-reads everything; `full: false` (default) only re-reads files/sessions whose mtime changed. Name/path search keeps working during either mode. Use `locator_status` or the content state in `locator_find` to watch progress.',
|
|
10
|
+
].join('\n'),
|
|
11
|
+
parameters: {
|
|
12
|
+
full: { type: 'boolean', description: 'Drop and rebuild the content database (default false = incremental).' },
|
|
13
|
+
},
|
|
14
|
+
output: {
|
|
15
|
+
schema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
properties: {
|
|
19
|
+
ok: { type: 'boolean', required: true },
|
|
20
|
+
started: { type: 'boolean' },
|
|
21
|
+
full: { type: 'boolean' },
|
|
22
|
+
error: { type: 'string' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
render: (_args, value) => {
|
|
26
|
+
const v = value;
|
|
27
|
+
return [{ type: 'text', text: v.ok ? `${v.full ? 'Full' : 'Incremental'} content rebuild started.` : `locator_reindex failed: ${v.error ?? 'unknown'}` }];
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
async execute(args) {
|
|
31
|
+
const full = args.full === true;
|
|
32
|
+
const started = full ? content.rebuild() : content.refreshNow();
|
|
33
|
+
if (!started)
|
|
34
|
+
return { ok: false, error: 'a content build is already running' };
|
|
35
|
+
return { ok: true, started: true, full };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=reindex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reindex.js","sourceRoot":"","sources":["../src/reindex.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGnD,MAAM,UAAU,eAAe,CAAC,OAAqB;IACnD,OAAO,UAAU,CAAC;QAChB,IAAI,EAAE,iBAAiB;QACvB,WAAW,EAAE;YACX,wFAAwF;YACxF,EAAE;YACF,8QAA8Q;SAC/Q,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,UAAU,EAAE;YACV,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sEAAsE,EAAE;SAC/G;QACD,MAAM,EAAE;YACN,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,oBAAoB,EAAE,KAAK;gBAC3B,UAAU,EAAE;oBACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;oBACvC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;oBAC5B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;oBACzB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;iBAC1B;aACF;YACD,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBACvB,MAAM,CAAC,GAAG,KAA2E,CAAA;gBACrF,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,2BAA2B,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,KAAK,IAAI,SAAS,EAAE,EAAE,CAAC,CAAA;YAC3J,CAAC;SACF;QACD,KAAK,CAAC,OAAO,CAAC,IAAI;YAChB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAA;YAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAA;YAC/D,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAA;YACxF,OAAO,EAAE,EAAE,EAAE,IAAa,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;QACnD,CAAC;KACF,CAAC,CAAA;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-locator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Whole-disk, millisecond file search for DeepSeek Harness without Everything: a self-built cross-platform index, an Everything-style query language, agent tools and a Web search panel (Ctrl+Shift+F). Windows, macOS and Linux.",
|
|
5
5
|
"author": "PerryLink contributors",
|
|
6
6
|
"repository": {
|
package/src/config.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface Config {
|
|
|
31
31
|
contentMaxFileBytes: number
|
|
32
32
|
/** Stop adding content after this many body bytes; 0 means unlimited. */
|
|
33
33
|
contentMaxTotalBytes: number
|
|
34
|
+
/** Incremental content refresh cadence; 0 disables it. */
|
|
35
|
+
contentRefreshMs: number
|
|
34
36
|
/** Maximum text files indexed per build; 0 means no cap. */
|
|
35
37
|
contentMaxFiles: number
|
|
36
38
|
/** Most recent session logs indexed; 0 means all. */
|
|
@@ -76,6 +78,7 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
76
78
|
contentIndex: Schema.boolean().default(true),
|
|
77
79
|
contentMaxFileBytes: Schema.number().default(4_000_000),
|
|
78
80
|
contentMaxTotalBytes: Schema.number().default(2_000_000_000),
|
|
81
|
+
contentRefreshMs: Schema.number().default(120_000),
|
|
79
82
|
contentMaxFiles: Schema.number().default(200_000),
|
|
80
83
|
contentMaxSessions: Schema.number().default(200),
|
|
81
84
|
contentSessionDirs: Schema.array(Schema.string()).default([]),
|
package/src/content.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content search: text files and DSH session messages in SQLite FTS5.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* File names stay in the Go engine. This store is built from the engine's
|
|
5
|
+
* `dump` RPC plus the session logs: Node reads text bodies, decompresses
|
|
6
|
+
* `session.v4.jsonl.zstd`, and SQLite's trigram tokenizer gives substring and
|
|
7
|
+
* CJK search. A meta table stores each source's mtime so refresh() only
|
|
8
|
+
* re-reads what changed; rebuild() drops and rebuilds everything.
|
|
8
9
|
* @module dsh-locator/content
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node
|
|
|
12
13
|
import { homedir } from 'node:os'
|
|
13
14
|
import { basename, dirname, extname, join } from 'node:path'
|
|
14
15
|
import { zstdDecompressSync } from 'node:zlib'
|
|
16
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
15
17
|
import type { LocatorRuntime } from './runtime.ts'
|
|
16
18
|
|
|
17
19
|
const TEXT_EXTENSIONS = new Set([
|
|
@@ -23,7 +25,6 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
23
25
|
'.sql', '.graphql', '.proto', '.tex', '.org', '.adoc',
|
|
24
26
|
])
|
|
25
27
|
|
|
26
|
-
/** One searchable document. */
|
|
27
28
|
export interface ContentHit {
|
|
28
29
|
kind: 'file' | 'session'
|
|
29
30
|
path: string
|
|
@@ -34,7 +35,6 @@ export interface ContentHit {
|
|
|
34
35
|
score: number
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** Build progress shown by the panel/status. */
|
|
38
38
|
export interface ContentState {
|
|
39
39
|
building: boolean
|
|
40
40
|
files: number
|
|
@@ -45,135 +45,163 @@ export interface ContentState {
|
|
|
45
45
|
updatedAt: number
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
export interface ContentOptions {
|
|
49
|
+
stateDir: string
|
|
50
|
+
sessionDirs?: string[] | undefined
|
|
51
|
+
maxFileBytes: number
|
|
52
|
+
maxTotalBytes?: number
|
|
53
|
+
maxFiles: number
|
|
54
|
+
maxSessions: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DocRow {
|
|
58
|
+
key: string
|
|
59
|
+
body: string
|
|
60
|
+
kind: 'file' | 'session'
|
|
61
|
+
path: string
|
|
62
|
+
title: string
|
|
63
|
+
sessionId: string | null
|
|
64
|
+
messageId: string | null
|
|
65
|
+
mtime: number
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
export class ContentIndex {
|
|
50
|
-
private db:
|
|
69
|
+
private db: DatabaseSync | null = null
|
|
51
70
|
private readonly state: ContentState = { building: false, files: 0, sessions: 0, messages: 0, truncated: false, error: '', updatedAt: 0 }
|
|
52
71
|
private generation = 0
|
|
72
|
+
private totalBytes = 0
|
|
73
|
+
private timer: NodeJS.Timeout | undefined
|
|
53
74
|
|
|
54
|
-
constructor(private readonly rt: LocatorRuntime, private readonly options: {
|
|
55
|
-
stateDir: string
|
|
56
|
-
sessionDirs: string[]
|
|
57
|
-
maxFileBytes: number
|
|
58
|
-
maxTotalBytes?: number
|
|
59
|
-
maxFiles: number
|
|
60
|
-
maxSessions: number
|
|
61
|
-
}) {}
|
|
75
|
+
constructor(private readonly rt: LocatorRuntime, private readonly options: ContentOptions) {}
|
|
62
76
|
|
|
63
77
|
status(): ContentState { return { ...this.state } }
|
|
64
78
|
|
|
65
|
-
/**
|
|
79
|
+
/** Start periodic incremental refreshes; 0 disables. */
|
|
80
|
+
startAutoRefresh(ms: number): void {
|
|
81
|
+
if (ms <= 0 || this.timer !== undefined) return
|
|
82
|
+
this.timer = setInterval(() => this.refresh(), ms)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Queue a full rebuild; false when one is already running. */
|
|
86
|
+
rebuild(): boolean {
|
|
87
|
+
if (this.state.building) return false
|
|
88
|
+
this.start()
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Queue an incremental refresh; false when one is already running. */
|
|
93
|
+
refreshNow(): boolean {
|
|
94
|
+
if (this.state.building) return false
|
|
95
|
+
this.refresh()
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
close(): void {
|
|
100
|
+
if (this.timer !== undefined) { clearInterval(this.timer); this.timer = undefined }
|
|
67
101
|
this.generation++
|
|
68
102
|
this.db?.close()
|
|
69
103
|
this.db = null
|
|
70
104
|
this.state.building = false
|
|
71
105
|
}
|
|
72
106
|
|
|
73
|
-
/** Build
|
|
74
|
-
start(): void {
|
|
107
|
+
/** Build everything from scratch. */
|
|
108
|
+
start(): void { this.begin('full') }
|
|
109
|
+
|
|
110
|
+
/** Re-read changed files/sessions; open the DB when this is the first run. */
|
|
111
|
+
refresh(): void { this.begin(this.db === null ? 'full' : 'incremental') }
|
|
112
|
+
|
|
113
|
+
private begin(mode: 'full' | 'incremental'): void {
|
|
114
|
+
if (this.state.building) return
|
|
75
115
|
const generation = ++this.generation
|
|
76
116
|
this.state.building = true
|
|
77
|
-
this.state.truncated = false
|
|
78
117
|
this.state.error = ''
|
|
79
|
-
|
|
118
|
+
this.state.truncated = false
|
|
119
|
+
const run = mode === 'full' ? this.full(generation) : this.incremental(generation)
|
|
120
|
+
void run.catch(error => {
|
|
80
121
|
if (generation !== this.generation) return
|
|
81
122
|
this.state.error = messageOf(error)
|
|
82
123
|
this.state.building = false
|
|
83
124
|
})
|
|
84
125
|
}
|
|
85
126
|
|
|
86
|
-
private async
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
private async full(generation: number): Promise<void> {
|
|
128
|
+
this.open(true)
|
|
129
|
+
this.totalBytes = 0
|
|
130
|
+
await this.indexFiles(generation)
|
|
131
|
+
if (generation !== this.generation) return
|
|
132
|
+
await this.indexSessions(generation)
|
|
133
|
+
if (generation !== this.generation) return
|
|
134
|
+
this.finish()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async incremental(generation: number): Promise<void> {
|
|
138
|
+
this.open(false)
|
|
139
|
+
this.totalBytes = Number((this.db!.prepare('SELECT COALESCE(SUM(length(body)), 0) AS n FROM docs').get() as { n: number }).n)
|
|
140
|
+
await this.refreshFiles(generation)
|
|
141
|
+
if (generation !== this.generation) return
|
|
142
|
+
await this.refreshSessions(generation)
|
|
143
|
+
if (generation !== this.generation) return
|
|
144
|
+
this.finish()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private open(fresh: boolean): void {
|
|
89
148
|
mkdirSync(this.options.stateDir, { recursive: true })
|
|
90
149
|
this.db?.close()
|
|
91
|
-
this.db = new DatabaseSync(
|
|
150
|
+
this.db = new DatabaseSync(join(this.options.stateDir, 'content.db'))
|
|
92
151
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
93
152
|
this.db.exec('PRAGMA synchronous = OFF')
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
path UNINDEXED,
|
|
99
|
-
title UNINDEXED,
|
|
100
|
-
session_id UNINDEXED,
|
|
101
|
-
message_id UNINDEXED,
|
|
102
|
-
tokenize = 'trigram'
|
|
103
|
-
)`)
|
|
104
|
-
const insert = this.db.prepare('INSERT INTO docs (body, kind, path, title, session_id, message_id) VALUES (?, ?, ?, ?, ?, ?)')
|
|
105
|
-
const flush = (rows: Array<[string, string, string, string, string | null, string | null]>) => {
|
|
106
|
-
if (rows.length === 0) return
|
|
107
|
-
this.db!.exec('BEGIN')
|
|
108
|
-
try {
|
|
109
|
-
for (const row of rows) insert.run(...row)
|
|
110
|
-
this.db!.exec('COMMIT')
|
|
111
|
-
} catch (error) {
|
|
112
|
-
this.db!.exec('ROLLBACK')
|
|
113
|
-
throw error
|
|
114
|
-
}
|
|
153
|
+
if (fresh) {
|
|
154
|
+
this.db.exec('DROP TABLE IF EXISTS docs')
|
|
155
|
+
this.db.exec('DROP TABLE IF EXISTS docs_meta')
|
|
156
|
+
this.db.exec('DROP TABLE IF EXISTS sources')
|
|
115
157
|
}
|
|
158
|
+
this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(
|
|
159
|
+
body, kind UNINDEXED, path UNINDEXED, title UNINDEXED,
|
|
160
|
+
session_id UNINDEXED, message_id UNINDEXED, tokenize = 'trigram'
|
|
161
|
+
)`)
|
|
162
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS docs_meta (rowid INTEGER PRIMARY KEY, doc_key TEXT UNIQUE, path TEXT, mtime INTEGER, kind TEXT)')
|
|
163
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS sources (path TEXT PRIMARY KEY, mtime INTEGER)')
|
|
164
|
+
}
|
|
116
165
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
private finish(): void {
|
|
167
|
+
const counts = this.db!.prepare("SELECT kind, COUNT(*) AS n FROM docs_meta GROUP BY kind").all() as Array<{ kind: string; n: number }>
|
|
168
|
+
this.state.files = Number(counts.find(row => row.kind === 'file')?.n ?? 0)
|
|
169
|
+
this.state.messages = Number(counts.find(row => row.kind === 'session')?.n ?? 0)
|
|
170
|
+
this.state.sessions = Number((this.db!.prepare('SELECT COUNT(*) AS n FROM sources').get() as { n: number }).n)
|
|
122
171
|
this.state.building = false
|
|
123
172
|
this.state.updatedAt = Date.now()
|
|
124
173
|
}
|
|
125
174
|
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
/** Account one body against the total cap; false means the cap was reached. */
|
|
129
|
-
private account(body: string): boolean {
|
|
175
|
+
private addDoc(row: DocRow): boolean {
|
|
130
176
|
const cap = this.options.maxTotalBytes ?? 0
|
|
131
|
-
if (cap > 0 && this.totalBytes + body.length > cap) {
|
|
177
|
+
if (cap > 0 && this.totalBytes + row.body.length > cap) {
|
|
132
178
|
this.state.truncated = true
|
|
133
179
|
return false
|
|
134
180
|
}
|
|
135
|
-
this.
|
|
181
|
+
this.deleteDoc(row.key)
|
|
182
|
+
const meta = this.db!.prepare('INSERT INTO docs_meta (doc_key, path, mtime, kind) VALUES (?, ?, ?, ?)').run(row.key, row.path, row.mtime, row.kind)
|
|
183
|
+
const rowid = meta.lastInsertRowid
|
|
184
|
+
this.db!.prepare('INSERT INTO docs (rowid, body, kind, path, title, session_id, message_id) VALUES (?, ?, ?, ?, ?, ?, ?)').run(rowid, row.body, row.kind, row.path, row.title, row.sessionId, row.messageId)
|
|
185
|
+
this.totalBytes += row.body.length
|
|
136
186
|
return true
|
|
137
187
|
}
|
|
138
188
|
|
|
139
|
-
private
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
files.sort((a, b) => b.mtime - a.mtime)
|
|
145
|
-
const limit = this.options.maxSessions > 0 ? this.options.maxSessions : files.length
|
|
146
|
-
let batch: Array<[string, string, string, string, string | null, string | null]> = []
|
|
147
|
-
for (const file of files.slice(0, limit)) {
|
|
148
|
-
if (generation !== this.generation) return
|
|
149
|
-
let text: string
|
|
150
|
-
try { text = zstdDecompressSync(readFileSync(file.path)).toString('utf8') } catch { continue }
|
|
151
|
-
const sessionId = basename(dirname(file.path))
|
|
152
|
-
let title = ''
|
|
153
|
-
let messages = 0
|
|
154
|
-
for (const line of text.split('\n')) {
|
|
155
|
-
if (!line) continue
|
|
156
|
-
let event: { type?: string; data?: Record<string, unknown> }
|
|
157
|
-
try { event = JSON.parse(line) } catch { continue }
|
|
158
|
-
const extracted = messageText(event)
|
|
159
|
-
if (extracted === undefined) continue
|
|
160
|
-
if (title === '') title = extracted.text.replace(/\s+/g, ' ').slice(0, 90)
|
|
161
|
-
const body = extracted.text.slice(0, 8_000)
|
|
162
|
-
if (!this.account(body)) break
|
|
163
|
-
batch.push([body, 'session', file.path, title || sessionId, sessionId, extracted.id])
|
|
164
|
-
messages++
|
|
165
|
-
if (batch.length >= 200) { flush(batch); batch = [] }
|
|
166
|
-
}
|
|
167
|
-
if (this.state.truncated) { flush(batch); return }
|
|
168
|
-
this.state.sessions++
|
|
169
|
-
this.state.messages += messages
|
|
170
|
-
}
|
|
171
|
-
flush(batch)
|
|
189
|
+
private deleteDoc(key: string): void {
|
|
190
|
+
const row = this.db!.prepare('SELECT rowid FROM docs_meta WHERE doc_key = ?').get(key) as { rowid: number } | undefined
|
|
191
|
+
if (row === undefined) return
|
|
192
|
+
this.db!.prepare('DELETE FROM docs WHERE rowid = ?').run(row.rowid)
|
|
193
|
+
this.db!.prepare('DELETE FROM docs_meta WHERE rowid = ?').run(row.rowid)
|
|
172
194
|
}
|
|
173
195
|
|
|
174
|
-
private
|
|
196
|
+
private deletePath(path: string): void {
|
|
197
|
+
const rows = this.db!.prepare('SELECT rowid FROM docs_meta WHERE path = ?').all(path) as Array<{ rowid: number }>
|
|
198
|
+
const delDoc = this.db!.prepare('DELETE FROM docs WHERE rowid = ?')
|
|
199
|
+
const delMeta = this.db!.prepare('DELETE FROM docs_meta WHERE rowid = ?')
|
|
200
|
+
for (const row of rows) { delDoc.run(row.rowid); delMeta.run(row.rowid) }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async indexFiles(generation: number): Promise<void> {
|
|
175
204
|
let after = 0
|
|
176
|
-
let batch: Array<[string, string, string, string, string | null, string | null]> = []
|
|
177
205
|
let seen = 0
|
|
178
206
|
for (;;) {
|
|
179
207
|
if (generation !== this.generation) return
|
|
@@ -182,29 +210,104 @@ export class ContentIndex {
|
|
|
182
210
|
if (entry.dir || entry.size <= 0 || entry.size > this.options.maxFileBytes) continue
|
|
183
211
|
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue
|
|
184
212
|
if (this.options.maxFiles > 0 && seen >= this.options.maxFiles) break
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (raw.includes('\u0000')) continue
|
|
188
|
-
const body = raw.slice(0, this.options.maxFileBytes)
|
|
189
|
-
if (!this.account(body)) { flush(batch); return }
|
|
213
|
+
const row = this.readFileDoc(entry.path, entry.name, entry.mtime)
|
|
214
|
+
if (row === null) continue
|
|
190
215
|
seen++
|
|
191
|
-
this.
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
if (!this.addDoc(row)) return
|
|
217
|
+
}
|
|
218
|
+
after = page.next
|
|
219
|
+
if (page.done || page.entries.length === 0) break
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async refreshFiles(generation: number): Promise<void> {
|
|
224
|
+
const meta = new Map<string, number>()
|
|
225
|
+
for (const row of this.db!.prepare("SELECT doc_key, mtime FROM docs_meta WHERE kind = 'file'").all() as Array<{ doc_key: string; mtime: number }>) meta.set(row.doc_key, row.mtime)
|
|
226
|
+
const seen = new Set<string>()
|
|
227
|
+
let after = 0
|
|
228
|
+
let indexed = 0
|
|
229
|
+
for (;;) {
|
|
230
|
+
if (generation !== this.generation) return
|
|
231
|
+
const page = await this.rt.dump(after, 2_000)
|
|
232
|
+
for (const entry of page.entries) {
|
|
233
|
+
if (entry.dir || entry.size <= 0 || entry.size > this.options.maxFileBytes) continue
|
|
234
|
+
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue
|
|
235
|
+
if (this.options.maxFiles > 0 && indexed >= this.options.maxFiles) break
|
|
236
|
+
const key = `file:${entry.path}`
|
|
237
|
+
seen.add(key)
|
|
238
|
+
indexed++
|
|
239
|
+
if (meta.get(key) === entry.mtime) continue
|
|
240
|
+
this.deleteDoc(key)
|
|
241
|
+
const row = this.readFileDoc(entry.path, entry.name, entry.mtime)
|
|
242
|
+
if (row !== null && !this.addDoc(row)) return
|
|
194
243
|
}
|
|
195
244
|
after = page.next
|
|
196
245
|
if (page.done || page.entries.length === 0) break
|
|
197
246
|
}
|
|
198
|
-
|
|
247
|
+
for (const key of meta.keys()) if (!seen.has(key)) this.deleteDoc(key)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private readFileDoc(path: string, name: string, mtime: number): DocRow | null {
|
|
251
|
+
let raw: string
|
|
252
|
+
try { raw = readFileSync(path, 'utf8') } catch { return null }
|
|
253
|
+
if (raw.includes('\u0000')) return null
|
|
254
|
+
return { key: `file:${path}`, body: raw.slice(0, this.options.maxFileBytes), kind: 'file', path, title: name, sessionId: null, messageId: null, mtime }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async indexSessions(generation: number): Promise<void> {
|
|
258
|
+
const files = sessionFiles(this.sessionRoots()).slice(0, this.options.maxSessions > 0 ? this.options.maxSessions : undefined)
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
if (generation !== this.generation) return
|
|
261
|
+
this.deletePath(file.path)
|
|
262
|
+
if (!this.indexSessionFile(file)) return
|
|
263
|
+
this.db!.prepare('INSERT OR REPLACE INTO sources (path, mtime) VALUES (?, ?)').run(file.path, file.mtime)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async refreshSessions(generation: number): Promise<void> {
|
|
268
|
+
const known = new Map<string, number>()
|
|
269
|
+
for (const row of this.db!.prepare('SELECT path, mtime FROM sources').all() as Array<{ path: string; mtime: number }>) known.set(row.path, row.mtime)
|
|
270
|
+
const files = sessionFiles(this.sessionRoots()).slice(0, this.options.maxSessions > 0 ? this.options.maxSessions : undefined)
|
|
271
|
+
const present = new Set<string>()
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
if (generation !== this.generation) return
|
|
274
|
+
present.add(file.path)
|
|
275
|
+
if (known.get(file.path) === file.mtime) continue
|
|
276
|
+
this.deletePath(file.path)
|
|
277
|
+
if (!this.indexSessionFile(file)) return
|
|
278
|
+
this.db!.prepare('INSERT OR REPLACE INTO sources (path, mtime) VALUES (?, ?)').run(file.path, file.mtime)
|
|
279
|
+
}
|
|
280
|
+
for (const path of known.keys()) {
|
|
281
|
+
if (present.has(path)) continue
|
|
282
|
+
this.deletePath(path)
|
|
283
|
+
this.db!.prepare('DELETE FROM sources WHERE path = ?').run(path)
|
|
284
|
+
}
|
|
199
285
|
}
|
|
200
286
|
|
|
201
|
-
private
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
287
|
+
private indexSessionFile(file: { path: string; mtime: number }): boolean {
|
|
288
|
+
let text: string
|
|
289
|
+
try { text = zstdDecompressSync(readFileSync(file.path)).toString('utf8') } catch { return true }
|
|
290
|
+
const sessionId = basename(dirname(file.path))
|
|
291
|
+
let title = ''
|
|
292
|
+
for (const line of text.split('\n')) {
|
|
293
|
+
if (!line) continue
|
|
294
|
+
let event: { type?: string; data?: Record<string, unknown> }
|
|
295
|
+
try { event = JSON.parse(line) } catch { continue }
|
|
296
|
+
const extracted = messageText(event)
|
|
297
|
+
if (extracted === undefined) continue
|
|
298
|
+
if (title === '') title = extracted.text.replace(/\s+/g, ' ').slice(0, 90)
|
|
299
|
+
const body = extracted.text.slice(0, 8_000)
|
|
300
|
+
const row: DocRow = { key: `session:${sessionId}:${extracted.id}`, body, kind: 'session', path: file.path, title: title || sessionId, sessionId, messageId: extracted.id, mtime: file.mtime }
|
|
301
|
+
if (!this.addDoc(row)) return false
|
|
302
|
+
}
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
sessionRoots(): string[] {
|
|
307
|
+
if (this.options.sessionDirs !== undefined) return this.options.sessionDirs
|
|
308
|
+
return [join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'sessions')]
|
|
205
309
|
}
|
|
206
310
|
|
|
207
|
-
/** Query the FTS table; short queries fall back to LIKE because trigram needs 3 chars. */
|
|
208
311
|
search(query: string, limit: number): ContentHit[] {
|
|
209
312
|
if (this.db === null) return []
|
|
210
313
|
const text = query.trim()
|
|
@@ -253,22 +356,26 @@ function messageText(event: { type?: string; data?: Record<string, unknown> }):
|
|
|
253
356
|
.join('\n')
|
|
254
357
|
.trim()
|
|
255
358
|
if (text === '') return undefined
|
|
256
|
-
const id = event.type === 'user/message'
|
|
257
|
-
? String(data.id ?? '')
|
|
258
|
-
: String(message.id ?? '')
|
|
359
|
+
const id = event.type === 'user/message' ? String(data.id ?? '') : String(message.id ?? '')
|
|
259
360
|
return { text, id: id || `seq-${String(data.seq ?? '')}` }
|
|
260
361
|
}
|
|
261
362
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
363
|
+
function sessionFiles(roots: string[]): Array<{ path: string; mtime: number }> {
|
|
364
|
+
const out: Array<{ path: string; mtime: number }> = []
|
|
365
|
+
const walk = (root: string, depth: number): void => {
|
|
366
|
+
if (depth > 4 || !existsSync(root)) return
|
|
367
|
+
let entries
|
|
368
|
+
try { entries = readdirSync(root, { withFileTypes: true }) } catch { return }
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const path = join(root, entry.name)
|
|
371
|
+
if (entry.isDirectory()) { walk(path, depth + 1); continue }
|
|
372
|
+
if (!entry.isFile() || !entry.name.endsWith('.v4.jsonl.zstd')) continue
|
|
373
|
+
try { out.push({ path, mtime: statSync(path).mtimeMs }) } catch { /* ignore */ }
|
|
374
|
+
}
|
|
271
375
|
}
|
|
376
|
+
for (const root of roots) walk(root, 0)
|
|
377
|
+
out.sort((a, b) => b.mtime - a.mtime)
|
|
378
|
+
return out
|
|
272
379
|
}
|
|
273
380
|
|
|
274
381
|
function messageOf(error: unknown): string {
|
package/src/http.ts
CHANGED
|
@@ -71,6 +71,15 @@ export function registerHttp(ctx: Context, rt: LocatorRuntime, content?: Content
|
|
|
71
71
|
handler: handler(scoped, async body => await reveal(rt, body)),
|
|
72
72
|
}))
|
|
73
73
|
if (content !== undefined) {
|
|
74
|
+
scoped.effect(() => server.register({
|
|
75
|
+
kind: 'exact',
|
|
76
|
+
path: '/dsh-locator/content/rebuild',
|
|
77
|
+
handler: handler(scoped, async body => {
|
|
78
|
+
const full = body.full === true
|
|
79
|
+
const started = full ? content.rebuild() : content.refreshNow()
|
|
80
|
+
return started ? { ok: true, started: true, full } : { ok: false, error: 'a content build is already running' }
|
|
81
|
+
}),
|
|
82
|
+
}))
|
|
74
83
|
scoped.effect(() => server.register({
|
|
75
84
|
kind: 'exact',
|
|
76
85
|
path: '/dsh-locator/content',
|