@gmickel/gno 1.35.0 → 1.36.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/assets/skill/SKILL.md +26 -3
- package/assets/skill/cli-reference.md +9 -0
- package/assets/skill/mcp-reference.md +3 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v1.35.0.zip → gno-browser-clipper-v1.36.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.36.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +74 -1
- package/spec/mcp.md +60 -1
- package/spec/output-schemas/peek.schema.json +212 -0
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/src/cli/commands/peek.ts +66 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +20 -0
- package/src/core/peek.ts +202 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +14 -0
- package/src/mcp/tools/peek.ts +78 -0
- package/src/serve/public/globals.built.css +1 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +0 -1
package/src/core/peek.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared peek snapshot builder for CLI and later MCP.
|
|
3
|
+
* Metadata-only: never opens the model cache, resolves model URIs, or activates.
|
|
4
|
+
*
|
|
5
|
+
* @module src/core/peek
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// node:path — no Bun path utils
|
|
9
|
+
import { join as pathJoin } from "node:path";
|
|
10
|
+
|
|
11
|
+
import type { DocumentRow, IndexStatus } from "../store/types";
|
|
12
|
+
|
|
13
|
+
import { DEFAULT_INDEX_NAME, VERSION, getIndexDbPath } from "../app/constants";
|
|
14
|
+
import {
|
|
15
|
+
isProcessAlive,
|
|
16
|
+
readPidFile,
|
|
17
|
+
resolveProcessPaths,
|
|
18
|
+
} from "../cli/detach";
|
|
19
|
+
import { CliError } from "../cli/errors";
|
|
20
|
+
import { isInitialized, loadConfig } from "../config";
|
|
21
|
+
import { SqliteAdapter } from "../store/sqlite/adapter";
|
|
22
|
+
|
|
23
|
+
export const PEEK_SCHEMA_VERSION = "peek@1.0" as const;
|
|
24
|
+
export const PEEK_RECENT_LIMIT = 10;
|
|
25
|
+
|
|
26
|
+
export interface PeekCounts {
|
|
27
|
+
documents: number;
|
|
28
|
+
collections: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PeekBacklog {
|
|
32
|
+
pending: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PeekRecentItem {
|
|
37
|
+
docid: string;
|
|
38
|
+
uri: string;
|
|
39
|
+
title: string | null;
|
|
40
|
+
collection: string;
|
|
41
|
+
absPath: string;
|
|
42
|
+
modifiedAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PeekServe {
|
|
46
|
+
running: boolean;
|
|
47
|
+
url: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PeekSnapshot {
|
|
51
|
+
schemaVersion: typeof PEEK_SCHEMA_VERSION;
|
|
52
|
+
gnoVersion: string;
|
|
53
|
+
generatedAt: string;
|
|
54
|
+
initialized: boolean;
|
|
55
|
+
indexName: string;
|
|
56
|
+
counts: PeekCounts | null;
|
|
57
|
+
backlog: PeekBacklog | null;
|
|
58
|
+
lastIndexedAt: string | null;
|
|
59
|
+
recent: PeekRecentItem[];
|
|
60
|
+
serve: PeekServe;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BuildPeekOptions {
|
|
64
|
+
configPath?: string;
|
|
65
|
+
indexName?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function asRuntimeError(error: unknown, fallback: string): CliError {
|
|
69
|
+
if (error instanceof CliError) {
|
|
70
|
+
return error;
|
|
71
|
+
}
|
|
72
|
+
return new CliError(
|
|
73
|
+
"RUNTIME",
|
|
74
|
+
error instanceof Error ? error.message : fallback
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readServeLiveness(): Promise<PeekServe> {
|
|
79
|
+
try {
|
|
80
|
+
const { pidFile } = resolveProcessPaths("serve");
|
|
81
|
+
const payload = await readPidFile(pidFile);
|
|
82
|
+
if (!payload || !isProcessAlive(payload.pid) || payload.port == null) {
|
|
83
|
+
return { running: false, url: null };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
running: true,
|
|
87
|
+
url: `http://localhost:${payload.port}`,
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw asRuntimeError(error, "Failed to read serve process state");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mapRecent(
|
|
95
|
+
documents: DocumentRow[],
|
|
96
|
+
status: IndexStatus
|
|
97
|
+
): PeekRecentItem[] {
|
|
98
|
+
const collectionPaths = new Map(
|
|
99
|
+
status.collections.map((collection) => [collection.name, collection.path])
|
|
100
|
+
);
|
|
101
|
+
return documents.slice(0, PEEK_RECENT_LIMIT).map((doc) => {
|
|
102
|
+
const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
|
|
103
|
+
const collectionPath = collectionPaths.get(doc.collection) ?? "";
|
|
104
|
+
return {
|
|
105
|
+
docid: doc.docid,
|
|
106
|
+
uri: doc.uri,
|
|
107
|
+
title: doc.title,
|
|
108
|
+
collection: doc.collection,
|
|
109
|
+
absPath: pathJoin(collectionPath, sourceRelPath),
|
|
110
|
+
modifiedAt: doc.sourceMtime,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function emptySnapshot(
|
|
116
|
+
indexName: string,
|
|
117
|
+
generatedAt: string,
|
|
118
|
+
serve: PeekServe
|
|
119
|
+
): PeekSnapshot {
|
|
120
|
+
return {
|
|
121
|
+
schemaVersion: PEEK_SCHEMA_VERSION,
|
|
122
|
+
gnoVersion: VERSION,
|
|
123
|
+
generatedAt,
|
|
124
|
+
initialized: false,
|
|
125
|
+
indexName,
|
|
126
|
+
counts: null,
|
|
127
|
+
backlog: null,
|
|
128
|
+
lastIndexedAt: null,
|
|
129
|
+
recent: [],
|
|
130
|
+
serve,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build a peek@1.0 snapshot. Throws CliError("RUNTIME") on any subquery
|
|
136
|
+
* failure so callers never emit a half-filled payload.
|
|
137
|
+
*/
|
|
138
|
+
export async function buildPeekSnapshot(
|
|
139
|
+
options: BuildPeekOptions = {}
|
|
140
|
+
): Promise<PeekSnapshot> {
|
|
141
|
+
const generatedAt = new Date().toISOString();
|
|
142
|
+
const requestedIndex = options.indexName ?? DEFAULT_INDEX_NAME;
|
|
143
|
+
const serve = await readServeLiveness();
|
|
144
|
+
|
|
145
|
+
const initialized = await isInitialized(options.configPath);
|
|
146
|
+
if (!initialized) {
|
|
147
|
+
return emptySnapshot(requestedIndex, generatedAt, serve);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const configResult = await loadConfig(options.configPath);
|
|
151
|
+
if (!configResult.ok) {
|
|
152
|
+
throw new CliError("RUNTIME", configResult.error.message);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const store = new SqliteAdapter();
|
|
156
|
+
const openResult = await store.open(
|
|
157
|
+
getIndexDbPath(options.indexName),
|
|
158
|
+
configResult.value.ftsTokenizer
|
|
159
|
+
);
|
|
160
|
+
if (!openResult.ok) {
|
|
161
|
+
throw new CliError("RUNTIME", openResult.error.message);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const statusResult = await store.getStatus();
|
|
166
|
+
if (!statusResult.ok) {
|
|
167
|
+
throw new CliError("RUNTIME", statusResult.error.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const recentResult = await store.listDocumentsPaginated({
|
|
171
|
+
limit: PEEK_RECENT_LIMIT,
|
|
172
|
+
offset: 0,
|
|
173
|
+
});
|
|
174
|
+
if (!recentResult.ok) {
|
|
175
|
+
throw new CliError("RUNTIME", recentResult.error.message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const status = statusResult.value;
|
|
179
|
+
return {
|
|
180
|
+
schemaVersion: PEEK_SCHEMA_VERSION,
|
|
181
|
+
gnoVersion: VERSION,
|
|
182
|
+
generatedAt,
|
|
183
|
+
initialized: true,
|
|
184
|
+
indexName: status.indexName,
|
|
185
|
+
counts: {
|
|
186
|
+
documents: status.activeDocuments,
|
|
187
|
+
collections: status.collections.length,
|
|
188
|
+
},
|
|
189
|
+
backlog: {
|
|
190
|
+
pending: status.embeddingBacklog,
|
|
191
|
+
failed: status.recentErrors,
|
|
192
|
+
},
|
|
193
|
+
lastIndexedAt: status.lastUpdatedAt,
|
|
194
|
+
recent: mapRecent(recentResult.value.documents, status),
|
|
195
|
+
serve,
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw asRuntimeError(error, "Failed to build peek snapshot");
|
|
199
|
+
} finally {
|
|
200
|
+
await store.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/mcp/http-egress.ts
CHANGED
package/src/mcp/tools/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
import { handleListJobs } from "./list-jobs";
|
|
66
66
|
import { handleListTags } from "./list-tags";
|
|
67
67
|
import { handleMultiGet } from "./multi-get";
|
|
68
|
+
import { handlePeek, PEEK_MCP_ANNOTATIONS } from "./peek";
|
|
68
69
|
import { handleQuery, handleQueryDiagnose } from "./query";
|
|
69
70
|
import { handleRemoveCollection } from "./remove-collection";
|
|
70
71
|
import { handleSearch } from "./search";
|
|
@@ -129,6 +130,7 @@ export const MCP_TOOL_DESCRIPTIONS = {
|
|
|
129
130
|
"Retrieve multiple documents by refs array or glob pattern. Use after gno_search/gno_query to batch top result URIs/docids; set maxBytes and lineNumbers to control context size.",
|
|
130
131
|
section:
|
|
131
132
|
"Create or resolve a durable SectionTargetV1 against one indexed document. action=create needs ref plus exactly one of anchor|line; action=resolve needs ref plus target. Exact/recovered include citation (uri, anchor, title, inclusive lines, fingerprint); ambiguous/stale/missing omit citation and are not safe to navigate or cite. Read-only — does not write or persist targets. Follow navigable ranges with gno_get fromLine/lineCount.",
|
|
133
|
+
peek: "Cheap peek@1.0 snapshot: initialized flag, document/collection counts, embedding backlog, recent files, and serve liveness. Model-free — never initializes embeddings or models. Use for counts/backlog/recent/serve questions; use gno_status for full health and activation.",
|
|
132
134
|
status:
|
|
133
135
|
"Get index health: collection count, document count, chunk count, embedding backlog, and per-collection stats. Check first when vector/hybrid results look stale or unavailable.",
|
|
134
136
|
audit:
|
|
@@ -664,6 +666,8 @@ const multiGetInputSchema = z.object({
|
|
|
664
666
|
.describe("Include line numbers in output"),
|
|
665
667
|
});
|
|
666
668
|
|
|
669
|
+
const peekInputSchema = z.object({});
|
|
670
|
+
|
|
667
671
|
const statusInputSchema = z.object({});
|
|
668
672
|
|
|
669
673
|
const jobStatusInputSchema = z.object({
|
|
@@ -1086,6 +1090,16 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1086
1090
|
(args) => handleMultiGet(args, ctx)
|
|
1087
1091
|
);
|
|
1088
1092
|
|
|
1093
|
+
server.registerTool(
|
|
1094
|
+
"gno_peek",
|
|
1095
|
+
{
|
|
1096
|
+
description: MCP_TOOL_DESCRIPTIONS.peek,
|
|
1097
|
+
inputSchema: peekInputSchema,
|
|
1098
|
+
annotations: PEEK_MCP_ANNOTATIONS,
|
|
1099
|
+
},
|
|
1100
|
+
(args) => handlePeek(args, ctx)
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1089
1103
|
server.tool(
|
|
1090
1104
|
"gno_status",
|
|
1091
1105
|
MCP_TOOL_DESCRIPTIONS.status,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP gno_peek tool — cheap peek@1.0 snapshot, model-free.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/peek
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PeekSnapshot } from "../../core/peek";
|
|
8
|
+
import type { ToolContext } from "../server";
|
|
9
|
+
|
|
10
|
+
import { buildPeekSnapshot } from "../../core/peek";
|
|
11
|
+
import { runTool, type ToolResult } from "./index";
|
|
12
|
+
|
|
13
|
+
type PeekInput = Record<string, never>;
|
|
14
|
+
|
|
15
|
+
export const PEEK_MCP_ANNOTATIONS = {
|
|
16
|
+
readOnlyHint: true,
|
|
17
|
+
destructiveHint: false,
|
|
18
|
+
idempotentHint: true,
|
|
19
|
+
openWorldHint: false,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
function formatPeek(snapshot: PeekSnapshot): string {
|
|
23
|
+
const lines = [
|
|
24
|
+
`schema: ${snapshot.schemaVersion}`,
|
|
25
|
+
`gno: ${snapshot.gnoVersion}`,
|
|
26
|
+
`index: ${snapshot.indexName}`,
|
|
27
|
+
`initialized: ${snapshot.initialized ? "yes" : "no"}`,
|
|
28
|
+
];
|
|
29
|
+
if (snapshot.counts) {
|
|
30
|
+
lines.push(
|
|
31
|
+
`documents: ${snapshot.counts.documents}`,
|
|
32
|
+
`collections: ${snapshot.counts.collections}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (snapshot.backlog) {
|
|
36
|
+
lines.push(
|
|
37
|
+
`backlog: ${snapshot.backlog.pending} pending, ${snapshot.backlog.failed} failed`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (snapshot.lastIndexedAt) {
|
|
41
|
+
lines.push(`lastIndexedAt: ${snapshot.lastIndexedAt}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(
|
|
44
|
+
snapshot.serve.running && snapshot.serve.url
|
|
45
|
+
? `serve: ${snapshot.serve.url}`
|
|
46
|
+
: "serve: down"
|
|
47
|
+
);
|
|
48
|
+
if (snapshot.recent.length > 0) {
|
|
49
|
+
lines.push("recent:");
|
|
50
|
+
for (const item of snapshot.recent) {
|
|
51
|
+
const label = item.title ?? item.uri;
|
|
52
|
+
lines.push(` ${item.docid} ${label}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle gno_peek tool call.
|
|
60
|
+
*
|
|
61
|
+
* Uninitialized (no config / no store) is a success payload, not an error.
|
|
62
|
+
* Never probes serve over HTTP; the shared builder uses pid-file liveness.
|
|
63
|
+
*/
|
|
64
|
+
export function handlePeek(
|
|
65
|
+
_args: PeekInput,
|
|
66
|
+
ctx: ToolContext
|
|
67
|
+
): Promise<ToolResult> {
|
|
68
|
+
return runTool(
|
|
69
|
+
ctx,
|
|
70
|
+
"gno_peek",
|
|
71
|
+
async () =>
|
|
72
|
+
buildPeekSnapshot({
|
|
73
|
+
configPath: ctx.actualConfigPath,
|
|
74
|
+
indexName: ctx.indexName,
|
|
75
|
+
}),
|
|
76
|
+
formatPeek
|
|
77
|
+
);
|
|
78
|
+
}
|