@gmickel/gno 1.16.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 +34 -18
- package/assets/skill/SKILL.md +25 -6
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +2 -2
- package/src/app/context-agent-projection.ts +303 -0
- package/src/app/context-format.ts +249 -0
- package/src/app/context-runtime-contract.ts +325 -0
- package/src/app/context-runtime-input.ts +362 -0
- package/src/app/context-runtime-types.ts +65 -0
- package/src/app/context-runtime.ts +170 -0
- package/src/app/context-surface.ts +145 -0
- package/src/cli/commands/context-build.ts +149 -0
- package/src/cli/commands/context-verify.ts +90 -0
- 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/options.ts +4 -0
- package/src/cli/program.ts +252 -27
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +37 -0
- package/src/core/context-budget.ts +461 -0
- package/src/core/context-capsule-index-schema.ts +15 -0
- package/src/core/context-capsule-retrieval-schema.ts +81 -0
- package/src/core/context-capsule-schema.ts +473 -0
- package/src/core/context-capsule-validation.ts +416 -0
- package/src/core/context-capsule-verification.ts +218 -0
- package/src/core/context-capsule.ts +439 -0
- package/src/core/context-compiler.ts +513 -0
- package/src/core/context-evidence-metadata.ts +33 -0
- package/src/core/context-evidence.ts +495 -0
- package/src/core/context-facets.ts +163 -0
- package/src/core/context-guidance.ts +69 -0
- package/src/core/context-scope.ts +32 -0
- package/src/core/context-verifier-canonical.ts +90 -0
- package/src/core/context-verifier-input.ts +66 -0
- package/src/core/context-verifier.ts +447 -0
- package/src/core/job-manager.ts +19 -0
- package/src/core/mutation-generations.ts +33 -0
- package/src/core/sections.ts +63 -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 +28 -82
- 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 +230 -0
- package/src/mcp/tools/embed.ts +62 -52
- package/src/mcp/tools/index-cmd.ts +88 -74
- package/src/mcp/tools/index.ts +49 -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/pipeline/chunk-lookup.ts +33 -0
- package/src/pipeline/hybrid.ts +79 -57
- package/src/pipeline/types.ts +14 -0
- package/src/sdk/client.ts +68 -6
- package/src/sdk/index.ts +21 -0
- package/src/sdk/types.ts +24 -0
- package/src/serve/background-runtime.ts +12 -211
- package/src/serve/context-capsule.ts +136 -0
- package/src/serve/context.ts +10 -1
- 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 +265 -167
- package/src/serve/routes/mcp.ts +69 -0
- package/src/serve/server.ts +212 -35
- package/src/serve/status-model.ts +51 -0
- package/src/serve/status.ts +5 -0
- package/src/store/sqlite/adapter.ts +64 -29
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Closed wire contracts shared by Context Capsule REST and MCP surfaces. */
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { ContextCapsuleBuildInput } from "./context-runtime-types";
|
|
6
|
+
|
|
7
|
+
import { ContextCapsuleContractError } from "../core/context-capsule";
|
|
8
|
+
|
|
9
|
+
const queryModeSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
mode: z.enum(["term", "intent", "hyde"]),
|
|
12
|
+
text: z.string(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
|
|
16
|
+
const stringList = z.array(z.string());
|
|
17
|
+
const positiveInteger = z.number().int().positive();
|
|
18
|
+
const nonnegativeInteger = z.number().int().nonnegative();
|
|
19
|
+
|
|
20
|
+
/** Host-owned index identity is deliberately absent from this public input. */
|
|
21
|
+
export const contextBuildSurfaceSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
goal: z.string(),
|
|
24
|
+
query: z.string().optional(),
|
|
25
|
+
collections: stringList.optional(),
|
|
26
|
+
uriPrefix: z.string().nullable().optional(),
|
|
27
|
+
queryModes: z.array(queryModeSchema).optional(),
|
|
28
|
+
tagsAll: stringList.optional(),
|
|
29
|
+
tagsAny: stringList.optional(),
|
|
30
|
+
categories: stringList.optional(),
|
|
31
|
+
author: z.string().optional(),
|
|
32
|
+
lang: z.string().optional(),
|
|
33
|
+
since: z.string().optional(),
|
|
34
|
+
until: z.string().optional(),
|
|
35
|
+
graph: z.boolean().optional(),
|
|
36
|
+
limit: positiveInteger.optional(),
|
|
37
|
+
candidateLimit: positiveInteger.optional(),
|
|
38
|
+
budgetTokens: positiveInteger,
|
|
39
|
+
budgetBytes: positiveInteger.optional(),
|
|
40
|
+
safetyMarginTokens: nonnegativeInteger.optional(),
|
|
41
|
+
safetyMarginBytes: nonnegativeInteger.optional(),
|
|
42
|
+
depthPolicy: z.enum(["fast", "balanced", "thorough"]).optional(),
|
|
43
|
+
format: z.enum(["json", "md"]).optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
|
|
47
|
+
export const contextVerifySurfaceSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
capsule: z.record(z.string(), z.unknown()),
|
|
50
|
+
format: z.enum(["json", "md"]).optional(),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
|
|
54
|
+
export type ContextSurfaceFormat = "json" | "md";
|
|
55
|
+
|
|
56
|
+
export interface ParsedContextBuildSurfaceInput {
|
|
57
|
+
input: ContextCapsuleBuildInput;
|
|
58
|
+
format: ContextSurfaceFormat;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ParsedContextVerifySurfaceInput {
|
|
62
|
+
capsule: Record<string, unknown>;
|
|
63
|
+
format: ContextSurfaceFormat;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const CONTEXT_SURFACE_ERROR_MESSAGES = {
|
|
67
|
+
capsule_mutated_during_verify:
|
|
68
|
+
"The Context Capsule changed during verification.",
|
|
69
|
+
chunk_coordinate_mismatch:
|
|
70
|
+
"Indexed evidence coordinates do not match the source.",
|
|
71
|
+
chunk_load_failed: "Context Capsule evidence chunks could not be loaded.",
|
|
72
|
+
collection_load_failed: "Context Capsule collections could not be loaded.",
|
|
73
|
+
content_load_failed: "Context Capsule source content could not be loaded.",
|
|
74
|
+
context_changed_during_compile:
|
|
75
|
+
"Configured context changed during compilation.",
|
|
76
|
+
context_changed_during_verify:
|
|
77
|
+
"Configured context changed during verification.",
|
|
78
|
+
context_load_failed: "Configured context could not be loaded.",
|
|
79
|
+
document_load_failed: "Context Capsule documents could not be loaded.",
|
|
80
|
+
identity_mismatch: "The Context Capsule identity does not match its content.",
|
|
81
|
+
index_changed_during_compile: "The index changed during compilation.",
|
|
82
|
+
index_changed_during_verify: "The index changed during verification.",
|
|
83
|
+
index_snapshot_failed: "The index snapshot could not be loaded.",
|
|
84
|
+
index_snapshot_mismatch:
|
|
85
|
+
"Indexed evidence does not match the captured snapshot.",
|
|
86
|
+
invalid_budget: "The Context Capsule budget is invalid.",
|
|
87
|
+
invalid_filter: "A Context Capsule filter is invalid.",
|
|
88
|
+
invalid_goal: "The Context Capsule goal is invalid.",
|
|
89
|
+
invalid_input: "The Context Capsule request is invalid.",
|
|
90
|
+
invalid_uri: "The Context Capsule URI is invalid.",
|
|
91
|
+
no_evidence: "No in-scope evidence was available for the Context Capsule.",
|
|
92
|
+
retrieval_failed: "Context Capsule retrieval failed.",
|
|
93
|
+
runtime_error: "The Context Capsule request failed.",
|
|
94
|
+
stored_provenance_mismatch:
|
|
95
|
+
"Stored evidence provenance does not match the source.",
|
|
96
|
+
tokenizer_unavailable: "The required tokenizer is unavailable.",
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
export type ContextSurfaceErrorCode =
|
|
100
|
+
keyof typeof CONTEXT_SURFACE_ERROR_MESSAGES;
|
|
101
|
+
|
|
102
|
+
const invalidInput = (error: z.ZodError): ContextCapsuleContractError =>
|
|
103
|
+
new ContextCapsuleContractError(
|
|
104
|
+
"invalid_input",
|
|
105
|
+
error.issues
|
|
106
|
+
.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`)
|
|
107
|
+
.join("; ")
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
export const parseContextBuildSurfaceInput = (
|
|
111
|
+
value: unknown,
|
|
112
|
+
indexName: string
|
|
113
|
+
): ParsedContextBuildSurfaceInput => {
|
|
114
|
+
const parsed = contextBuildSurfaceSchema.safeParse(value);
|
|
115
|
+
if (!parsed.success) throw invalidInput(parsed.error);
|
|
116
|
+
const { format = "json", ...input } = parsed.data;
|
|
117
|
+
return { input: { ...input, indexName }, format };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const parseContextVerifySurfaceInput = (
|
|
121
|
+
value: unknown
|
|
122
|
+
): ParsedContextVerifySurfaceInput => {
|
|
123
|
+
const parsed = contextVerifySurfaceSchema.safeParse(value);
|
|
124
|
+
if (!parsed.success) throw invalidInput(parsed.error);
|
|
125
|
+
return {
|
|
126
|
+
capsule: parsed.data.capsule,
|
|
127
|
+
format: parsed.data.format ?? "json",
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const contextSurfaceError = (
|
|
132
|
+
error: unknown
|
|
133
|
+
): { code: ContextSurfaceErrorCode; message: string } => {
|
|
134
|
+
const candidate =
|
|
135
|
+
error !== null &&
|
|
136
|
+
typeof error === "object" &&
|
|
137
|
+
"code" in error &&
|
|
138
|
+
typeof error.code === "string"
|
|
139
|
+
? error.code
|
|
140
|
+
: "runtime_error";
|
|
141
|
+
const code = Object.hasOwn(CONTEXT_SURFACE_ERROR_MESSAGES, candidate)
|
|
142
|
+
? (candidate as ContextSurfaceErrorCode)
|
|
143
|
+
: "runtime_error";
|
|
144
|
+
return { code, message: CONTEXT_SURFACE_ERROR_MESSAGES[code] };
|
|
145
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Context Capsule build command over the shared application runtime. */
|
|
2
|
+
|
|
3
|
+
import type { ContextCapsuleBuildInput } from "../../app/context-runtime";
|
|
4
|
+
import type { EmbeddingPort, RerankPort } from "../../llm/types";
|
|
5
|
+
import type { VectorIndexPort } from "../../store/vector";
|
|
6
|
+
|
|
7
|
+
import { formatContextCapsuleMarkdown } from "../../app/context-format";
|
|
8
|
+
import {
|
|
9
|
+
buildContextCapsule,
|
|
10
|
+
canonicalBuiltContextCapsuleJson,
|
|
11
|
+
validateContextCapsuleBuildInput,
|
|
12
|
+
} from "../../app/context-runtime";
|
|
13
|
+
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
14
|
+
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
15
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
16
|
+
import { createVectorIndexPort } from "../../store/vector";
|
|
17
|
+
import { CliError } from "../errors";
|
|
18
|
+
import { getGlobals } from "../program";
|
|
19
|
+
import {
|
|
20
|
+
createProgressRenderer,
|
|
21
|
+
createThrottledProgressRenderer,
|
|
22
|
+
} from "../progress";
|
|
23
|
+
import { initStore } from "./shared";
|
|
24
|
+
|
|
25
|
+
export interface ContextBuildCommandOptions extends Omit<
|
|
26
|
+
ContextCapsuleBuildInput,
|
|
27
|
+
"goal"
|
|
28
|
+
> {
|
|
29
|
+
configPath?: string;
|
|
30
|
+
format: "json" | "md";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const validationCodes = new Set([
|
|
34
|
+
"identity_mismatch",
|
|
35
|
+
"invalid_budget",
|
|
36
|
+
"invalid_filter",
|
|
37
|
+
"invalid_goal",
|
|
38
|
+
"invalid_input",
|
|
39
|
+
"invalid_uri",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export const contextCliError = (error: unknown): CliError => {
|
|
43
|
+
const contextCode =
|
|
44
|
+
error !== null &&
|
|
45
|
+
typeof error === "object" &&
|
|
46
|
+
"code" in error &&
|
|
47
|
+
typeof error.code === "string"
|
|
48
|
+
? error.code
|
|
49
|
+
: "runtime_error";
|
|
50
|
+
return new CliError(
|
|
51
|
+
validationCodes.has(contextCode) ? "VALIDATION" : "RUNTIME",
|
|
52
|
+
error instanceof Error ? error.message : String(error),
|
|
53
|
+
{ details: { contextCode } }
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// oxlint-disable-next-line max-lines-per-function -- owns one bounded model/store lifecycle
|
|
58
|
+
export const contextBuild = async (
|
|
59
|
+
goal: string,
|
|
60
|
+
options: ContextBuildCommandOptions
|
|
61
|
+
): Promise<string> => {
|
|
62
|
+
try {
|
|
63
|
+
validateContextCapsuleBuildInput({ goal, ...options }, options.indexName);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw contextCliError(error);
|
|
66
|
+
}
|
|
67
|
+
const initResult = await initStore({
|
|
68
|
+
configPath: options.configPath,
|
|
69
|
+
indexName: options.indexName,
|
|
70
|
+
syncConfig: true,
|
|
71
|
+
});
|
|
72
|
+
if (!initResult.ok) {
|
|
73
|
+
throw new CliError("RUNTIME", initResult.error);
|
|
74
|
+
}
|
|
75
|
+
const { config, store } = initResult;
|
|
76
|
+
const llm = new LlmAdapter(config);
|
|
77
|
+
let embedPort: EmbeddingPort | null = null;
|
|
78
|
+
let rerankPort: RerankPort | null = null;
|
|
79
|
+
let vectorIndex: VectorIndexPort | null = null;
|
|
80
|
+
try {
|
|
81
|
+
validateContextCapsuleBuildInput(
|
|
82
|
+
{ goal, ...options },
|
|
83
|
+
options.indexName,
|
|
84
|
+
config.collections.map((collection) => collection.name)
|
|
85
|
+
);
|
|
86
|
+
if (options.depthPolicy !== "fast") {
|
|
87
|
+
const globals = getGlobals();
|
|
88
|
+
const policy = resolveDownloadPolicy(process.env, {
|
|
89
|
+
offline: globals.offline,
|
|
90
|
+
});
|
|
91
|
+
const showProgress = process.stderr.isTTY && !globals.quiet;
|
|
92
|
+
const progress = showProgress
|
|
93
|
+
? createThrottledProgressRenderer(createProgressRenderer())
|
|
94
|
+
: undefined;
|
|
95
|
+
const collection =
|
|
96
|
+
options.collections?.length === 1 ? options.collections[0] : undefined;
|
|
97
|
+
const embedUri = resolveModelUri(config, "embed", undefined, collection);
|
|
98
|
+
const embedResult = await llm.createEmbeddingPort(embedUri, {
|
|
99
|
+
policy,
|
|
100
|
+
onProgress: progress ? (value) => progress("embed", value) : undefined,
|
|
101
|
+
});
|
|
102
|
+
if (embedResult.ok) {
|
|
103
|
+
embedPort = embedResult.value;
|
|
104
|
+
const initialized = await embedPort.init();
|
|
105
|
+
if (initialized.ok) {
|
|
106
|
+
const vectorResult = await createVectorIndexPort(store.getRawDb(), {
|
|
107
|
+
model: embedUri,
|
|
108
|
+
dimensions: embedPort.dimensions(),
|
|
109
|
+
});
|
|
110
|
+
if (vectorResult.ok) vectorIndex = vectorResult.value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const rerankUri = resolveModelUri(
|
|
114
|
+
config,
|
|
115
|
+
"rerank",
|
|
116
|
+
undefined,
|
|
117
|
+
collection
|
|
118
|
+
);
|
|
119
|
+
const rerankResult = await llm.createRerankPort(rerankUri, {
|
|
120
|
+
policy,
|
|
121
|
+
onProgress: progress ? (value) => progress("rerank", value) : undefined,
|
|
122
|
+
});
|
|
123
|
+
if (rerankResult.ok) rerankPort = rerankResult.value;
|
|
124
|
+
if (showProgress && progress) process.stderr.write("\n");
|
|
125
|
+
}
|
|
126
|
+
const capsule = await buildContextCapsule(
|
|
127
|
+
{ goal, ...options },
|
|
128
|
+
{
|
|
129
|
+
store,
|
|
130
|
+
config,
|
|
131
|
+
indexName: options.indexName,
|
|
132
|
+
vectorIndex,
|
|
133
|
+
embedPort,
|
|
134
|
+
rerankPort,
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
return options.format === "md"
|
|
138
|
+
? formatContextCapsuleMarkdown(capsule)
|
|
139
|
+
: canonicalBuiltContextCapsuleJson(capsule);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof CliError) throw error;
|
|
142
|
+
throw contextCliError(error);
|
|
143
|
+
} finally {
|
|
144
|
+
await embedPort?.dispose();
|
|
145
|
+
await rerankPort?.dispose();
|
|
146
|
+
await llm.dispose();
|
|
147
|
+
await store.close();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Context Capsule verification command over the shared application runtime. */
|
|
2
|
+
|
|
3
|
+
import { formatContextCapsuleVerificationMarkdown } from "../../app/context-format";
|
|
4
|
+
import {
|
|
5
|
+
canonicalVerifiedContextCapsuleJson,
|
|
6
|
+
verifyContextCapsuleRuntime,
|
|
7
|
+
} from "../../app/context-runtime";
|
|
8
|
+
import { canonicalizeIndexName } from "../../app/index-name";
|
|
9
|
+
import { parseCanonicalContextCapsuleForVerification } from "../../core/context-verifier";
|
|
10
|
+
import { CliError } from "../errors";
|
|
11
|
+
import { contextCliError } from "./context-build";
|
|
12
|
+
import { initStore } from "./shared";
|
|
13
|
+
|
|
14
|
+
export interface ContextVerifyCommandOptions {
|
|
15
|
+
configPath?: string;
|
|
16
|
+
indexName?: string;
|
|
17
|
+
format: "json" | "md";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const readCapsule = async (source: string): Promise<unknown> => {
|
|
21
|
+
let raw: string;
|
|
22
|
+
try {
|
|
23
|
+
raw =
|
|
24
|
+
source === "-" ? await Bun.stdin.text() : await Bun.file(source).text();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new CliError(
|
|
27
|
+
"RUNTIME",
|
|
28
|
+
`Failed to read Context Capsule: ${
|
|
29
|
+
error instanceof Error ? error.message : String(error)
|
|
30
|
+
}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(raw) as unknown;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new CliError("VALIDATION", "Context Capsule must be valid JSON", {
|
|
37
|
+
details: {
|
|
38
|
+
contextCode: "invalid_input",
|
|
39
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const contextVerify = async (
|
|
46
|
+
source: string,
|
|
47
|
+
options: ContextVerifyCommandOptions
|
|
48
|
+
): Promise<string> => {
|
|
49
|
+
const input = await readCapsule(source);
|
|
50
|
+
let capsule: ReturnType<typeof parseCanonicalContextCapsuleForVerification>;
|
|
51
|
+
try {
|
|
52
|
+
capsule = parseCanonicalContextCapsuleForVerification(input);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw contextCliError(error);
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
options.indexName !== undefined &&
|
|
58
|
+
canonicalizeIndexName(options.indexName) !== capsule.scope.indexName
|
|
59
|
+
) {
|
|
60
|
+
throw new CliError(
|
|
61
|
+
"VALIDATION",
|
|
62
|
+
`Context Capsule index ${capsule.scope.indexName} does not match --index ${options.indexName}`,
|
|
63
|
+
{ details: { contextCode: "invalid_filter" } }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const initResult = await initStore({
|
|
67
|
+
configPath: options.configPath,
|
|
68
|
+
indexName: capsule.scope.indexName,
|
|
69
|
+
syncConfig: true,
|
|
70
|
+
});
|
|
71
|
+
if (!initResult.ok) {
|
|
72
|
+
throw new CliError("RUNTIME", initResult.error);
|
|
73
|
+
}
|
|
74
|
+
const { config, store } = initResult;
|
|
75
|
+
try {
|
|
76
|
+
const receipt = await verifyContextCapsuleRuntime(capsule, {
|
|
77
|
+
store,
|
|
78
|
+
config,
|
|
79
|
+
indexName: capsule.scope.indexName,
|
|
80
|
+
});
|
|
81
|
+
return options.format === "md"
|
|
82
|
+
? formatContextCapsuleVerificationMarkdown(receipt)
|
|
83
|
+
: canonicalVerifiedContextCapsuleJson(receipt);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof CliError) throw error;
|
|
86
|
+
throw contextCliError(error);
|
|
87
|
+
} finally {
|
|
88
|
+
await store.close();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -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,
|