@gmickel/gno 0.29.1 → 0.30.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 +17 -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/get.ts +4 -1
- package/src/cli/commands/graph.ts +4 -1
- 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/README.md
CHANGED
|
@@ -39,6 +39,13 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
|
|
|
39
39
|
- **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
40
|
- **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
41
|
- **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`
|
|
42
|
+
- **Workspace UI Polish**: richer scholarly-dusk presentation across dashboard, tabs, search, ask, footer, and global styling without introducing external font or asset dependencies
|
|
43
|
+
|
|
44
|
+
## What's New in v0.30
|
|
45
|
+
|
|
46
|
+
- **Headless Daemon Mode**: `gno daemon` keeps your index fresh continuously without opening the Web UI
|
|
47
|
+
- **CLI Concurrency Hardening**: read-only commands no longer trip transient `database is locked` errors when they overlap with `gno update`
|
|
48
|
+
- **Web/Desktop UI Polish**: sharper workspace styling across dashboard, tabs, search, ask, and footer surfaces
|
|
42
49
|
|
|
43
50
|
### v0.24
|
|
44
51
|
|
|
@@ -144,6 +151,7 @@ gno query "ECONNREFUSED 127.0.0.1:5432" --thorough
|
|
|
144
151
|
```bash
|
|
145
152
|
gno init ~/notes --name notes # Point at your docs
|
|
146
153
|
gno index # Build search index
|
|
154
|
+
gno daemon # Keep index fresh in background (foreground process)
|
|
147
155
|
gno query "auth best practices" # Hybrid search
|
|
148
156
|
gno ask "summarize the API" --answer # AI answer with citations
|
|
149
157
|
```
|
|
@@ -174,6 +182,15 @@ Verify everything works:
|
|
|
174
182
|
gno doctor
|
|
175
183
|
```
|
|
176
184
|
|
|
185
|
+
Keep an index fresh continuously without opening the Web UI:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
gno daemon
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`gno daemon` runs as a foreground watcher/sync/embed process. Use `nohup`,
|
|
192
|
+
launchd, or systemd if you want it supervised long-term.
|
|
193
|
+
|
|
177
194
|
### Connect to AI Agents
|
|
178
195
|
|
|
179
196
|
#### MCP Server (Claude Desktop, Cursor, Zed, etc.)
|
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
|
+
}
|
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
|
}
|
|
@@ -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")
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { Config } from "../config/types";
|
|
2
|
+
import type { SyncResult } from "../ingestion";
|
|
3
|
+
import type { DocumentEventBus } from "./doc-events";
|
|
4
|
+
import type { EmbedResult, EmbedScheduler } from "./embed-scheduler";
|
|
5
|
+
import type { ContextHolder } from "./routes/api";
|
|
6
|
+
import type {
|
|
7
|
+
CollectionWatchCallbacks,
|
|
8
|
+
CollectionWatchService,
|
|
9
|
+
} from "./watch-service";
|
|
10
|
+
|
|
11
|
+
import { getIndexDbPath } from "../app/constants";
|
|
12
|
+
import {
|
|
13
|
+
ensureDirectories,
|
|
14
|
+
getConfigPaths,
|
|
15
|
+
isInitialized,
|
|
16
|
+
loadConfig,
|
|
17
|
+
} from "../config";
|
|
18
|
+
import { defaultSyncService } from "../ingestion";
|
|
19
|
+
import { getActivePreset } from "../llm/registry";
|
|
20
|
+
import { SqliteAdapter } from "../store/sqlite/adapter";
|
|
21
|
+
import {
|
|
22
|
+
createServerContext,
|
|
23
|
+
type CreateServerContextOptions,
|
|
24
|
+
disposeServerContext,
|
|
25
|
+
type ServerContext,
|
|
26
|
+
} from "./context";
|
|
27
|
+
import { createEmbedScheduler } from "./embed-scheduler";
|
|
28
|
+
import { CollectionWatchService as DefaultCollectionWatchService } from "./watch-service";
|
|
29
|
+
|
|
30
|
+
export interface BackgroundRuntimeOptions {
|
|
31
|
+
configPath?: string;
|
|
32
|
+
index?: string;
|
|
33
|
+
requireCollections?: boolean;
|
|
34
|
+
offline?: boolean;
|
|
35
|
+
eventBus?: DocumentEventBus | null;
|
|
36
|
+
watchCallbacks?: CollectionWatchCallbacks;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BackgroundRuntime {
|
|
40
|
+
store: SqliteAdapter;
|
|
41
|
+
config: Config;
|
|
42
|
+
actualConfigPath: string;
|
|
43
|
+
ctxHolder: ContextHolder;
|
|
44
|
+
scheduler: EmbedScheduler;
|
|
45
|
+
eventBus: DocumentEventBus | null;
|
|
46
|
+
watchService: CollectionWatchService;
|
|
47
|
+
syncAll(options?: {
|
|
48
|
+
gitPull?: boolean;
|
|
49
|
+
runUpdateCmd?: boolean;
|
|
50
|
+
triggerEmbed?: boolean;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
syncResult: SyncResult;
|
|
53
|
+
embedResult: EmbedResult | null;
|
|
54
|
+
}>;
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type BackgroundRuntimeResult =
|
|
59
|
+
| { success: true; runtime: BackgroundRuntime }
|
|
60
|
+
| { success: false; error: string };
|
|
61
|
+
|
|
62
|
+
type BackgroundRuntimeDeps = {
|
|
63
|
+
isInitialized?: typeof isInitialized;
|
|
64
|
+
loadConfig?: typeof loadConfig;
|
|
65
|
+
getConfigPaths?: typeof getConfigPaths;
|
|
66
|
+
ensureDirectories?: typeof ensureDirectories;
|
|
67
|
+
storeFactory?: () => SqliteAdapter;
|
|
68
|
+
createServerContext?: (
|
|
69
|
+
store: SqliteAdapter,
|
|
70
|
+
config: Config,
|
|
71
|
+
options?: CreateServerContextOptions
|
|
72
|
+
) => Promise<ServerContext>;
|
|
73
|
+
disposeServerContext?: (ctx: ServerContext) => Promise<void>;
|
|
74
|
+
createEmbedScheduler?: typeof createEmbedScheduler;
|
|
75
|
+
syncAllService?: typeof defaultSyncService.syncAll;
|
|
76
|
+
watchServiceFactory?: (options: {
|
|
77
|
+
collections: Config["collections"];
|
|
78
|
+
store: SqliteAdapter;
|
|
79
|
+
scheduler: EmbedScheduler | null;
|
|
80
|
+
eventBus?: DocumentEventBus | null;
|
|
81
|
+
callbacks?: CollectionWatchCallbacks;
|
|
82
|
+
}) => CollectionWatchService;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export async function startBackgroundRuntime(
|
|
86
|
+
options: BackgroundRuntimeOptions = {},
|
|
87
|
+
deps: BackgroundRuntimeDeps = {}
|
|
88
|
+
): Promise<BackgroundRuntimeResult> {
|
|
89
|
+
const syncAllService = deps.syncAllService
|
|
90
|
+
? (...args: Parameters<typeof defaultSyncService.syncAll>) =>
|
|
91
|
+
deps.syncAllService!(...args)
|
|
92
|
+
: defaultSyncService.syncAll.bind(defaultSyncService);
|
|
93
|
+
const initialized = await (deps.isInitialized ?? isInitialized)(
|
|
94
|
+
options.configPath
|
|
95
|
+
);
|
|
96
|
+
if (!initialized) {
|
|
97
|
+
return { success: false, error: "GNO not initialized. Run: gno init" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const configResult = await (deps.loadConfig ?? loadConfig)(
|
|
101
|
+
options.configPath
|
|
102
|
+
);
|
|
103
|
+
if (!configResult.ok) {
|
|
104
|
+
return { success: false, error: configResult.error.message };
|
|
105
|
+
}
|
|
106
|
+
const config = configResult.value;
|
|
107
|
+
|
|
108
|
+
if (options.requireCollections && config.collections.length === 0) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: "No collections configured. Run: gno collection add <path>",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await (deps.ensureDirectories ?? ensureDirectories)();
|
|
116
|
+
|
|
117
|
+
const store = deps.storeFactory ? deps.storeFactory() : new SqliteAdapter();
|
|
118
|
+
const dbPath = getIndexDbPath(options.index);
|
|
119
|
+
const paths = (deps.getConfigPaths ?? getConfigPaths)();
|
|
120
|
+
const actualConfigPath = options.configPath ?? paths.configFile;
|
|
121
|
+
store.setConfigPath(actualConfigPath);
|
|
122
|
+
|
|
123
|
+
const openResult = await store.open(dbPath, config.ftsTokenizer);
|
|
124
|
+
if (!openResult.ok) {
|
|
125
|
+
return { success: false, error: openResult.error.message };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const syncCollResult = await store.syncCollections(config.collections);
|
|
129
|
+
if (!syncCollResult.ok) {
|
|
130
|
+
await store.close();
|
|
131
|
+
return { success: false, error: syncCollResult.error.message };
|
|
132
|
+
}
|
|
133
|
+
const syncCtxResult = await store.syncContexts(config.contexts ?? []);
|
|
134
|
+
if (!syncCtxResult.ok) {
|
|
135
|
+
await store.close();
|
|
136
|
+
return { success: false, error: syncCtxResult.error.message };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ctx = await (deps.createServerContext ?? createServerContext)(
|
|
140
|
+
store,
|
|
141
|
+
config,
|
|
142
|
+
{
|
|
143
|
+
offline: options.offline ?? false,
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const ctxHolder: ContextHolder = {
|
|
148
|
+
current: ctx,
|
|
149
|
+
config,
|
|
150
|
+
scheduler: null,
|
|
151
|
+
eventBus: options.eventBus ?? null,
|
|
152
|
+
watchService: null,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const scheduler = (deps.createEmbedScheduler ?? createEmbedScheduler)({
|
|
156
|
+
db: store.getRawDb(),
|
|
157
|
+
getEmbedPort: () => ctxHolder.current.embedPort,
|
|
158
|
+
getVectorIndex: () => ctxHolder.current.vectorIndex,
|
|
159
|
+
getModelUri: () => getActivePreset(ctxHolder.config).embed,
|
|
160
|
+
});
|
|
161
|
+
ctxHolder.scheduler = scheduler;
|
|
162
|
+
ctxHolder.current.scheduler = scheduler;
|
|
163
|
+
ctxHolder.current.eventBus = options.eventBus ?? null;
|
|
164
|
+
|
|
165
|
+
const watchService = (
|
|
166
|
+
deps.watchServiceFactory ??
|
|
167
|
+
((watchOptions) => new DefaultCollectionWatchService(watchOptions))
|
|
168
|
+
)({
|
|
169
|
+
collections: config.collections,
|
|
170
|
+
store,
|
|
171
|
+
scheduler,
|
|
172
|
+
eventBus: options.eventBus ?? null,
|
|
173
|
+
callbacks: options.watchCallbacks,
|
|
174
|
+
});
|
|
175
|
+
watchService.start();
|
|
176
|
+
ctxHolder.watchService = watchService;
|
|
177
|
+
ctxHolder.current.watchService = watchService;
|
|
178
|
+
|
|
179
|
+
let disposed = false;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
runtime: {
|
|
184
|
+
store,
|
|
185
|
+
config,
|
|
186
|
+
actualConfigPath,
|
|
187
|
+
ctxHolder,
|
|
188
|
+
scheduler,
|
|
189
|
+
eventBus: options.eventBus ?? null,
|
|
190
|
+
watchService,
|
|
191
|
+
async syncAll(syncOptions = {}) {
|
|
192
|
+
const syncResult = await syncAllService(config.collections, store, {
|
|
193
|
+
gitPull: syncOptions.gitPull,
|
|
194
|
+
runUpdateCmd: syncOptions.runUpdateCmd,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let embedResult: EmbedResult | null = null;
|
|
198
|
+
if (syncOptions.triggerEmbed !== false) {
|
|
199
|
+
embedResult = await scheduler.triggerNow();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { syncResult, embedResult };
|
|
203
|
+
},
|
|
204
|
+
async dispose() {
|
|
205
|
+
if (disposed) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
disposed = true;
|
|
209
|
+
await Promise.resolve(watchService.dispose());
|
|
210
|
+
options.eventBus?.close();
|
|
211
|
+
scheduler.dispose();
|
|
212
|
+
await (deps.disposeServerContext ?? disposeServerContext)(
|
|
213
|
+
ctxHolder.current
|
|
214
|
+
);
|
|
215
|
+
await store.close();
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/serve/context.ts
CHANGED
|
@@ -80,13 +80,18 @@ export interface ServerContext {
|
|
|
80
80
|
watchService?: CollectionWatchService | null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export interface CreateServerContextOptions {
|
|
84
|
+
offline?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
83
87
|
/**
|
|
84
88
|
* Initialize server context with LLM ports.
|
|
85
89
|
* Attempts to load models; missing models are logged but don't fail.
|
|
86
90
|
*/
|
|
87
91
|
export async function createServerContext(
|
|
88
92
|
store: SqliteAdapter,
|
|
89
|
-
config: Config
|
|
93
|
+
config: Config,
|
|
94
|
+
options: CreateServerContextOptions = {}
|
|
90
95
|
): Promise<ServerContext> {
|
|
91
96
|
let embedPort: EmbeddingPort | null = null;
|
|
92
97
|
let expandPort: GenerationPort | null = null;
|
|
@@ -99,7 +104,9 @@ export async function createServerContext(
|
|
|
99
104
|
const llm = new LlmAdapter(config);
|
|
100
105
|
|
|
101
106
|
// Resolve download policy from env (serve has no CLI flags)
|
|
102
|
-
const policy = resolveDownloadPolicy(process.env, {
|
|
107
|
+
const policy = resolveDownloadPolicy(process.env, {
|
|
108
|
+
offline: options.offline ?? false,
|
|
109
|
+
});
|
|
103
110
|
|
|
104
111
|
// Progress callback updates downloadState for WebUI polling
|
|
105
112
|
const createPortOptions = (type: ModelType): CreatePortOptions => ({
|
|
@@ -231,8 +238,9 @@ export async function disposeServerContext(ctx: ServerContext): Promise<void> {
|
|
|
231
238
|
*/
|
|
232
239
|
export async function reloadServerContext(
|
|
233
240
|
ctx: ServerContext,
|
|
234
|
-
newConfig?: Config
|
|
241
|
+
newConfig?: Config,
|
|
242
|
+
options: CreateServerContextOptions = {}
|
|
235
243
|
): Promise<ServerContext> {
|
|
236
244
|
await disposeServerContext(ctx);
|
|
237
|
-
return createServerContext(ctx.store, newConfig ?? ctx.config);
|
|
245
|
+
return createServerContext(ctx.store, newConfig ?? ctx.config, options);
|
|
238
246
|
}
|
package/src/serve/index.ts
CHANGED