@gmickel/gno 0.29.1 → 0.31.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/README.md +50 -0
- package/package.json +1 -1
- package/src/cli/commands/ask.ts +1 -0
- package/src/cli/commands/completion/scripts.ts +2 -0
- package/src/cli/commands/daemon.ts +159 -0
- package/src/cli/commands/doctor.ts +12 -0
- package/src/cli/commands/embed.ts +40 -12
- package/src/cli/commands/get.ts +4 -1
- package/src/cli/commands/graph.ts +4 -1
- package/src/cli/commands/index-cmd.ts +14 -2
- package/src/cli/commands/links.ts +12 -3
- package/src/cli/commands/ls.ts +4 -1
- package/src/cli/commands/multi-get.ts +4 -1
- package/src/cli/commands/query.ts +1 -0
- package/src/cli/commands/search.ts +1 -0
- package/src/cli/commands/shared.ts +6 -0
- package/src/cli/commands/vsearch.ts +1 -0
- package/src/cli/program.ts +26 -0
- package/src/serve/background-runtime.ts +219 -0
- package/src/serve/context.ts +12 -4
- package/src/serve/index.ts +6 -0
- package/src/serve/public/app.tsx +14 -11
- package/src/serve/public/components/WorkspaceTabs.tsx +13 -7
- package/src/serve/public/globals.css +113 -10
- package/src/serve/public/pages/Ask.tsx +15 -6
- package/src/serve/public/pages/Collections.tsx +1 -1
- package/src/serve/public/pages/Dashboard.tsx +27 -19
- package/src/serve/public/pages/Search.tsx +1 -1
- package/src/serve/server.ts +20 -85
- package/src/serve/watch-service.ts +47 -5
- package/src/store/migrations/runner.ts +12 -2
- package/src/store/sqlite/adapter.ts +22 -1
- package/src/store/vector/stats.ts +56 -22
- package/src/store/vector/types.ts +5 -2
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
|
|
|
20
20
|
|
|
21
21
|
- [Quick Start](#quick-start)
|
|
22
22
|
- [Installation](#installation)
|
|
23
|
+
- [Daemon Mode](#daemon-mode)
|
|
23
24
|
- [Search Modes](#search-modes)
|
|
24
25
|
- [Agent Integration](#agent-integration)
|
|
25
26
|
- [Web UI](#web-ui)
|
|
@@ -39,6 +40,20 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
|
|
|
39
40
|
- **GNO Desktop Beta**: first mac-first desktop beta shell with deep-link routing, singleton handoff, and the same onboarding/search/edit flows as `gno serve`
|
|
40
41
|
- **Desktop Onboarding Polish**: guided setup now covers folders, presets, model readiness, indexing, connectors, import preview, app tabs, file actions, and recovery without drift between web and desktop
|
|
41
42
|
- **Default Preset Upgrade**: `slim-tuned` is now the built-in default, using the fine-tuned retrieval expansion model while keeping the same embed, rerank, and answer stack as `slim`
|
|
43
|
+
- **Workspace UI Polish**: richer scholarly-dusk presentation across dashboard, tabs, search, ask, footer, and global styling without introducing external font or asset dependencies
|
|
44
|
+
|
|
45
|
+
## What's New in v0.30
|
|
46
|
+
|
|
47
|
+
- **Headless Daemon Mode**: `gno daemon` keeps your index fresh continuously without opening the Web UI
|
|
48
|
+
- **CLI Concurrency Hardening**: read-only commands no longer trip transient `database is locked` errors when they overlap with `gno update`
|
|
49
|
+
- **Web/Desktop UI Polish**: sharper workspace styling across dashboard, tabs, search, ask, and footer surfaces
|
|
50
|
+
|
|
51
|
+
## What's New in v0.31
|
|
52
|
+
|
|
53
|
+
- **Windows Desktop Beta Artifact**: release flow now includes a packaged `windows-x64` desktop beta zip, not just source-level support claims
|
|
54
|
+
- **Packaged Runtime Proof**: Windows desktop packaging validates bundled Bun + staged GNO runtime + FTS5 + vendored snowball + `sqlite-vec`
|
|
55
|
+
- **Scoped Index Fix**: `gno index <collection>` now embeds only that collection instead of accidentally burning through unrelated backlog from other collections
|
|
56
|
+
- **CLI Reporting Fix**: long embed runs now report sane durations instead of bogus sub-second summaries
|
|
42
57
|
|
|
43
58
|
### v0.24
|
|
44
59
|
|
|
@@ -144,6 +159,7 @@ gno query "ECONNREFUSED 127.0.0.1:5432" --thorough
|
|
|
144
159
|
```bash
|
|
145
160
|
gno init ~/notes --name notes # Point at your docs
|
|
146
161
|
gno index # Build search index
|
|
162
|
+
gno daemon # Keep index fresh in background (foreground process)
|
|
147
163
|
gno query "auth best practices" # Hybrid search
|
|
148
164
|
gno ask "summarize the API" --answer # AI answer with citations
|
|
149
165
|
```
|
|
@@ -174,6 +190,21 @@ Verify everything works:
|
|
|
174
190
|
gno doctor
|
|
175
191
|
```
|
|
176
192
|
|
|
193
|
+
**Windows**: current validated target is `windows-x64`. See
|
|
194
|
+
[docs/WINDOWS.md](./docs/WINDOWS.md) for the support stance and packaged desktop
|
|
195
|
+
beta notes.
|
|
196
|
+
|
|
197
|
+
Keep an index fresh continuously without opening the Web UI:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
gno daemon
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`gno daemon` runs as a foreground watcher/sync/embed process. Use `nohup`,
|
|
204
|
+
launchd, or systemd if you want it supervised long-term.
|
|
205
|
+
|
|
206
|
+
See also: [docs/DAEMON.md](./docs/DAEMON.md)
|
|
207
|
+
|
|
177
208
|
### Connect to AI Agents
|
|
178
209
|
|
|
179
210
|
#### MCP Server (Claude Desktop, Cursor, Zed, etc.)
|
|
@@ -211,6 +242,25 @@ gno skill install --target all # All targets
|
|
|
211
242
|
|
|
212
243
|
---
|
|
213
244
|
|
|
245
|
+
## Daemon Mode
|
|
246
|
+
|
|
247
|
+
Use `gno daemon` when you want continuous indexing without the browser or
|
|
248
|
+
desktop shell open.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
gno daemon
|
|
252
|
+
gno daemon --no-sync-on-start
|
|
253
|
+
nohup gno daemon > /tmp/gno-daemon.log 2>&1 &
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
It reuses the same watch/sync/embed runtime as `gno serve`, but stays
|
|
257
|
+
headless. In v0.30 it is foreground-only and does not expose built-in
|
|
258
|
+
`start/stop/status` management.
|
|
259
|
+
|
|
260
|
+
[Daemon guide →](https://gno.sh/docs/DAEMON/)
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
214
264
|
## SDK
|
|
215
265
|
|
|
216
266
|
Embed GNO directly in another Bun or TypeScript app. No CLI subprocesses. No local server required.
|
package/package.json
CHANGED
package/src/cli/commands/ask.ts
CHANGED
|
@@ -28,6 +28,7 @@ const COMMANDS = [
|
|
|
28
28
|
"get",
|
|
29
29
|
"multi-get",
|
|
30
30
|
"ls",
|
|
31
|
+
"daemon",
|
|
31
32
|
"serve",
|
|
32
33
|
"mcp",
|
|
33
34
|
"mcp serve",
|
|
@@ -330,6 +331,7 @@ function getCommandDescription(cmd: string): string {
|
|
|
330
331
|
get: "Get document by URI",
|
|
331
332
|
"multi-get": "Get multiple documents",
|
|
332
333
|
ls: "List indexed documents",
|
|
334
|
+
daemon: "Start headless continuous indexing",
|
|
333
335
|
serve: "Start web UI server",
|
|
334
336
|
mcp: "MCP server and configuration",
|
|
335
337
|
"mcp serve": "Start MCP server",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { CollectionSyncResult } from "../../ingestion";
|
|
2
|
+
import type { BackgroundRuntimeResult } from "../../serve/background-runtime";
|
|
3
|
+
|
|
4
|
+
import { startBackgroundRuntime } from "../../serve/background-runtime";
|
|
5
|
+
|
|
6
|
+
export interface DaemonOptions {
|
|
7
|
+
configPath?: string;
|
|
8
|
+
index?: string;
|
|
9
|
+
offline?: boolean;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
quiet?: boolean;
|
|
12
|
+
noSyncOnStart?: boolean;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type DaemonResult =
|
|
17
|
+
| { success: true }
|
|
18
|
+
| { success: false; error: string };
|
|
19
|
+
|
|
20
|
+
type DaemonLogger = {
|
|
21
|
+
log: (message: string) => void;
|
|
22
|
+
error: (message: string) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type DaemonDeps = {
|
|
26
|
+
startBackgroundRuntime?: typeof startBackgroundRuntime;
|
|
27
|
+
logger?: DaemonLogger;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function formatCollectionSyncSummary(result: CollectionSyncResult): string {
|
|
31
|
+
return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createSignalPromise(
|
|
35
|
+
signal: AbortSignal | undefined,
|
|
36
|
+
logger: DaemonLogger,
|
|
37
|
+
quiet: boolean
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
if (signal?.aborted) {
|
|
41
|
+
resolve();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const complete = (message?: string): void => {
|
|
46
|
+
signal?.removeEventListener("abort", onAbort);
|
|
47
|
+
process.off("SIGINT", onSigint);
|
|
48
|
+
process.off("SIGTERM", onSigterm);
|
|
49
|
+
if (message && !quiet) {
|
|
50
|
+
logger.log(message);
|
|
51
|
+
}
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onAbort = (): void => complete("Daemon stopped.");
|
|
56
|
+
const onSigint = (): void => complete("Received SIGINT. Shutting down...");
|
|
57
|
+
const onSigterm = (): void =>
|
|
58
|
+
complete("Received SIGTERM. Shutting down...");
|
|
59
|
+
|
|
60
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
61
|
+
process.once("SIGINT", onSigint);
|
|
62
|
+
process.once("SIGTERM", onSigterm);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function daemon(
|
|
67
|
+
options: DaemonOptions = {},
|
|
68
|
+
deps: DaemonDeps = {}
|
|
69
|
+
): Promise<DaemonResult> {
|
|
70
|
+
const logger = deps.logger ?? {
|
|
71
|
+
log: (message: string) => {
|
|
72
|
+
console.log(message);
|
|
73
|
+
},
|
|
74
|
+
error: (message: string) => {
|
|
75
|
+
console.error(message);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const runtimeResult: BackgroundRuntimeResult = await (
|
|
80
|
+
deps.startBackgroundRuntime ?? startBackgroundRuntime
|
|
81
|
+
)({
|
|
82
|
+
configPath: options.configPath,
|
|
83
|
+
index: options.index,
|
|
84
|
+
requireCollections: true,
|
|
85
|
+
offline: options.offline,
|
|
86
|
+
watchCallbacks: {
|
|
87
|
+
onSyncStart: ({ collection, relPaths }) => {
|
|
88
|
+
if (!options.quiet) {
|
|
89
|
+
logger.log(
|
|
90
|
+
`watch sync started: ${collection} (${relPaths.length} path${relPaths.length === 1 ? "" : "s"})`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
onSyncComplete: ({ result }) => {
|
|
95
|
+
if (!options.quiet) {
|
|
96
|
+
logger.log(formatCollectionSyncSummary(result));
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
onSyncError: ({ collection, error }) => {
|
|
100
|
+
logger.error(
|
|
101
|
+
`watch sync failed: ${collection}: ${error instanceof Error ? error.message : String(error)}`
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (!runtimeResult.success) {
|
|
107
|
+
return { success: false, error: runtimeResult.error };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { runtime } = runtimeResult;
|
|
111
|
+
try {
|
|
112
|
+
if (!options.quiet) {
|
|
113
|
+
logger.log(
|
|
114
|
+
`GNO daemon started for index "${options.index ?? "default"}" using ${runtime.config.collections.length} collection${runtime.config.collections.length === 1 ? "" : "s"}.`
|
|
115
|
+
);
|
|
116
|
+
const watchState = runtime.watchService.getState();
|
|
117
|
+
if (watchState.activeCollections.length > 0) {
|
|
118
|
+
logger.log(`watching: ${watchState.activeCollections.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
if (watchState.failedCollections.length > 0) {
|
|
121
|
+
for (const failed of watchState.failedCollections) {
|
|
122
|
+
logger.error(`watch failed: ${failed.collection}: ${failed.reason}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!options.noSyncOnStart) {
|
|
128
|
+
if (!options.quiet) {
|
|
129
|
+
logger.log("Running initial sync...");
|
|
130
|
+
}
|
|
131
|
+
const { syncResult, embedResult } = await runtime.syncAll({
|
|
132
|
+
runUpdateCmd: true,
|
|
133
|
+
triggerEmbed: true,
|
|
134
|
+
});
|
|
135
|
+
if (!options.quiet) {
|
|
136
|
+
logger.log(
|
|
137
|
+
`sync totals: ${syncResult.totalFilesAdded} added, ${syncResult.totalFilesUpdated} updated, ${syncResult.totalFilesErrored} errors, ${syncResult.totalFilesSkipped} skipped`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!options.quiet && embedResult) {
|
|
141
|
+
logger.log(
|
|
142
|
+
`embed: ${embedResult.embedded} embedded, ${embedResult.errors} errors`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} else if (!options.quiet) {
|
|
146
|
+
logger.log("Skipping initial sync (--no-sync-on-start).");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await createSignalPromise(options.signal, logger, options.quiet ?? false);
|
|
150
|
+
return { success: true };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error),
|
|
155
|
+
};
|
|
156
|
+
} finally {
|
|
157
|
+
await runtime.dispose();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -16,6 +16,7 @@ import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
|
16
16
|
import { getConfigPaths, isInitialized, loadConfig } from "../../config";
|
|
17
17
|
import { ModelCache } from "../../llm/cache";
|
|
18
18
|
import { getActivePreset } from "../../llm/registry";
|
|
19
|
+
import { loadFts5Snowball } from "../../store/sqlite/fts5-snowball";
|
|
19
20
|
import {
|
|
20
21
|
getCustomSqlitePath,
|
|
21
22
|
getExtensionLoadingMode,
|
|
@@ -221,6 +222,17 @@ async function checkSqliteExtensions(): Promise<DoctorCheck[]> {
|
|
|
221
222
|
message: jsonAvailable ? "JSON1 available" : "JSON1 not available",
|
|
222
223
|
});
|
|
223
224
|
|
|
225
|
+
// Probe vendored fts5-snowball extension
|
|
226
|
+
const snowballResult = loadFts5Snowball(db);
|
|
227
|
+
checks.push({
|
|
228
|
+
name: "fts5-snowball",
|
|
229
|
+
status: snowballResult.loaded ? "ok" : "error",
|
|
230
|
+
message: snowballResult.loaded
|
|
231
|
+
? "fts5-snowball loaded"
|
|
232
|
+
: (snowballResult.error ?? "fts5-snowball failed to load"),
|
|
233
|
+
details: snowballResult.path ? [`Path: ${snowballResult.path}`] : undefined,
|
|
234
|
+
});
|
|
235
|
+
|
|
224
236
|
// Probe sqlite-vec extension
|
|
225
237
|
let sqliteVecAvailable = false;
|
|
226
238
|
let sqliteVecVersion = "";
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
export interface EmbedOptions {
|
|
45
45
|
/** Override config path */
|
|
46
46
|
configPath?: string;
|
|
47
|
+
/** Restrict embedding work to a single collection */
|
|
48
|
+
collection?: string;
|
|
47
49
|
/** Override model URI */
|
|
48
50
|
model?: string;
|
|
49
51
|
/** Batch size for embedding */
|
|
@@ -102,6 +104,7 @@ interface BatchContext {
|
|
|
102
104
|
embedPort: EmbeddingPort;
|
|
103
105
|
vectorIndex: VectorIndexPort;
|
|
104
106
|
modelUri: string;
|
|
107
|
+
collection?: string;
|
|
105
108
|
batchSize: number;
|
|
106
109
|
force: boolean;
|
|
107
110
|
showProgress: boolean;
|
|
@@ -127,10 +130,11 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
|
|
|
127
130
|
while (embedded + errors < ctx.totalToEmbed) {
|
|
128
131
|
// Get next batch using seek pagination (cursor-based)
|
|
129
132
|
const batchResult = ctx.force
|
|
130
|
-
? await getActiveChunks(ctx.db, ctx.batchSize, cursor)
|
|
133
|
+
? await getActiveChunks(ctx.db, ctx.batchSize, cursor, ctx.collection)
|
|
131
134
|
: await ctx.stats.getBacklog(ctx.modelUri, {
|
|
132
135
|
limit: ctx.batchSize,
|
|
133
136
|
after: cursor,
|
|
137
|
+
collection: ctx.collection,
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
if (!batchResult.ok) {
|
|
@@ -247,6 +251,7 @@ interface EmbedContext {
|
|
|
247
251
|
*/
|
|
248
252
|
async function initEmbedContext(
|
|
249
253
|
configPath?: string,
|
|
254
|
+
collection?: string,
|
|
250
255
|
model?: string
|
|
251
256
|
): Promise<({ ok: true } & EmbedContext) | { ok: false; error: string }> {
|
|
252
257
|
const initialized = await isInitialized(configPath);
|
|
@@ -259,6 +264,12 @@ async function initEmbedContext(
|
|
|
259
264
|
return { ok: false, error: configResult.error.message };
|
|
260
265
|
}
|
|
261
266
|
const config = configResult.value;
|
|
267
|
+
if (
|
|
268
|
+
collection &&
|
|
269
|
+
!config.collections.some((candidate) => candidate.name === collection)
|
|
270
|
+
) {
|
|
271
|
+
return { ok: false, error: `Collection not found: ${collection}` };
|
|
272
|
+
}
|
|
262
273
|
|
|
263
274
|
const preset = getActivePreset(config);
|
|
264
275
|
const modelUri = model ?? preset.embed;
|
|
@@ -289,7 +300,11 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
|
289
300
|
const dryRun = options.dryRun ?? false;
|
|
290
301
|
|
|
291
302
|
// Initialize config and store
|
|
292
|
-
const initResult = await initEmbedContext(
|
|
303
|
+
const initResult = await initEmbedContext(
|
|
304
|
+
options.configPath,
|
|
305
|
+
options.collection,
|
|
306
|
+
options.model
|
|
307
|
+
);
|
|
293
308
|
if (!initResult.ok) {
|
|
294
309
|
return { success: false, error: initResult.error };
|
|
295
310
|
}
|
|
@@ -306,8 +321,8 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
|
306
321
|
|
|
307
322
|
// Get backlog count first (before loading model)
|
|
308
323
|
const backlogResult = force
|
|
309
|
-
? await getActiveChunkCount(db)
|
|
310
|
-
: await stats.countBacklog(modelUri);
|
|
324
|
+
? await getActiveChunkCount(db, options.collection)
|
|
325
|
+
: await stats.countBacklog(modelUri, { collection: options.collection });
|
|
311
326
|
|
|
312
327
|
if (!backlogResult.ok) {
|
|
313
328
|
return { success: false, error: backlogResult.error.message };
|
|
@@ -392,6 +407,7 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
|
392
407
|
embedPort,
|
|
393
408
|
vectorIndex,
|
|
394
409
|
modelUri,
|
|
410
|
+
collection: options.collection,
|
|
395
411
|
batchSize,
|
|
396
412
|
force,
|
|
397
413
|
showProgress: !options.json,
|
|
@@ -443,19 +459,23 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
|
443
459
|
// Helper: Get all active chunks (for --force mode)
|
|
444
460
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
445
461
|
|
|
446
|
-
function getActiveChunkCount(
|
|
462
|
+
function getActiveChunkCount(
|
|
463
|
+
db: Database,
|
|
464
|
+
collection?: string
|
|
465
|
+
): Promise<StoreResult<number>> {
|
|
447
466
|
try {
|
|
467
|
+
const collectionClause = collection ? " AND d.collection = ?" : "";
|
|
448
468
|
const result = db
|
|
449
469
|
.prepare(
|
|
450
470
|
`
|
|
451
471
|
SELECT COUNT(*) as count FROM content_chunks c
|
|
452
472
|
WHERE EXISTS (
|
|
453
473
|
SELECT 1 FROM documents d
|
|
454
|
-
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1
|
|
474
|
+
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
|
|
455
475
|
)
|
|
456
476
|
`
|
|
457
477
|
)
|
|
458
|
-
.get() as { count: number };
|
|
478
|
+
.get(...(collection ? [collection] : [])) as { count: number };
|
|
459
479
|
return Promise.resolve(ok(result.count));
|
|
460
480
|
} catch (e) {
|
|
461
481
|
return Promise.resolve(
|
|
@@ -470,9 +490,11 @@ function getActiveChunkCount(db: Database): Promise<StoreResult<number>> {
|
|
|
470
490
|
function getActiveChunks(
|
|
471
491
|
db: Database,
|
|
472
492
|
limit: number,
|
|
473
|
-
after?: { mirrorHash: string; seq: number }
|
|
493
|
+
after?: { mirrorHash: string; seq: number },
|
|
494
|
+
collection?: string
|
|
474
495
|
): Promise<StoreResult<BacklogItem[]>> {
|
|
475
496
|
try {
|
|
497
|
+
const collectionClause = collection ? " AND d.collection = ?" : "";
|
|
476
498
|
// Include title for contextual embedding
|
|
477
499
|
const sql = after
|
|
478
500
|
? `
|
|
@@ -482,7 +504,7 @@ function getActiveChunks(
|
|
|
482
504
|
FROM content_chunks c
|
|
483
505
|
WHERE EXISTS (
|
|
484
506
|
SELECT 1 FROM documents d
|
|
485
|
-
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1
|
|
507
|
+
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
|
|
486
508
|
)
|
|
487
509
|
AND (c.mirror_hash > ? OR (c.mirror_hash = ? AND c.seq > ?))
|
|
488
510
|
ORDER BY c.mirror_hash, c.seq
|
|
@@ -495,15 +517,21 @@ function getActiveChunks(
|
|
|
495
517
|
FROM content_chunks c
|
|
496
518
|
WHERE EXISTS (
|
|
497
519
|
SELECT 1 FROM documents d
|
|
498
|
-
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1
|
|
520
|
+
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
|
|
499
521
|
)
|
|
500
522
|
ORDER BY c.mirror_hash, c.seq
|
|
501
523
|
LIMIT ?
|
|
502
524
|
`;
|
|
503
525
|
|
|
504
526
|
const params = after
|
|
505
|
-
? [
|
|
506
|
-
|
|
527
|
+
? [
|
|
528
|
+
...(collection ? [collection] : []),
|
|
529
|
+
after.mirrorHash,
|
|
530
|
+
after.mirrorHash,
|
|
531
|
+
after.seq,
|
|
532
|
+
limit,
|
|
533
|
+
]
|
|
534
|
+
: [...(collection ? [collection] : []), limit];
|
|
507
535
|
|
|
508
536
|
const results = db.prepare(sql).all(...params) as BacklogItem[];
|
|
509
537
|
return Promise.resolve(ok(results));
|
package/src/cli/commands/get.ts
CHANGED
|
@@ -110,7 +110,10 @@ export async function get(
|
|
|
110
110
|
return { success: false, error: parsed.error, isValidation: true };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
const initResult = await initStore({
|
|
113
|
+
const initResult = await initStore({
|
|
114
|
+
configPath: options.configPath,
|
|
115
|
+
syncConfig: false,
|
|
116
|
+
});
|
|
114
117
|
if (!initResult.ok) {
|
|
115
118
|
return { success: false, error: initResult.error };
|
|
116
119
|
}
|
|
@@ -49,7 +49,10 @@ export type GraphCommandResult =
|
|
|
49
49
|
export async function graph(
|
|
50
50
|
options: GraphOptions = {}
|
|
51
51
|
): Promise<GraphCommandResult> {
|
|
52
|
-
const initResult = await initStore({
|
|
52
|
+
const initResult = await initStore({
|
|
53
|
+
configPath: options.configPath,
|
|
54
|
+
syncConfig: false,
|
|
55
|
+
});
|
|
53
56
|
if (!initResult.ok) {
|
|
54
57
|
return { success: false, error: initResult.error };
|
|
55
58
|
}
|
|
@@ -71,6 +71,7 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
|
|
|
71
71
|
const { embed } = await import("./embed");
|
|
72
72
|
const result = await embed({
|
|
73
73
|
configPath: options.configPath,
|
|
74
|
+
collection: options.collection,
|
|
74
75
|
verbose: options.verbose,
|
|
75
76
|
});
|
|
76
77
|
if (result.success) {
|
|
@@ -95,6 +96,15 @@ export function formatIndex(
|
|
|
95
96
|
result: IndexResult,
|
|
96
97
|
options: IndexOptions
|
|
97
98
|
): string {
|
|
99
|
+
function formatDuration(seconds: number): string {
|
|
100
|
+
if (seconds < 60) {
|
|
101
|
+
return `${seconds.toFixed(1)}s`;
|
|
102
|
+
}
|
|
103
|
+
const mins = Math.floor(seconds / 60);
|
|
104
|
+
const secs = seconds % 60;
|
|
105
|
+
return `${mins}m ${secs.toFixed(0)}s`;
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
if (!result.success) {
|
|
99
109
|
return `Error: ${result.error}`;
|
|
100
110
|
}
|
|
@@ -110,10 +120,12 @@ export function formatIndex(
|
|
|
110
120
|
} else if (result.embedResult) {
|
|
111
121
|
lines.push("");
|
|
112
122
|
const { embedded, errors, duration } = result.embedResult;
|
|
113
|
-
const errPart = errors > 0 ? ` (${errors} errors)` : "";
|
|
114
123
|
lines.push(
|
|
115
|
-
`Embedded ${embedded} chunks in ${(duration
|
|
124
|
+
`Embedded ${embedded.toLocaleString()} chunks in ${formatDuration(duration)}`
|
|
116
125
|
);
|
|
126
|
+
if (errors > 0) {
|
|
127
|
+
lines.push(`${errors.toLocaleString()} chunks failed to embed.`);
|
|
128
|
+
}
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
return lines.join("\n");
|
|
@@ -315,7 +315,10 @@ export async function linksList(
|
|
|
315
315
|
docRef: string,
|
|
316
316
|
options: LinksListOptions = {}
|
|
317
317
|
): Promise<LinksListResult> {
|
|
318
|
-
const initResult = await initStore({
|
|
318
|
+
const initResult = await initStore({
|
|
319
|
+
configPath: options.configPath,
|
|
320
|
+
syncConfig: false,
|
|
321
|
+
});
|
|
319
322
|
if (!initResult.ok) {
|
|
320
323
|
return { success: false, error: initResult.error };
|
|
321
324
|
}
|
|
@@ -428,7 +431,10 @@ export async function backlinks(
|
|
|
428
431
|
docRef: string,
|
|
429
432
|
options: BacklinksOptions = {}
|
|
430
433
|
): Promise<BacklinksResult> {
|
|
431
|
-
const initResult = await initStore({
|
|
434
|
+
const initResult = await initStore({
|
|
435
|
+
configPath: options.configPath,
|
|
436
|
+
syncConfig: false,
|
|
437
|
+
});
|
|
432
438
|
if (!initResult.ok) {
|
|
433
439
|
return { success: false, error: initResult.error };
|
|
434
440
|
}
|
|
@@ -507,7 +513,10 @@ export async function similar(
|
|
|
507
513
|
const threshold = options.threshold ?? 0.7;
|
|
508
514
|
const crossCollection = options.crossCollection ?? false;
|
|
509
515
|
|
|
510
|
-
const initResult = await initStore({
|
|
516
|
+
const initResult = await initStore({
|
|
517
|
+
configPath: options.configPath,
|
|
518
|
+
syncConfig: false,
|
|
519
|
+
});
|
|
511
520
|
if (!initResult.ok) {
|
|
512
521
|
return { success: false, error: initResult.error };
|
|
513
522
|
}
|
package/src/cli/commands/ls.ts
CHANGED
|
@@ -109,7 +109,10 @@ export async function ls(
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const initResult = await initStore({
|
|
112
|
+
const initResult = await initStore({
|
|
113
|
+
configPath: options.configPath,
|
|
114
|
+
syncConfig: false,
|
|
115
|
+
});
|
|
113
116
|
if (!initResult.ok) {
|
|
114
117
|
return { success: false, error: initResult.error };
|
|
115
118
|
}
|
|
@@ -250,7 +250,10 @@ export async function multiGet(
|
|
|
250
250
|
const maxBytes = options.maxBytes ?? 10_240;
|
|
251
251
|
const allRefs = splitRefs(refs);
|
|
252
252
|
|
|
253
|
-
const initResult = await initStore({
|
|
253
|
+
const initResult = await initStore({
|
|
254
|
+
configPath: options.configPath,
|
|
255
|
+
syncConfig: false,
|
|
256
|
+
});
|
|
254
257
|
if (!initResult.ok) {
|
|
255
258
|
return { success: false, error: initResult.error };
|
|
256
259
|
}
|
|
@@ -36,6 +36,8 @@ export interface InitStoreOptions {
|
|
|
36
36
|
indexName?: string;
|
|
37
37
|
/** Filter to single collection by name */
|
|
38
38
|
collection?: string;
|
|
39
|
+
/** Sync collections/contexts from config into DB on open */
|
|
40
|
+
syncConfig?: boolean;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -99,6 +101,10 @@ export async function initStore(
|
|
|
99
101
|
return { ok: false, error: openResult.error.message };
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
if (options.syncConfig === false) {
|
|
105
|
+
return { ok: true, store, config, collections, actualConfigPath };
|
|
106
|
+
}
|
|
107
|
+
|
|
102
108
|
// Sync collections from config to DB
|
|
103
109
|
const syncCollResult = await store.syncCollections(config.collections);
|
|
104
110
|
if (!syncCollResult.ok) {
|
package/src/cli/program.ts
CHANGED
|
@@ -188,6 +188,7 @@ export function createProgram(): Command {
|
|
|
188
188
|
wireGraphCommand(program);
|
|
189
189
|
wireMcpCommand(program);
|
|
190
190
|
wireSkillCommands(program);
|
|
191
|
+
wireDaemonCommand(program);
|
|
191
192
|
wireServeCommand(program);
|
|
192
193
|
wireCompletionCommand(program);
|
|
193
194
|
|
|
@@ -2038,6 +2039,31 @@ function wireGraphCommand(program: Command): void {
|
|
|
2038
2039
|
// Serve Command (web UI)
|
|
2039
2040
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2040
2041
|
|
|
2042
|
+
function wireDaemonCommand(program: Command): void {
|
|
2043
|
+
program
|
|
2044
|
+
.command("daemon")
|
|
2045
|
+
.description("Start headless continuous indexing")
|
|
2046
|
+
.option(
|
|
2047
|
+
"--no-sync-on-start",
|
|
2048
|
+
"skip initial sync and only watch future file changes"
|
|
2049
|
+
)
|
|
2050
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
2051
|
+
const globals = getGlobals();
|
|
2052
|
+
const { daemon } = await import("./commands/daemon.js");
|
|
2053
|
+
const result = await daemon({
|
|
2054
|
+
configPath: globals.config,
|
|
2055
|
+
index: globals.index,
|
|
2056
|
+
offline: globals.offline,
|
|
2057
|
+
verbose: globals.verbose,
|
|
2058
|
+
quiet: globals.quiet,
|
|
2059
|
+
noSyncOnStart: cmdOpts.syncOnStart === false,
|
|
2060
|
+
});
|
|
2061
|
+
if (!result.success) {
|
|
2062
|
+
throw new CliError("RUNTIME", result.error);
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2041
2067
|
function wireServeCommand(program: Command): void {
|
|
2042
2068
|
program
|
|
2043
2069
|
.command("serve")
|