dsh-locator 0.6.2 → 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 +14 -0
- package/README.en.md +2 -2
- package/README.md +11 -7
- 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 +4 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +5 -3
- package/lib/config.js.map +1 -1
- package/lib/content.d.ts +40 -20
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +257 -131
- package/lib/content.js.map +1 -1
- package/lib/find.d.ts.map +1 -1
- package/lib/find.js +1 -0
- package/lib/find.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 +9 -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 +9 -3
- package/src/content.ts +233 -104
- package/src/find.ts +1 -0
- package/src/http.ts +9 -0
- package/src/index.ts +10 -4
- package/src/reindex.ts +39 -0
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAUzC,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,CAAA;AAEjC,0BAA0B;AAC1B,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC,2DAA2D;AAC3D,eAAO,MAAM,MAAM,UAAY,CAAA;AAE/B;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CA6CxD"}
|
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,8 +34,9 @@ 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,
|
|
39
|
+
maxTotalBytes: config.contentMaxTotalBytes,
|
|
38
40
|
maxFiles: config.contentMaxFiles,
|
|
39
41
|
maxSessions: config.contentMaxSessions,
|
|
40
42
|
})
|
|
@@ -45,7 +47,7 @@ export function apply(ctx, config) {
|
|
|
45
47
|
rt.start();
|
|
46
48
|
logger.info(`index roots: ${roots.join(', ')} (state ${stateDir})`);
|
|
47
49
|
if (content !== undefined)
|
|
48
|
-
void startContent(rt, content, logger);
|
|
50
|
+
void startContent(rt, content, logger, config.contentRefreshMs);
|
|
49
51
|
}, 1500);
|
|
50
52
|
return () => {
|
|
51
53
|
clearTimeout(timer);
|
|
@@ -57,13 +59,15 @@ export function apply(ctx, config) {
|
|
|
57
59
|
ctx.tools.register(makeStatusTool(rt));
|
|
58
60
|
ctx.tools.register(makeRebuildTool(rt));
|
|
59
61
|
ctx.tools.register(makeExportTool(rt));
|
|
60
|
-
if (content !== undefined)
|
|
62
|
+
if (content !== undefined) {
|
|
61
63
|
ctx.tools.register(makeFindTool(content));
|
|
64
|
+
ctx.tools.register(makeReindexTool(content));
|
|
65
|
+
}
|
|
62
66
|
// The Web GUI panel is optional: profiles without a Web server keep the
|
|
63
67
|
// three tools and never mount an HTTP route.
|
|
64
68
|
registerHttp(ctx, rt, content);
|
|
65
69
|
}
|
|
66
|
-
async function startContent(rt, content, logger) {
|
|
70
|
+
async function startContent(rt, content, logger, refreshMs) {
|
|
67
71
|
try {
|
|
68
72
|
await rt.waitReady();
|
|
69
73
|
for (let i = 0; i < 900; i++) {
|
|
@@ -73,6 +77,7 @@ async function startContent(rt, content, logger) {
|
|
|
73
77
|
await new Promise(resolve => setTimeout(resolve, 2_000));
|
|
74
78
|
}
|
|
75
79
|
content.start();
|
|
80
|
+
content.startAutoRefresh(refreshMs);
|
|
76
81
|
logger.info('content index started');
|
|
77
82
|
}
|
|
78
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
|
@@ -29,6 +29,10 @@ export interface Config {
|
|
|
29
29
|
contentIndex: boolean
|
|
30
30
|
/** Maximum bytes read from one text file. */
|
|
31
31
|
contentMaxFileBytes: number
|
|
32
|
+
/** Stop adding content after this many body bytes; 0 means unlimited. */
|
|
33
|
+
contentMaxTotalBytes: number
|
|
34
|
+
/** Incremental content refresh cadence; 0 disables it. */
|
|
35
|
+
contentRefreshMs: number
|
|
32
36
|
/** Maximum text files indexed per build; 0 means no cap. */
|
|
33
37
|
contentMaxFiles: number
|
|
34
38
|
/** Most recent session logs indexed; 0 means all. */
|
|
@@ -72,9 +76,11 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
72
76
|
scanWorkers: Schema.number().default(0),
|
|
73
77
|
memoryLimitMB: Schema.number().default(0),
|
|
74
78
|
contentIndex: Schema.boolean().default(true),
|
|
75
|
-
contentMaxFileBytes: Schema.number().default(
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
contentMaxFileBytes: Schema.number().default(4_000_000),
|
|
80
|
+
contentMaxTotalBytes: Schema.number().default(2_000_000_000),
|
|
81
|
+
contentRefreshMs: Schema.number().default(120_000),
|
|
82
|
+
contentMaxFiles: Schema.number().default(200_000),
|
|
83
|
+
contentMaxSessions: Schema.number().default(200),
|
|
78
84
|
contentSessionDirs: Schema.array(Schema.string()).default([]),
|
|
79
85
|
windowsUsn: Schema.boolean().default(false),
|
|
80
86
|
stateDir: 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,126 +35,173 @@ 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
|
|
41
41
|
sessions: number
|
|
42
42
|
messages: number
|
|
43
|
+
truncated: boolean
|
|
43
44
|
error: string
|
|
44
45
|
updatedAt: number
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
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
|
+
|
|
48
68
|
export class ContentIndex {
|
|
49
|
-
private db:
|
|
50
|
-
private readonly state: ContentState = { building: false, files: 0, sessions: 0, messages: 0, error: '', updatedAt: 0 }
|
|
69
|
+
private db: DatabaseSync | null = null
|
|
70
|
+
private readonly state: ContentState = { building: false, files: 0, sessions: 0, messages: 0, truncated: false, error: '', updatedAt: 0 }
|
|
51
71
|
private generation = 0
|
|
72
|
+
private totalBytes = 0
|
|
73
|
+
private timer: NodeJS.Timeout | undefined
|
|
52
74
|
|
|
53
|
-
constructor(private readonly rt: LocatorRuntime, private readonly options: {
|
|
54
|
-
stateDir: string
|
|
55
|
-
sessionDirs: string[]
|
|
56
|
-
maxFileBytes: number
|
|
57
|
-
maxFiles: number
|
|
58
|
-
maxSessions: number
|
|
59
|
-
}) {}
|
|
75
|
+
constructor(private readonly rt: LocatorRuntime, private readonly options: ContentOptions) {}
|
|
60
76
|
|
|
61
77
|
status(): ContentState { return { ...this.state } }
|
|
62
78
|
|
|
63
|
-
/**
|
|
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
|
+
|
|
64
99
|
close(): void {
|
|
100
|
+
if (this.timer !== undefined) { clearInterval(this.timer); this.timer = undefined }
|
|
65
101
|
this.generation++
|
|
66
102
|
this.db?.close()
|
|
67
103
|
this.db = null
|
|
68
104
|
this.state.building = false
|
|
69
105
|
}
|
|
70
106
|
|
|
71
|
-
/** Build
|
|
72
|
-
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
|
|
73
115
|
const generation = ++this.generation
|
|
74
116
|
this.state.building = true
|
|
75
117
|
this.state.error = ''
|
|
76
|
-
|
|
118
|
+
this.state.truncated = false
|
|
119
|
+
const run = mode === 'full' ? this.full(generation) : this.incremental(generation)
|
|
120
|
+
void run.catch(error => {
|
|
77
121
|
if (generation !== this.generation) return
|
|
78
122
|
this.state.error = messageOf(error)
|
|
79
123
|
this.state.building = false
|
|
80
124
|
})
|
|
81
125
|
}
|
|
82
126
|
|
|
83
|
-
private async
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
86
148
|
mkdirSync(this.options.stateDir, { recursive: true })
|
|
87
149
|
this.db?.close()
|
|
88
|
-
this.db = new DatabaseSync(
|
|
150
|
+
this.db = new DatabaseSync(join(this.options.stateDir, 'content.db'))
|
|
89
151
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
90
152
|
this.db.exec('PRAGMA synchronous = OFF')
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
path UNINDEXED,
|
|
96
|
-
title UNINDEXED,
|
|
97
|
-
session_id UNINDEXED,
|
|
98
|
-
message_id UNINDEXED,
|
|
99
|
-
tokenize = 'trigram'
|
|
100
|
-
)`)
|
|
101
|
-
const insert = this.db.prepare('INSERT INTO docs (body, kind, path, title, session_id, message_id) VALUES (?, ?, ?, ?, ?, ?)')
|
|
102
|
-
const flush = (rows: Array<[string, string, string, string, string | null, string | null]>) => {
|
|
103
|
-
if (rows.length === 0) return
|
|
104
|
-
this.db!.exec('BEGIN')
|
|
105
|
-
try {
|
|
106
|
-
for (const row of rows) insert.run(...row)
|
|
107
|
-
this.db!.exec('COMMIT')
|
|
108
|
-
} catch (error) {
|
|
109
|
-
this.db!.exec('ROLLBACK')
|
|
110
|
-
throw error
|
|
111
|
-
}
|
|
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')
|
|
112
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
|
+
}
|
|
113
165
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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)
|
|
118
171
|
this.state.building = false
|
|
119
172
|
this.state.updatedAt = Date.now()
|
|
120
173
|
}
|
|
121
174
|
|
|
122
|
-
private
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
files.sort((a, b) => b.mtime - a.mtime)
|
|
128
|
-
const limit = this.options.maxSessions > 0 ? this.options.maxSessions : files.length
|
|
129
|
-
let batch: Array<[string, string, string, string, string | null, string | null]> = []
|
|
130
|
-
for (const file of files.slice(0, limit)) {
|
|
131
|
-
if (generation !== this.generation) return
|
|
132
|
-
let text: string
|
|
133
|
-
try { text = zstdDecompressSync(readFileSync(file.path)).toString('utf8') } catch { continue }
|
|
134
|
-
const sessionId = basename(dirname(file.path))
|
|
135
|
-
let title = ''
|
|
136
|
-
let messages = 0
|
|
137
|
-
for (const line of text.split('\n')) {
|
|
138
|
-
if (!line) continue
|
|
139
|
-
let event: { type?: string; data?: Record<string, unknown> }
|
|
140
|
-
try { event = JSON.parse(line) } catch { continue }
|
|
141
|
-
const extracted = messageText(event)
|
|
142
|
-
if (extracted === undefined) continue
|
|
143
|
-
if (title === '') title = extracted.text.replace(/\s+/g, ' ').slice(0, 90)
|
|
144
|
-
batch.push([extracted.text.slice(0, 8_000), 'session', file.path, title || sessionId, sessionId, extracted.id])
|
|
145
|
-
messages++
|
|
146
|
-
if (batch.length >= 200) { flush(batch); batch = [] }
|
|
147
|
-
}
|
|
148
|
-
this.state.sessions++
|
|
149
|
-
this.state.messages += messages
|
|
175
|
+
private addDoc(row: DocRow): boolean {
|
|
176
|
+
const cap = this.options.maxTotalBytes ?? 0
|
|
177
|
+
if (cap > 0 && this.totalBytes + row.body.length > cap) {
|
|
178
|
+
this.state.truncated = true
|
|
179
|
+
return false
|
|
150
180
|
}
|
|
151
|
-
|
|
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
|
|
186
|
+
return true
|
|
152
187
|
}
|
|
153
188
|
|
|
154
|
-
private
|
|
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)
|
|
194
|
+
}
|
|
195
|
+
|
|
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> {
|
|
155
204
|
let after = 0
|
|
156
|
-
let batch: Array<[string, string, string, string, string | null, string | null]> = []
|
|
157
205
|
let seen = 0
|
|
158
206
|
for (;;) {
|
|
159
207
|
if (generation !== this.generation) return
|
|
@@ -162,27 +210,104 @@ export class ContentIndex {
|
|
|
162
210
|
if (entry.dir || entry.size <= 0 || entry.size > this.options.maxFileBytes) continue
|
|
163
211
|
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue
|
|
164
212
|
if (this.options.maxFiles > 0 && seen >= this.options.maxFiles) break
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (body.includes('\u0000')) continue
|
|
213
|
+
const row = this.readFileDoc(entry.path, entry.name, entry.mtime)
|
|
214
|
+
if (row === null) continue
|
|
168
215
|
seen++
|
|
169
|
-
this.
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
243
|
}
|
|
173
244
|
after = page.next
|
|
174
245
|
if (page.done || page.entries.length === 0) break
|
|
175
246
|
}
|
|
176
|
-
|
|
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
|
+
}
|
|
177
285
|
}
|
|
178
286
|
|
|
179
|
-
private
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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')]
|
|
183
309
|
}
|
|
184
310
|
|
|
185
|
-
/** Query the FTS table; short queries fall back to LIKE because trigram needs 3 chars. */
|
|
186
311
|
search(query: string, limit: number): ContentHit[] {
|
|
187
312
|
if (this.db === null) return []
|
|
188
313
|
const text = query.trim()
|
|
@@ -231,22 +356,26 @@ function messageText(event: { type?: string; data?: Record<string, unknown> }):
|
|
|
231
356
|
.join('\n')
|
|
232
357
|
.trim()
|
|
233
358
|
if (text === '') return undefined
|
|
234
|
-
const id = event.type === 'user/message'
|
|
235
|
-
? String(data.id ?? '')
|
|
236
|
-
: String(message.id ?? '')
|
|
359
|
+
const id = event.type === 'user/message' ? String(data.id ?? '') : String(message.id ?? '')
|
|
237
360
|
return { text, id: id || `seq-${String(data.seq ?? '')}` }
|
|
238
361
|
}
|
|
239
362
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
}
|
|
249
375
|
}
|
|
376
|
+
for (const root of roots) walk(root, 0)
|
|
377
|
+
out.sort((a, b) => b.mtime - a.mtime)
|
|
378
|
+
return out
|
|
250
379
|
}
|
|
251
380
|
|
|
252
381
|
function messageOf(error: unknown): string {
|
package/src/find.ts
CHANGED
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',
|