@gmickel/gno 1.17.0 → 1.18.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 +14 -2
- package/assets/skill/SKILL.md +17 -1
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +2 -2
- package/src/cli/commands/daemon.ts +69 -2
- package/src/cli/commands/models/pull.ts +13 -3
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/detach.ts +37 -20
- package/src/cli/program.ts +74 -27
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +37 -0
- package/src/core/job-manager.ts +19 -0
- package/src/core/mutation-generations.ts +33 -0
- package/src/llm/cache.ts +13 -3
- package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
- package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
- package/src/mcp/context.ts +161 -0
- package/src/mcp/http-security.ts +477 -0
- package/src/mcp/http-session.ts +272 -0
- package/src/mcp/http-transport.ts +370 -0
- package/src/mcp/resources/index.ts +141 -134
- package/src/mcp/server.ts +19 -79
- package/src/mcp/tools/add-collection.ts +3 -1
- package/src/mcp/tools/capture.ts +3 -0
- package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
- package/src/mcp/tools/context.ts +9 -8
- package/src/mcp/tools/embed.ts +62 -52
- package/src/mcp/tools/index-cmd.ts +88 -74
- package/src/mcp/tools/index.ts +22 -2
- package/src/mcp/tools/remove-collection.ts +2 -0
- package/src/mcp/tools/status.ts +11 -0
- package/src/mcp/tools/sync.ts +16 -14
- package/src/mcp/tools/workspace-write.ts +7 -3
- package/src/serve/background-runtime.ts +12 -212
- package/src/serve/embed-scheduler.ts +74 -43
- package/src/serve/index.ts +9 -0
- package/src/serve/jobs.ts +78 -80
- package/src/serve/public/components/HealthCenter.tsx +74 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/resident-admission.ts +159 -0
- package/src/serve/resident-background-work.ts +39 -0
- package/src/serve/resident-request.ts +55 -0
- package/src/serve/resident-runtime.ts +490 -0
- package/src/serve/resident-status.ts +96 -0
- package/src/serve/routes/api.ts +263 -167
- package/src/serve/routes/mcp.ts +69 -0
- package/src/serve/server.ts +191 -37
- package/src/serve/status-model.ts +51 -0
- package/src/serve/status.ts +5 -0
- package/src/store/sqlite/adapter.ts +26 -9
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
|
|
|
94
94
|
|
|
95
95
|
<!-- public-truth:current-version -->
|
|
96
96
|
|
|
97
|
-
> Current release: **v1.
|
|
97
|
+
> Current release: **v1.17.0** — see [CHANGELOG.md](./CHANGELOG.md)
|
|
98
98
|
|
|
99
99
|
<!-- /public-truth -->
|
|
100
100
|
|
|
@@ -104,6 +104,11 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
|
|
|
104
104
|
Web/Desktop dashboard now share a per-folder lexical retrieval proof. Local
|
|
105
105
|
semantic readiness remains independent, and installed MCP targets can run an
|
|
106
106
|
explicit read-only retrieval smoke from Connectors.
|
|
107
|
+
- **One resident gateway**: `gno serve` and `gno daemon` now host stateful
|
|
108
|
+
Streamable HTTP MCP at `/mcp` from the same long-lived runtime as their
|
|
109
|
+
watcher, jobs, stores, and models. The packed npm smoke proves two-client
|
|
110
|
+
parity, warm reuse, redacted lifecycle status, fail-closed security, restart,
|
|
111
|
+
and shutdown.
|
|
107
112
|
- **Second-brain capture**: `gno capture`, REST `/api/capture`, SDK
|
|
108
113
|
`client.capture()`, MCP `gno_capture`, and Web UI Quick Capture write
|
|
109
114
|
provenance-rich notes from text, stdin, or files, including typed presets for
|
|
@@ -308,7 +313,7 @@ Use `gno daemon` when you want continuous indexing without the browser or
|
|
|
308
313
|
desktop shell open.
|
|
309
314
|
|
|
310
315
|
```bash
|
|
311
|
-
gno daemon # foreground
|
|
316
|
+
gno daemon # foreground + /mcp on 127.0.0.1:3000
|
|
312
317
|
gno daemon --no-sync-on-start
|
|
313
318
|
gno daemon --detach # background (macOS/Linux); auto-writes pid + log files
|
|
314
319
|
gno daemon --status # check the detached process
|
|
@@ -571,6 +576,13 @@ adds 11 opt-in mutation tools, for 30 total.
|
|
|
571
576
|
inspection. Your AI assistant synthesizes answers from retrieved context. Write
|
|
572
577
|
tools are available only through the explicit `--enable-write` opt-in.
|
|
573
578
|
|
|
579
|
+
`gno serve` and `gno daemon` also expose this surface as stateful Streamable
|
|
580
|
+
HTTP at `http://127.0.0.1:3000/mcp`. HTTP stays read-only by default.
|
|
581
|
+
Authenticated non-loopback access is available through the headless daemon and
|
|
582
|
+
requires an explicit restrictive bearer-token file plus exact Host and Origin
|
|
583
|
+
allowlists; `gno serve` remains loopback-only. Authentication alone never
|
|
584
|
+
enables mutation tools.
|
|
585
|
+
|
|
574
586
|
[MCP setup guide →](https://gno.sh/docs/MCP/)
|
|
575
587
|
|
|
576
588
|
---
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -72,7 +72,7 @@ Recipe rules:
|
|
|
72
72
|
| **Tags** | `tags`, `tags add`, `tags rm` | Organize and filter documents |
|
|
73
73
|
| **Context** | `context add/list/rm/check/build/verify` | Configure guidance or compile/verify evidence Capsules |
|
|
74
74
|
| **Models** | `models list/use/pull/clear/path` | Manage local AI models |
|
|
75
|
-
| **Serve** | `serve`
|
|
75
|
+
| **Serve** | `serve`, `daemon` | One resident Web/headless gateway and watcher |
|
|
76
76
|
| **Publish** | `publish export` | Export gno.sh publish artifacts |
|
|
77
77
|
| **MCP** | `mcp`, `mcp install/uninstall/status` | AI assistant integration |
|
|
78
78
|
| **Skill** | `skill install/uninstall/show/paths` | Install skill for AI agents |
|
|
@@ -155,12 +155,28 @@ gno multi-get gno://work/doc1.md gno://work/doc2.md
|
|
|
155
155
|
# Search, get full content of top result
|
|
156
156
|
gno query "auth" --json | jq -r '.results[0].uri' | xargs gno get
|
|
157
157
|
|
|
158
|
+
# Exclude documents containing a term
|
|
159
|
+
gno search "deployment" --exclude staging
|
|
160
|
+
|
|
158
161
|
# Get all results
|
|
159
162
|
gno search "error handling" --json | jq -r '.results[].uri' | xargs gno multi-get
|
|
160
163
|
```
|
|
161
164
|
|
|
165
|
+
When the user wants a synthesized answer instead of ranked evidence:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gno ask "What changed in the deployment process?" --answer
|
|
169
|
+
```
|
|
170
|
+
|
|
162
171
|
## MCP Retrieval Strategy
|
|
163
172
|
|
|
173
|
+
For a long-lived client that supports Streamable HTTP, start one resident owner
|
|
174
|
+
with `gno serve` or `gno daemon` and connect to
|
|
175
|
+
`http://127.0.0.1:3000/mcp`. Existing installed stdio entries remain valid.
|
|
176
|
+
Serve is always loopback-only. Only daemon accepts an explicit non-loopback bind,
|
|
177
|
+
and only with a restrictive bearer-token file plus exact Host/Origin allowlists.
|
|
178
|
+
Authentication never enables writes by itself.
|
|
179
|
+
|
|
164
180
|
When using GNO through MCP, prefer this retrieval order:
|
|
165
181
|
|
|
166
182
|
1. Check `gno_status` first when freshness, missing vectors, or stale results are plausible.
|
|
@@ -72,6 +72,27 @@ Do not shorten a generated entry to `gno mcp`.
|
|
|
72
72
|
gno mcp status
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Resident Streamable HTTP
|
|
76
|
+
|
|
77
|
+
`gno serve` and `gno daemon` expose the same read-only-by-default MCP surface at
|
|
78
|
+
`http://127.0.0.1:3000/mcp`. Use this URL for clients that support Streamable
|
|
79
|
+
HTTP and benefit from shared warm stores, jobs, watchers, and model leases.
|
|
80
|
+
Existing `gno mcp install` stdio entries remain valid.
|
|
81
|
+
|
|
82
|
+
Only `gno daemon` supports an explicit non-loopback bind. It requires a
|
|
83
|
+
restrictive bearer-token file plus exact Host and Origin allowlists.
|
|
84
|
+
Authentication does not grant writes; `gateway.enableWrite` or
|
|
85
|
+
`--mcp-enable-write` is a separate opt-in. `gno serve` always remains
|
|
86
|
+
loopback-only.
|
|
87
|
+
|
|
88
|
+
Inspect the safe lifecycle without exposing paths or secrets:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
curl http://127.0.0.1:3000/api/resident/status
|
|
92
|
+
gno serve --status --json
|
|
93
|
+
gno daemon --status --json
|
|
94
|
+
```
|
|
95
|
+
|
|
75
96
|
## Retrieval Order
|
|
76
97
|
|
|
77
98
|
For normal questions, start with `gno_query`, then read targeted snippets with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"embeddings",
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"dependencies": {
|
|
144
144
|
"@codemirror/lang-markdown": "6.5.0",
|
|
145
145
|
"@codemirror/theme-one-dark": "6.1.3",
|
|
146
|
-
"@modelcontextprotocol/sdk": "1.
|
|
146
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
147
147
|
"@radix-ui/react-collapsible": "1.1.12",
|
|
148
148
|
"@radix-ui/react-dialog": "1.1.15",
|
|
149
149
|
"@radix-ui/react-dropdown-menu": "2.1.16",
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { CollectionSyncResult } from "../../ingestion";
|
|
2
|
+
import type { HttpGatewayOverrides } from "../../mcp/http-security";
|
|
2
3
|
import type { BackgroundRuntimeResult } from "../../serve/background-runtime";
|
|
4
|
+
import type { ResidentRuntime } from "../../serve/resident-runtime";
|
|
3
5
|
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_HTTP_GATEWAY_PORT,
|
|
8
|
+
isHttpGatewayLoopbackBind,
|
|
9
|
+
resolveHttpGatewayConfig,
|
|
10
|
+
} from "../../mcp/http-security";
|
|
4
11
|
import { startBackgroundRuntime } from "../../serve/background-runtime";
|
|
12
|
+
import { handleResidentStatus, handleStatus } from "../../serve/routes/api";
|
|
13
|
+
import { createMcpHttpGateway } from "../../serve/routes/mcp";
|
|
5
14
|
|
|
6
|
-
export interface DaemonOptions {
|
|
15
|
+
export interface DaemonOptions extends HttpGatewayOverrides {
|
|
7
16
|
configPath?: string;
|
|
8
17
|
index?: string;
|
|
9
18
|
offline?: boolean;
|
|
@@ -24,6 +33,8 @@ type DaemonLogger = {
|
|
|
24
33
|
|
|
25
34
|
type DaemonDeps = {
|
|
26
35
|
startBackgroundRuntime?: typeof startBackgroundRuntime;
|
|
36
|
+
createMcpHttpGateway?: typeof createMcpHttpGateway;
|
|
37
|
+
serve?: typeof Bun.serve;
|
|
27
38
|
logger?: DaemonLogger;
|
|
28
39
|
};
|
|
29
40
|
|
|
@@ -31,6 +42,18 @@ function formatCollectionSyncSummary(result: CollectionSyncResult): string {
|
|
|
31
42
|
return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
export function handleDaemonAppStatus(
|
|
46
|
+
runtime: ResidentRuntime,
|
|
47
|
+
host: string
|
|
48
|
+
): Promise<Response> | Response {
|
|
49
|
+
if (!isHttpGatewayLoopbackBind(host)) {
|
|
50
|
+
return new Response(null, { status: 404 });
|
|
51
|
+
}
|
|
52
|
+
return handleStatus(runtime.ctxHolder.current, {
|
|
53
|
+
getResidentStatus: () => runtime.getStatus(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
function createSignalPromise(
|
|
35
58
|
signal: AbortSignal | undefined,
|
|
36
59
|
logger: DaemonLogger,
|
|
@@ -79,6 +102,7 @@ export async function daemon(
|
|
|
79
102
|
const runtimeResult: BackgroundRuntimeResult = await (
|
|
80
103
|
deps.startBackgroundRuntime ?? startBackgroundRuntime
|
|
81
104
|
)({
|
|
105
|
+
mode: "daemon",
|
|
82
106
|
configPath: options.configPath,
|
|
83
107
|
index: options.index,
|
|
84
108
|
requireCollections: true,
|
|
@@ -108,11 +132,52 @@ export async function daemon(
|
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
const { runtime } = runtimeResult;
|
|
135
|
+
const gatewayConfig = resolveHttpGatewayConfig(runtime.config.gateway, {
|
|
136
|
+
host: options.host,
|
|
137
|
+
port: options.port ?? DEFAULT_HTTP_GATEWAY_PORT,
|
|
138
|
+
tokenFile: options.tokenFile,
|
|
139
|
+
allowedHosts: options.allowedHosts,
|
|
140
|
+
allowedOrigins: options.allowedOrigins,
|
|
141
|
+
enableWrite: options.enableWrite,
|
|
142
|
+
});
|
|
143
|
+
let gateway: Awaited<ReturnType<typeof createMcpHttpGateway>> | undefined;
|
|
144
|
+
let server: ReturnType<typeof Bun.serve> | undefined;
|
|
111
145
|
try {
|
|
146
|
+
gateway = await (deps.createMcpHttpGateway ?? createMcpHttpGateway)(
|
|
147
|
+
runtime as ResidentRuntime,
|
|
148
|
+
gatewayConfig
|
|
149
|
+
);
|
|
150
|
+
server = (deps.serve ?? Bun.serve)({
|
|
151
|
+
port: gatewayConfig.port,
|
|
152
|
+
hostname: gatewayConfig.host,
|
|
153
|
+
development: false,
|
|
154
|
+
routes: {
|
|
155
|
+
"/mcp": gateway.route,
|
|
156
|
+
"/api/status": {
|
|
157
|
+
GET: () =>
|
|
158
|
+
handleDaemonAppStatus(
|
|
159
|
+
runtime as ResidentRuntime,
|
|
160
|
+
gatewayConfig.host
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
"/api/resident/status": {
|
|
164
|
+
GET: () =>
|
|
165
|
+
handleResidentStatus(() =>
|
|
166
|
+
(runtime as ResidentRuntime).getStatus()
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
(runtime as Partial<ResidentRuntime>).setListenerPort?.(
|
|
172
|
+
server.port ?? gatewayConfig.port
|
|
173
|
+
);
|
|
112
174
|
if (!options.quiet) {
|
|
113
175
|
logger.log(
|
|
114
176
|
`GNO daemon started for index "${options.index ?? "default"}" using ${runtime.config.collections.length} collection${runtime.config.collections.length === 1 ? "" : "s"}.`
|
|
115
177
|
);
|
|
178
|
+
logger.log(
|
|
179
|
+
`MCP gateway listening at http://${gatewayConfig.host}:${server.port}/mcp`
|
|
180
|
+
);
|
|
116
181
|
const watchState = runtime.watchService.getState();
|
|
117
182
|
if (watchState.activeCollections.length > 0) {
|
|
118
183
|
logger.log(`watching: ${watchState.activeCollections.join(", ")}`);
|
|
@@ -154,6 +219,8 @@ export async function daemon(
|
|
|
154
219
|
error: error instanceof Error ? error.message : String(error),
|
|
155
220
|
};
|
|
156
221
|
} finally {
|
|
157
|
-
await
|
|
222
|
+
await Promise.allSettled([server?.stop(true)]);
|
|
223
|
+
await Promise.allSettled([gateway?.close()]);
|
|
224
|
+
await Promise.allSettled([runtime.dispose()]);
|
|
158
225
|
}
|
|
159
226
|
}
|
|
@@ -35,6 +35,8 @@ export interface ModelsPullOptions {
|
|
|
35
35
|
force?: boolean;
|
|
36
36
|
/** Progress callback for UI (omit to disable progress) */
|
|
37
37
|
onProgress?: (type: ModelType, progress: DownloadProgress) => void;
|
|
38
|
+
/** Stop before the next model and suppress subsequent work. */
|
|
39
|
+
signal?: AbortSignal;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface ModelPullResult {
|
|
@@ -52,6 +54,10 @@ export interface ModelsPullResult {
|
|
|
52
54
|
skipped: number;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
export interface ModelsPullDependencies {
|
|
58
|
+
cache?: Pick<ModelCache, "download" | "getCachedPath" | "isCached">;
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
62
|
// Implementation
|
|
57
63
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -87,7 +93,8 @@ function getTypesToPull(options: ModelsPullOptions): ModelType[] {
|
|
|
87
93
|
* Execute gno models pull command.
|
|
88
94
|
*/
|
|
89
95
|
export async function modelsPull(
|
|
90
|
-
options: ModelsPullOptions = {}
|
|
96
|
+
options: ModelsPullOptions = {},
|
|
97
|
+
deps: ModelsPullDependencies = {}
|
|
91
98
|
): Promise<ModelsPullResult> {
|
|
92
99
|
// Use provided config, or load from disk (use defaults if not initialized)
|
|
93
100
|
let config = options.config;
|
|
@@ -98,7 +105,7 @@ export async function modelsPull(
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
const preset = getActivePreset(config);
|
|
101
|
-
const cache = new ModelCache(getModelsCachePath());
|
|
108
|
+
const cache = deps.cache ?? new ModelCache(getModelsCachePath());
|
|
102
109
|
const types = getTypesToPull(options);
|
|
103
110
|
|
|
104
111
|
const results: ModelPullResult[] = [];
|
|
@@ -106,6 +113,7 @@ export async function modelsPull(
|
|
|
106
113
|
let skipped = 0;
|
|
107
114
|
|
|
108
115
|
for (const type of types) {
|
|
116
|
+
if (options.signal?.aborted) break;
|
|
109
117
|
const uri =
|
|
110
118
|
type === "expand" ? (preset.expand ?? preset.gen) : preset[type];
|
|
111
119
|
|
|
@@ -133,8 +141,10 @@ export async function modelsPull(
|
|
|
133
141
|
(progress) => {
|
|
134
142
|
options.onProgress?.(type, progress);
|
|
135
143
|
},
|
|
136
|
-
options.force
|
|
144
|
+
options.force,
|
|
145
|
+
options.signal
|
|
137
146
|
);
|
|
147
|
+
if (options.signal?.aborted) break;
|
|
138
148
|
|
|
139
149
|
if (result.ok) {
|
|
140
150
|
results.push({
|
|
@@ -15,6 +15,7 @@ import { buildActivationStatus } from "../../core/activation-status";
|
|
|
15
15
|
import { ModelCache } from "../../llm/cache";
|
|
16
16
|
import { getActivePreset, resolveModelUri } from "../../llm/registry";
|
|
17
17
|
import { getConnectorVerificationTargets } from "../../serve/connectors";
|
|
18
|
+
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
18
19
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -277,6 +278,7 @@ export function formatStatus(
|
|
|
277
278
|
const s = result.status;
|
|
278
279
|
return JSON.stringify(
|
|
279
280
|
{
|
|
281
|
+
resident: createStandaloneResidentStatus("direct-cli"),
|
|
280
282
|
indexName: s.indexName,
|
|
281
283
|
configPath: s.configPath,
|
|
282
284
|
dbPath: s.dbPath,
|
package/src/cli/detach.ts
CHANGED
|
@@ -20,9 +20,12 @@ import { mkdir, stat, unlink } from "node:fs/promises";
|
|
|
20
20
|
// node:path — no Bun path utils.
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
|
|
23
|
+
import type { ResidentStatus } from "../serve/status-model";
|
|
24
|
+
|
|
23
25
|
import { VERSION, resolveDirs } from "../app/constants";
|
|
24
26
|
import { toAbsolutePath } from "../config/paths";
|
|
25
27
|
import { atomicWrite } from "../core/file-ops";
|
|
28
|
+
import { isResidentStatus } from "../serve/resident-status";
|
|
26
29
|
import { CliError } from "./errors";
|
|
27
30
|
|
|
28
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -63,6 +66,7 @@ export interface ProcessStatus {
|
|
|
63
66
|
version: string | null;
|
|
64
67
|
started_at: string | null;
|
|
65
68
|
uptime_seconds: number | null;
|
|
69
|
+
resident: ResidentStatus | null;
|
|
66
70
|
pid_file: string;
|
|
67
71
|
log_file: string;
|
|
68
72
|
log_size_bytes: number | null;
|
|
@@ -424,7 +428,7 @@ export interface SpawnDetachedOptions {
|
|
|
424
428
|
* self-contained executable like a single `.mjs` file).
|
|
425
429
|
*/
|
|
426
430
|
entryScript?: string | null;
|
|
427
|
-
/** Optional port to embed in the pid-file payload
|
|
431
|
+
/** Optional resident HTTP port to embed in the pid-file payload. */
|
|
428
432
|
port?: number | null;
|
|
429
433
|
/** Working directory for the child. Defaults to `process.cwd()`. */
|
|
430
434
|
cwd?: string;
|
|
@@ -736,6 +740,23 @@ export interface StatusOptions {
|
|
|
736
740
|
logFile: string;
|
|
737
741
|
/** Clock override for deterministic tests. Defaults to `Date.now`. */
|
|
738
742
|
now?: () => number;
|
|
743
|
+
fetchResidentStatus?: (port: number) => Promise<ResidentStatus | null>;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function fetchResidentStatus(
|
|
747
|
+
port: number
|
|
748
|
+
): Promise<ResidentStatus | null> {
|
|
749
|
+
try {
|
|
750
|
+
const response = await fetch(
|
|
751
|
+
`http://127.0.0.1:${port}/api/resident/status`,
|
|
752
|
+
{ signal: AbortSignal.timeout(500) }
|
|
753
|
+
);
|
|
754
|
+
if (!response.ok) return null;
|
|
755
|
+
const body: unknown = await response.json();
|
|
756
|
+
return isResidentStatus(body) ? body : null;
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
739
760
|
}
|
|
740
761
|
|
|
741
762
|
/**
|
|
@@ -765,6 +786,7 @@ export async function statusProcess(
|
|
|
765
786
|
version: null,
|
|
766
787
|
started_at: null,
|
|
767
788
|
uptime_seconds: null,
|
|
789
|
+
resident: null,
|
|
768
790
|
pid_file: options.pidFile,
|
|
769
791
|
log_file: options.logFile,
|
|
770
792
|
log_size_bytes: logSize,
|
|
@@ -789,33 +811,28 @@ export async function statusProcess(
|
|
|
789
811
|
? Math.max(0, Math.floor((now() - Date.parse(payload.started_at)) / 1000))
|
|
790
812
|
: null;
|
|
791
813
|
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
running && !(effectiveKind === "serve" && portForRunningServe === null);
|
|
814
|
+
// Both resident modes expose an HTTP listener. If a live pid-file is
|
|
815
|
+
// missing its port, fall back to not-running rather than claiming a gateway
|
|
816
|
+
// that clients cannot locate.
|
|
817
|
+
const portForRunningProcess =
|
|
818
|
+
running && typeof payload.port === "number" ? payload.port : null;
|
|
819
|
+
const runningFinal = running && portForRunningProcess !== null;
|
|
820
|
+
const resident =
|
|
821
|
+
runningFinal && portForRunningProcess !== null
|
|
822
|
+
? await (options.fetchResidentStatus ?? fetchResidentStatus)(
|
|
823
|
+
portForRunningProcess
|
|
824
|
+
)
|
|
825
|
+
: null;
|
|
805
826
|
|
|
806
827
|
return {
|
|
807
828
|
running: runningFinal,
|
|
808
829
|
pid: payload.pid,
|
|
809
|
-
port:
|
|
810
|
-
runningFinal && effectiveKind === "serve"
|
|
811
|
-
? portForRunningServe
|
|
812
|
-
: effectiveKind === "daemon"
|
|
813
|
-
? null
|
|
814
|
-
: (payload.port ?? null),
|
|
830
|
+
port: runningFinal ? portForRunningProcess : (payload.port ?? null),
|
|
815
831
|
cmd: effectiveKind,
|
|
816
832
|
version: payload.version,
|
|
817
833
|
started_at: payload.started_at,
|
|
818
834
|
uptime_seconds: runningFinal ? uptimeSeconds : null,
|
|
835
|
+
resident,
|
|
819
836
|
pid_file: options.pidFile,
|
|
820
837
|
log_file: options.logFile,
|
|
821
838
|
log_size_bytes: logSize,
|
package/src/cli/program.ts
CHANGED
|
@@ -2806,20 +2806,46 @@ function wireGraphCommand(program: Command): void {
|
|
|
2806
2806
|
// Serve Command (web UI)
|
|
2807
2807
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2808
2808
|
|
|
2809
|
-
function
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2809
|
+
function addGatewayOptions(
|
|
2810
|
+
command: Command,
|
|
2811
|
+
hostDescription = "HTTP listen address (default: 127.0.0.1)"
|
|
2812
|
+
): Command {
|
|
2813
|
+
return command
|
|
2814
|
+
.option("--host <address>", hostDescription)
|
|
2815
|
+
.option("--mcp-token-file <path>", "restrictive bearer token file")
|
|
2816
|
+
.option(
|
|
2817
|
+
"--mcp-allowed-host <host>",
|
|
2818
|
+
"exact allowed Host header (repeatable)",
|
|
2819
|
+
(value: string, previous: string[] = []) => [...previous, value]
|
|
2820
|
+
)
|
|
2813
2821
|
.option(
|
|
2814
|
-
"--
|
|
2815
|
-
"
|
|
2822
|
+
"--mcp-allowed-origin <origin>",
|
|
2823
|
+
"exact allowed Origin (repeatable)",
|
|
2824
|
+
(value: string, previous: string[] = []) => [...previous, value]
|
|
2816
2825
|
)
|
|
2817
|
-
// Local `--json` so `gno daemon --status --json` parses cleanly in any
|
|
2818
|
-
// argv position. Mirrors the serve wiring: gated to --status only.
|
|
2819
2826
|
.option(
|
|
2820
|
-
"--
|
|
2821
|
-
"
|
|
2827
|
+
"--mcp-enable-write",
|
|
2828
|
+
"authorize HTTP MCP mutation tools (independent of authentication)"
|
|
2822
2829
|
);
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function wireDaemonCommand(program: Command): void {
|
|
2833
|
+
const daemonCmd = addGatewayOptions(
|
|
2834
|
+
program
|
|
2835
|
+
.command("daemon")
|
|
2836
|
+
.description("Start headless continuous indexing")
|
|
2837
|
+
.option("-p, --port <num>", "MCP gateway port", "3000")
|
|
2838
|
+
.option(
|
|
2839
|
+
"--no-sync-on-start",
|
|
2840
|
+
"skip initial sync and only watch future file changes"
|
|
2841
|
+
)
|
|
2842
|
+
// Local `--json` so `gno daemon --status --json` parses cleanly in any
|
|
2843
|
+
// argv position. Mirrors the serve wiring: gated to --status only.
|
|
2844
|
+
.option(
|
|
2845
|
+
"--json",
|
|
2846
|
+
"JSON output (applies to --status; see process-status schema)"
|
|
2847
|
+
)
|
|
2848
|
+
);
|
|
2823
2849
|
|
|
2824
2850
|
// --detach / --status / --stop are mutually exclusive. Use Commander's
|
|
2825
2851
|
// Option API so the conflict error is the native "option '--status'
|
|
@@ -2873,8 +2899,8 @@ function wireDaemonCommand(program: Command): void {
|
|
|
2873
2899
|
* the foreground + detached-child paths boot the runtime.
|
|
2874
2900
|
*
|
|
2875
2901
|
* Mirrors `handleServeAction` exactly — same helper imports, same JSON
|
|
2876
|
-
* gating, same detached-child verification + pid-file cleanup. Daemon
|
|
2877
|
-
*
|
|
2902
|
+
* gating, same detached-child verification + pid-file cleanup. Daemon's port
|
|
2903
|
+
* identifies its headless MCP gateway.
|
|
2878
2904
|
*/
|
|
2879
2905
|
async function handleDaemonAction(
|
|
2880
2906
|
cmdOpts: Record<string, unknown>,
|
|
@@ -2927,7 +2953,9 @@ async function handleDaemonAction(
|
|
|
2927
2953
|
}
|
|
2928
2954
|
|
|
2929
2955
|
if (cmdOpts.detach) {
|
|
2956
|
+
const port = parsePositiveInt("port", cmdOpts.port);
|
|
2930
2957
|
await runDaemonDetach({
|
|
2958
|
+
port,
|
|
2931
2959
|
paths,
|
|
2932
2960
|
spawnDetached,
|
|
2933
2961
|
argv: resolveCliArgv(cmd),
|
|
@@ -2954,6 +2982,8 @@ async function handleDaemonAction(
|
|
|
2954
2982
|
installPidFileCleanup(paths.pidFile);
|
|
2955
2983
|
}
|
|
2956
2984
|
|
|
2985
|
+
const port = parsePositiveInt("port", cmdOpts.port);
|
|
2986
|
+
|
|
2957
2987
|
const { daemon } = await import("./commands/daemon.js");
|
|
2958
2988
|
const result = await daemon({
|
|
2959
2989
|
configPath: globals.config,
|
|
@@ -2962,6 +2992,12 @@ async function handleDaemonAction(
|
|
|
2962
2992
|
verbose: globals.verbose,
|
|
2963
2993
|
quiet: globals.quiet,
|
|
2964
2994
|
noSyncOnStart: cmdOpts.syncOnStart === false,
|
|
2995
|
+
port,
|
|
2996
|
+
host: cmdOpts.host as string | undefined,
|
|
2997
|
+
tokenFile: cmdOpts.mcpTokenFile as string | undefined,
|
|
2998
|
+
allowedHosts: cmdOpts.mcpAllowedHost as string[] | undefined,
|
|
2999
|
+
allowedOrigins: cmdOpts.mcpAllowedOrigin as string[] | undefined,
|
|
3000
|
+
enableWrite: cmdOpts.mcpEnableWrite === true ? true : undefined,
|
|
2965
3001
|
});
|
|
2966
3002
|
if (!result.success) {
|
|
2967
3003
|
throw new CliError("RUNTIME", result.error);
|
|
@@ -2995,8 +3031,9 @@ async function runDaemonStatus(deps: DaemonStatusDeps): Promise<void> {
|
|
|
2995
3031
|
process.stdout.write("gno daemon status\n");
|
|
2996
3032
|
process.stdout.write(`${"─".repeat(50)}\n`);
|
|
2997
3033
|
if (status.running) {
|
|
2998
|
-
|
|
2999
|
-
|
|
3034
|
+
process.stdout.write(
|
|
3035
|
+
` running yes (pid ${status.pid}, port ${status.port})\n`
|
|
3036
|
+
);
|
|
3000
3037
|
if (status.version) {
|
|
3001
3038
|
process.stdout.write(` version ${status.version}\n`);
|
|
3002
3039
|
}
|
|
@@ -3091,6 +3128,7 @@ async function runDaemonStop(deps: DaemonStopDeps): Promise<void> {
|
|
|
3091
3128
|
}
|
|
3092
3129
|
|
|
3093
3130
|
interface DaemonDetachDeps {
|
|
3131
|
+
port: number;
|
|
3094
3132
|
paths: { pidFile: string; logFile: string };
|
|
3095
3133
|
spawnDetached: typeof import("./detach.js").spawnDetached;
|
|
3096
3134
|
/**
|
|
@@ -3111,24 +3149,28 @@ async function runDaemonDetach(deps: DaemonDetachDeps): Promise<void> {
|
|
|
3111
3149
|
argv: childArgv,
|
|
3112
3150
|
pidFile: deps.paths.pidFile,
|
|
3113
3151
|
logFile: deps.paths.logFile,
|
|
3152
|
+
port: deps.port,
|
|
3114
3153
|
});
|
|
3115
|
-
// Daemon
|
|
3154
|
+
// Daemon has no Web UI URL; its listener exists solely for MCP.
|
|
3116
3155
|
process.stdout.write(`PID ${result.pid}\n`);
|
|
3117
3156
|
}
|
|
3118
3157
|
|
|
3119
3158
|
function wireServeCommand(program: Command): void {
|
|
3120
|
-
const serveCmd =
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3159
|
+
const serveCmd = addGatewayOptions(
|
|
3160
|
+
program
|
|
3161
|
+
.command("serve")
|
|
3162
|
+
.description("Start web UI server")
|
|
3163
|
+
.option("-p, --port <num>", "port to listen on", "3000")
|
|
3164
|
+
// Local `--json` so `gno serve --status --json` parses cleanly in any
|
|
3165
|
+
// argv position. Global `--json` already exists on the root program
|
|
3166
|
+
// (see line 208); resolution goes through `getFormat()` /
|
|
3167
|
+
// `getGlobals()` which precedence-merges local over global.
|
|
3168
|
+
.option(
|
|
3169
|
+
"--json",
|
|
3170
|
+
"JSON output (applies to --status; see process-status schema)"
|
|
3171
|
+
),
|
|
3172
|
+
"loopback HTTP listen address (default: 127.0.0.1)"
|
|
3173
|
+
);
|
|
3132
3174
|
|
|
3133
3175
|
// --detach / --status / --stop are mutually exclusive. Use Commander's
|
|
3134
3176
|
// Option API so the conflict error is the native "option '--status'
|
|
@@ -3275,6 +3317,11 @@ async function handleServeAction(
|
|
|
3275
3317
|
port,
|
|
3276
3318
|
configPath: globals.config,
|
|
3277
3319
|
index: globals.index,
|
|
3320
|
+
host: cmdOpts.host as string | undefined,
|
|
3321
|
+
tokenFile: cmdOpts.mcpTokenFile as string | undefined,
|
|
3322
|
+
allowedHosts: cmdOpts.mcpAllowedHost as string[] | undefined,
|
|
3323
|
+
allowedOrigins: cmdOpts.mcpAllowedOrigin as string[] | undefined,
|
|
3324
|
+
enableWrite: cmdOpts.mcpEnableWrite === true ? true : undefined,
|
|
3278
3325
|
});
|
|
3279
3326
|
|
|
3280
3327
|
if (!result.success) {
|
package/src/config/index.ts
CHANGED
|
@@ -50,6 +50,8 @@ export {
|
|
|
50
50
|
CollectionSchema,
|
|
51
51
|
type Config,
|
|
52
52
|
ConfigSchema,
|
|
53
|
+
HttpGatewayConfigSchema,
|
|
54
|
+
HttpGatewayLimitsSchema,
|
|
53
55
|
CONTENT_TYPE_GRAPH_HINTS,
|
|
54
56
|
type ContentTypeConfig,
|
|
55
57
|
type ContentTypeGraphHint,
|
|
@@ -67,3 +69,4 @@ export {
|
|
|
67
69
|
type ScopeType,
|
|
68
70
|
ScopeTypeSchema,
|
|
69
71
|
} from "./types";
|
|
72
|
+
export type { HttpGatewayConfig } from "./types";
|
package/src/config/types.ts
CHANGED
|
@@ -245,6 +245,40 @@ export const ModelConfigSchema = z.object({
|
|
|
245
245
|
|
|
246
246
|
export type ModelConfig = z.infer<typeof ModelConfigSchema>;
|
|
247
247
|
|
|
248
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// Resident HTTP gateway schema
|
|
250
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
export const HttpGatewayLimitsSchema = z.object({
|
|
253
|
+
maxBodyBytes: z
|
|
254
|
+
.number()
|
|
255
|
+
.int()
|
|
256
|
+
.min(1)
|
|
257
|
+
.max(16 * 1024 * 1024)
|
|
258
|
+
.optional(),
|
|
259
|
+
maxRequestsPerMinute: z.number().int().min(1).max(100_000).optional(),
|
|
260
|
+
maxConcurrentRequests: z.number().int().min(1).max(10_000).optional(),
|
|
261
|
+
maxQueuedRequests: z.number().int().min(0).max(10_000).optional(),
|
|
262
|
+
maxSessions: z.number().int().min(1).max(10_000).optional(),
|
|
263
|
+
sessionIdleTimeoutMs: z.number().int().min(1_000).optional(),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export const HttpGatewayConfigSchema = z.object({
|
|
267
|
+
/** Literal listen address. Defaults to IPv4 loopback. */
|
|
268
|
+
host: z.string().min(1).optional(),
|
|
269
|
+
/** Bearer token file. Required for wildcard/non-loopback binding. */
|
|
270
|
+
tokenFile: z.string().min(1).optional(),
|
|
271
|
+
/** Exact Host header allowlist. */
|
|
272
|
+
allowedHosts: z.array(z.string().min(1)).optional(),
|
|
273
|
+
/** Exact Origin allowlist. Origin-less non-browser clients remain supported. */
|
|
274
|
+
allowedOrigins: z.array(z.string().min(1)).optional(),
|
|
275
|
+
/** Separate mutation authorization; authentication alone never enables it. */
|
|
276
|
+
enableWrite: z.boolean().optional(),
|
|
277
|
+
limits: HttpGatewayLimitsSchema.optional(),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
export type HttpGatewayConfig = z.infer<typeof HttpGatewayConfigSchema>;
|
|
281
|
+
|
|
248
282
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
283
|
// Content Type Schema
|
|
250
284
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -301,6 +335,9 @@ export const ConfigSchema = z.object({
|
|
|
301
335
|
|
|
302
336
|
/** Model configuration */
|
|
303
337
|
models: ModelConfigSchema.optional(),
|
|
338
|
+
|
|
339
|
+
/** Resident Streamable HTTP MCP gateway configuration */
|
|
340
|
+
gateway: HttpGatewayConfigSchema.optional(),
|
|
304
341
|
});
|
|
305
342
|
|
|
306
343
|
export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
|