@gmickel/gno 2.4.0 → 2.6.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 +69 -6
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +98 -1
- package/assets/skill/cli-reference.md +140 -0
- package/assets/skill/examples.md +45 -0
- package/assets/skill/mcp-reference.md +62 -0
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +358 -24
- package/spec/compiled-context.md +68 -0
- package/spec/mcp.md +223 -2
- package/spec/output-schemas/capture-receipt.schema.json +3 -0
- package/spec/output-schemas/compiled-context-check.schema.json +44 -0
- package/spec/output-schemas/compiled-context-file.schema.json +165 -0
- package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
- package/spec/output-schemas/memory-remember.schema.json +8 -2
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/src/app/compiled-context-files.ts +361 -0
- package/src/app/compiled-context.ts +240 -0
- package/src/app/context-surface.ts +7 -2
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/audit.ts +4 -0
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/context-compiled.ts +136 -0
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- package/src/cli/errors.ts +10 -3
- package/src/cli/program.ts +458 -1
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +3 -0
- package/src/core/compiled-context.ts +254 -0
- package/src/core/context-budget.ts +2 -10
- package/src/core/file-lock.ts +22 -5
- package/src/core/folder-setup-planning.ts +2 -1
- package/src/core/memory-remember.ts +233 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +16 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/setup-receipt.ts +27 -20
- package/src/core/typed-metadata.ts +4 -0
- package/src/core/validation.ts +9 -2
- package/src/core/windows-private-path.ts +96 -0
- package/src/index.ts +11 -2
- package/src/ingestion/compiled-context.ts +15 -0
- package/src/ingestion/sync.ts +40 -8
- package/src/ingestion/walker.ts +5 -4
- package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
- package/src/mcp/context.ts +8 -0
- package/src/mcp/http-egress.ts +15 -3
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/retrieval-warnings.ts +24 -0
- package/src/mcp/tools/ask.ts +14 -1
- package/src/mcp/tools/capture.ts +87 -83
- package/src/mcp/tools/context.ts +46 -2
- package/src/mcp/tools/index.ts +119 -7
- package/src/mcp/tools/memory-remember.ts +7 -0
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/query.ts +3 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/search.ts +3 -1
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/vsearch.ts +3 -1
- package/src/sdk/client.ts +252 -89
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +82 -3
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/compiled-context.ts +84 -0
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/public/app.tsx +15 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/components/sessions/snippet.tsx +53 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +10 -2
- package/src/serve/public/lib/request-intent.ts +69 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +4 -0
- package/src/serve/public/pages/CompiledContext.tsx +364 -0
- package/src/serve/public/pages/Dashboard.tsx +17 -0
- package/src/serve/public/pages/DocView.tsx +15 -1
- package/src/serve/public/pages/DocumentEditor.tsx +139 -96
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +43 -3
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +248 -3
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/spa-production-build.ts +6 -5
- package/src/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -0
- package/src/store/sqlite/adapter.ts +68 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Bun has no lstat, exclusive-create, hard-link or atomic-rename primitives.
|
|
2
|
+
import { lstat, open, link, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, parse, resolve, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CompiledContextCheck,
|
|
8
|
+
CompiledContextPreview,
|
|
9
|
+
} from "../core/compiled-context";
|
|
10
|
+
import type { ContextCapsuleV1 } from "../core/context-capsule";
|
|
11
|
+
import type { ContextCapsuleBuildInput } from "./context-runtime";
|
|
12
|
+
|
|
13
|
+
import { withWriteLock } from "../core/file-lock";
|
|
14
|
+
import {
|
|
15
|
+
checkCompiledContext,
|
|
16
|
+
compiledContextRefreshRequest,
|
|
17
|
+
previewCompiledContext,
|
|
18
|
+
type CompiledContextRuntimeDeps,
|
|
19
|
+
} from "./compiled-context";
|
|
20
|
+
|
|
21
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
const snapshotSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
capsulePath: z.string().min(1),
|
|
25
|
+
outputDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
26
|
+
settings: z
|
|
27
|
+
.object({
|
|
28
|
+
budgetTokens: z.number().int().positive(),
|
|
29
|
+
budgetBytes: z.number().int().positive().optional(),
|
|
30
|
+
})
|
|
31
|
+
.strict(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
const sidecarSchema = snapshotSchema
|
|
35
|
+
.extend({
|
|
36
|
+
artifactKind: z.literal("gno_compiled_context_sidecar"),
|
|
37
|
+
schemaVersion: z.literal("1.0"),
|
|
38
|
+
rendererVersion: z.literal("1"),
|
|
39
|
+
previous: snapshotSchema.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
type Snapshot = z.infer<typeof snapshotSchema>;
|
|
43
|
+
type Sidecar = z.infer<typeof sidecarSchema>;
|
|
44
|
+
export interface CompiledContextFileResult {
|
|
45
|
+
status: "written" | "unchanged";
|
|
46
|
+
outputPath: string;
|
|
47
|
+
capsulePath: string;
|
|
48
|
+
sidecarPath: string;
|
|
49
|
+
preview: CompiledContextPreview;
|
|
50
|
+
}
|
|
51
|
+
const digest = (text: string): string =>
|
|
52
|
+
new Bun.CryptoHasher("sha256").update(text).digest("hex");
|
|
53
|
+
const isMissing = (error: unknown): boolean =>
|
|
54
|
+
!!error &&
|
|
55
|
+
typeof error === "object" &&
|
|
56
|
+
"code" in error &&
|
|
57
|
+
error.code === "ENOENT";
|
|
58
|
+
|
|
59
|
+
/** Refuse links in every existing path component; never create parent directories. */
|
|
60
|
+
async function safePath(path: string, allowMissing = false): Promise<string> {
|
|
61
|
+
const absolute = resolve(path);
|
|
62
|
+
const root = parse(absolute).root;
|
|
63
|
+
const parts = absolute.slice(root.length).split(/[\\/]/).filter(Boolean);
|
|
64
|
+
let current = root;
|
|
65
|
+
for (const [index, part] of parts.entries()) {
|
|
66
|
+
current = join(current, part);
|
|
67
|
+
try {
|
|
68
|
+
const stat = await lstat(current);
|
|
69
|
+
if (
|
|
70
|
+
stat.isSymbolicLink() ||
|
|
71
|
+
(index < parts.length - 1 ? !stat.isDirectory() : !stat.isFile())
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`Unsafe compiled-context path: ${current}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (allowMissing && index === parts.length - 1 && isMissing(error))
|
|
77
|
+
return absolute;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return absolute;
|
|
82
|
+
}
|
|
83
|
+
async function readBounded(path: string): Promise<string> {
|
|
84
|
+
await safePath(path);
|
|
85
|
+
const file = Bun.file(path);
|
|
86
|
+
if (file.size > MAX_BYTES)
|
|
87
|
+
throw new Error("Compiled-context input exceeds 4 MiB");
|
|
88
|
+
return file.text();
|
|
89
|
+
}
|
|
90
|
+
async function absent(path: string): Promise<void> {
|
|
91
|
+
await safePath(path, true);
|
|
92
|
+
try {
|
|
93
|
+
await lstat(path);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (isMissing(error)) return;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Destination already exists: ${path}`);
|
|
99
|
+
}
|
|
100
|
+
function outputTarget(path: string): string {
|
|
101
|
+
if (!path.endsWith(".gno-context.md"))
|
|
102
|
+
throw new Error("Output must end in .gno-context.md");
|
|
103
|
+
return resolve(path);
|
|
104
|
+
}
|
|
105
|
+
async function stage(path: string, content: string): Promise<string> {
|
|
106
|
+
await safePath(path, true);
|
|
107
|
+
const temp = join(dirname(path), `.${crypto.randomUUID()}.gno-context.tmp`);
|
|
108
|
+
const handle = await open(temp, "wx", 0o600);
|
|
109
|
+
try {
|
|
110
|
+
await Bun.write(Bun.file(handle.fd), content);
|
|
111
|
+
await handle.sync();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await unlink(temp);
|
|
114
|
+
throw error;
|
|
115
|
+
} finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
return temp;
|
|
119
|
+
}
|
|
120
|
+
async function removeTemp(path: string): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await unlink(path);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!isMissing(error)) throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function sidecar(snapshot: Snapshot, previous?: Snapshot): Sidecar {
|
|
128
|
+
return {
|
|
129
|
+
artifactKind: "gno_compiled_context_sidecar",
|
|
130
|
+
schemaVersion: "1.0",
|
|
131
|
+
rendererVersion: "1",
|
|
132
|
+
...snapshot,
|
|
133
|
+
...(previous ? { previous } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function owned(
|
|
137
|
+
outputPath: string
|
|
138
|
+
): Promise<{ markdown: string; snapshot: Snapshot; sidecarText: string }> {
|
|
139
|
+
const markdown = await readBounded(outputPath);
|
|
140
|
+
const sidecarText = await readBounded(`${outputPath}.json`);
|
|
141
|
+
const metadata = sidecarSchema.parse(JSON.parse(sidecarText));
|
|
142
|
+
const outputDigest = digest(markdown);
|
|
143
|
+
const snapshot =
|
|
144
|
+
metadata.outputDigest === outputDigest
|
|
145
|
+
? snapshotSchema.parse({
|
|
146
|
+
capsulePath: metadata.capsulePath,
|
|
147
|
+
outputDigest: metadata.outputDigest,
|
|
148
|
+
settings: metadata.settings,
|
|
149
|
+
})
|
|
150
|
+
: metadata.previous;
|
|
151
|
+
if (!snapshot || snapshot.outputDigest !== outputDigest)
|
|
152
|
+
throw new Error("Conflict: compiled context was manually changed");
|
|
153
|
+
return { markdown, snapshot, sidecarText };
|
|
154
|
+
}
|
|
155
|
+
async function unchanged(
|
|
156
|
+
outputPath: string,
|
|
157
|
+
original: Awaited<ReturnType<typeof owned>>
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
if (
|
|
160
|
+
(await readBounded(outputPath)) !== original.markdown ||
|
|
161
|
+
(await readBounded(`${outputPath}.json`)) !== original.sidecarText
|
|
162
|
+
) {
|
|
163
|
+
throw new Error("Conflict: compiled context changed during publication");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function result(
|
|
167
|
+
outputPath: string,
|
|
168
|
+
capsulePath: string,
|
|
169
|
+
preview: CompiledContextPreview,
|
|
170
|
+
status: "written" | "unchanged"
|
|
171
|
+
): CompiledContextFileResult {
|
|
172
|
+
return {
|
|
173
|
+
status,
|
|
174
|
+
outputPath,
|
|
175
|
+
capsulePath,
|
|
176
|
+
sidecarPath: `${outputPath}.json`,
|
|
177
|
+
preview,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function compileContextFile(
|
|
182
|
+
input: {
|
|
183
|
+
capsulePath: string;
|
|
184
|
+
outputPath: string;
|
|
185
|
+
budgetTokens: number;
|
|
186
|
+
budgetBytes?: number;
|
|
187
|
+
},
|
|
188
|
+
deps: CompiledContextRuntimeDeps
|
|
189
|
+
): Promise<CompiledContextFileResult> {
|
|
190
|
+
const outputPath = outputTarget(input.outputPath);
|
|
191
|
+
const capsulePath = await safePath(input.capsulePath);
|
|
192
|
+
await absent(outputPath);
|
|
193
|
+
await absent(`${outputPath}.json`);
|
|
194
|
+
await safePath(`${outputPath}.lock`, true);
|
|
195
|
+
return withWriteLock(`${outputPath}.lock`, async () => {
|
|
196
|
+
await absent(outputPath);
|
|
197
|
+
await absent(`${outputPath}.json`);
|
|
198
|
+
const capsuleText = await readBounded(capsulePath);
|
|
199
|
+
const capsule = JSON.parse(capsuleText);
|
|
200
|
+
const settings = {
|
|
201
|
+
budgetTokens: input.budgetTokens,
|
|
202
|
+
...(input.budgetBytes === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { budgetBytes: input.budgetBytes }),
|
|
205
|
+
};
|
|
206
|
+
const preview = await previewCompiledContext(
|
|
207
|
+
{ capsule, ...settings },
|
|
208
|
+
deps
|
|
209
|
+
);
|
|
210
|
+
const metadata = sidecar({
|
|
211
|
+
capsulePath,
|
|
212
|
+
outputDigest: preview.digest,
|
|
213
|
+
settings,
|
|
214
|
+
});
|
|
215
|
+
const stagedOutput = await stage(outputPath, preview.markdown);
|
|
216
|
+
let stagedSidecar: string | undefined;
|
|
217
|
+
let publishedSidecar = false;
|
|
218
|
+
try {
|
|
219
|
+
stagedSidecar = await stage(
|
|
220
|
+
`${outputPath}.json`,
|
|
221
|
+
JSON.stringify(metadata)
|
|
222
|
+
);
|
|
223
|
+
const checked = await checkCompiledContext(
|
|
224
|
+
{ capsule, markdown: preview.markdown },
|
|
225
|
+
deps
|
|
226
|
+
);
|
|
227
|
+
if (
|
|
228
|
+
checked.status !== "current" ||
|
|
229
|
+
(await readBounded(capsulePath)) !== capsuleText
|
|
230
|
+
)
|
|
231
|
+
throw new Error("Source or policy changed before publication");
|
|
232
|
+
await absent(outputPath);
|
|
233
|
+
await absent(`${outputPath}.json`);
|
|
234
|
+
await link(stagedSidecar, `${outputPath}.json`);
|
|
235
|
+
publishedSidecar = true;
|
|
236
|
+
await link(stagedOutput, outputPath);
|
|
237
|
+
return result(outputPath, capsulePath, preview, "written");
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (publishedSidecar) await removeTemp(`${outputPath}.json`);
|
|
240
|
+
throw error;
|
|
241
|
+
} finally {
|
|
242
|
+
await removeTemp(stagedOutput);
|
|
243
|
+
if (stagedSidecar) await removeTemp(stagedSidecar);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function checkContextFile(
|
|
249
|
+
input: { outputPath: string; capsulePath?: string },
|
|
250
|
+
deps: CompiledContextRuntimeDeps
|
|
251
|
+
): Promise<CompiledContextCheck> {
|
|
252
|
+
try {
|
|
253
|
+
const outputPath = outputTarget(input.outputPath);
|
|
254
|
+
const saved = await owned(outputPath);
|
|
255
|
+
const capsule = JSON.parse(
|
|
256
|
+
await readBounded(input.capsulePath ?? saved.snapshot.capsulePath)
|
|
257
|
+
);
|
|
258
|
+
return await checkCompiledContext(
|
|
259
|
+
{ capsule, markdown: saved.markdown },
|
|
260
|
+
deps
|
|
261
|
+
);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const message =
|
|
264
|
+
error instanceof Error ? error.message : "Unavailable compiled context";
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion: "1.0",
|
|
267
|
+
status: message.startsWith("Conflict:") ? "conflict" : "unverifiable",
|
|
268
|
+
reasons: [message],
|
|
269
|
+
digest: null,
|
|
270
|
+
capsuleId: null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function refreshContextFile(
|
|
276
|
+
input: { outputPath: string; capsuleOutputPath: string },
|
|
277
|
+
deps: CompiledContextRuntimeDeps,
|
|
278
|
+
build: (request: ContextCapsuleBuildInput) => Promise<ContextCapsuleV1>
|
|
279
|
+
): Promise<CompiledContextFileResult> {
|
|
280
|
+
const outputPath = outputTarget(input.outputPath);
|
|
281
|
+
if (!input.capsuleOutputPath.endsWith(".gno-context.capsule.json"))
|
|
282
|
+
throw new Error("Refreshed Capsule must end in .gno-context.capsule.json");
|
|
283
|
+
const capsulePath = await safePath(input.capsuleOutputPath, true);
|
|
284
|
+
await safePath(`${outputPath}.lock`, true);
|
|
285
|
+
return withWriteLock(`${outputPath}.lock`, async () => {
|
|
286
|
+
const saved = await owned(outputPath);
|
|
287
|
+
const priorCapsuleText = await readBounded(saved.snapshot.capsulePath);
|
|
288
|
+
const priorCapsule = JSON.parse(priorCapsuleText);
|
|
289
|
+
const current = await checkCompiledContext(
|
|
290
|
+
{ capsule: priorCapsule, markdown: saved.markdown },
|
|
291
|
+
deps
|
|
292
|
+
);
|
|
293
|
+
if (current.status === "current") {
|
|
294
|
+
const preview = await previewCompiledContext(
|
|
295
|
+
{ capsule: priorCapsule, ...saved.snapshot.settings },
|
|
296
|
+
deps
|
|
297
|
+
);
|
|
298
|
+
await unchanged(outputPath, saved);
|
|
299
|
+
if ((await readBounded(saved.snapshot.capsulePath)) !== priorCapsuleText)
|
|
300
|
+
throw new Error("Capsule changed during refresh");
|
|
301
|
+
return result(
|
|
302
|
+
outputPath,
|
|
303
|
+
saved.snapshot.capsulePath,
|
|
304
|
+
preview,
|
|
305
|
+
"unchanged"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (current.status !== "stale")
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Cannot refresh ${current.status} context: ${current.reasons.join("; ")}`
|
|
311
|
+
);
|
|
312
|
+
await absent(capsulePath);
|
|
313
|
+
// Stage first so an unwritable Capsule destination fails before retrieval/publication.
|
|
314
|
+
const stagedCapsule = await stage(capsulePath, "");
|
|
315
|
+
let stagedOutput: string | undefined;
|
|
316
|
+
let stagedSidecar: string | undefined;
|
|
317
|
+
try {
|
|
318
|
+
const capsule = await build(
|
|
319
|
+
compiledContextRefreshRequest(priorCapsule, deps)
|
|
320
|
+
);
|
|
321
|
+
const preview = await previewCompiledContext(
|
|
322
|
+
{ capsule, ...saved.snapshot.settings },
|
|
323
|
+
deps
|
|
324
|
+
);
|
|
325
|
+
await Bun.write(stagedCapsule, JSON.stringify(capsule));
|
|
326
|
+
stagedOutput = await stage(outputPath, preview.markdown);
|
|
327
|
+
stagedSidecar = await stage(
|
|
328
|
+
`${outputPath}.json`,
|
|
329
|
+
JSON.stringify(
|
|
330
|
+
sidecar(
|
|
331
|
+
{
|
|
332
|
+
capsulePath,
|
|
333
|
+
outputDigest: preview.digest,
|
|
334
|
+
settings: saved.snapshot.settings,
|
|
335
|
+
},
|
|
336
|
+
saved.snapshot
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
const checked = await checkCompiledContext(
|
|
341
|
+
{ capsule, markdown: preview.markdown },
|
|
342
|
+
deps
|
|
343
|
+
);
|
|
344
|
+
if (checked.status !== "current")
|
|
345
|
+
throw new Error("Source or policy changed before publication");
|
|
346
|
+
await unchanged(outputPath, saved);
|
|
347
|
+
if ((await readBounded(saved.snapshot.capsulePath)) !== priorCapsuleText)
|
|
348
|
+
throw new Error("Capsule changed during refresh");
|
|
349
|
+
await absent(capsulePath);
|
|
350
|
+
await link(stagedCapsule, capsulePath);
|
|
351
|
+
// Retain one prior reference: either output remains verifiable after interruption.
|
|
352
|
+
await rename(stagedSidecar, `${outputPath}.json`);
|
|
353
|
+
await rename(stagedOutput, outputPath);
|
|
354
|
+
return result(outputPath, capsulePath, preview, "written");
|
|
355
|
+
} finally {
|
|
356
|
+
await removeTemp(stagedCapsule);
|
|
357
|
+
if (stagedOutput) await removeTemp(stagedOutput);
|
|
358
|
+
if (stagedSidecar) await removeTemp(stagedSidecar);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Verification and caller authority shared by all compiled-context surfaces. */
|
|
2
|
+
import type { ContextCapsuleV1 } from "../core/context-capsule";
|
|
3
|
+
import type {
|
|
4
|
+
EgressCallerContext,
|
|
5
|
+
EgressDestinationZone,
|
|
6
|
+
} from "../core/egress-policy";
|
|
7
|
+
import type {
|
|
8
|
+
ContextCapsuleBuildInput,
|
|
9
|
+
ContextCapsuleRuntimeDeps,
|
|
10
|
+
} from "./context-runtime";
|
|
11
|
+
|
|
12
|
+
import { currentEgressSources } from "../core/collection-egress-policy-service";
|
|
13
|
+
import {
|
|
14
|
+
compiledContextPreviewInputSchema,
|
|
15
|
+
compiledContextCheckInputSchema,
|
|
16
|
+
COMPILED_CONTEXT_MAX_BYTES,
|
|
17
|
+
readCompiledContextMetadata,
|
|
18
|
+
compiledContextBodyMatches,
|
|
19
|
+
renderCompiledContext,
|
|
20
|
+
type CompiledContextCheck,
|
|
21
|
+
type CompiledContextPreview,
|
|
22
|
+
} from "../core/compiled-context";
|
|
23
|
+
import { sha256Text } from "../core/context-capsule-validation";
|
|
24
|
+
import {
|
|
25
|
+
parseCanonicalContextCapsuleForVerification,
|
|
26
|
+
rawCanonicalContextJson,
|
|
27
|
+
} from "../core/context-verifier-input";
|
|
28
|
+
import { evaluateEgressPolicy } from "../core/egress-policy";
|
|
29
|
+
import { resolveEgressLineage } from "../core/egress-provenance";
|
|
30
|
+
import {
|
|
31
|
+
verifyContextCapsuleRuntime,
|
|
32
|
+
canonicalVerifiedContextCapsuleJson,
|
|
33
|
+
} from "./context-runtime";
|
|
34
|
+
export interface CompiledContextRuntimeDeps extends ContextCapsuleRuntimeDeps {
|
|
35
|
+
destinationZone?: EgressDestinationZone;
|
|
36
|
+
caller?: EgressCallerContext;
|
|
37
|
+
}
|
|
38
|
+
class CompiledContextStateError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
readonly state: "stale" | "unverifiable",
|
|
41
|
+
message: string
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "CompiledContextStateError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parse(
|
|
48
|
+
input: unknown,
|
|
49
|
+
deps: CompiledContextRuntimeDeps
|
|
50
|
+
): ContextCapsuleV1 {
|
|
51
|
+
if (
|
|
52
|
+
new TextEncoder().encode(rawCanonicalContextJson(input)).byteLength >
|
|
53
|
+
COMPILED_CONTEXT_MAX_BYTES
|
|
54
|
+
)
|
|
55
|
+
throw new Error("Capsule exceeds 4 MiB");
|
|
56
|
+
const capsule = parseCanonicalContextCapsuleForVerification(input, deps);
|
|
57
|
+
if (capsule.schemaVersion === "1.0")
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Rebuild legacy Capsule with current provenance before compiling"
|
|
60
|
+
);
|
|
61
|
+
return capsule;
|
|
62
|
+
}
|
|
63
|
+
function authorize(
|
|
64
|
+
capsule: ContextCapsuleV1,
|
|
65
|
+
deps: CompiledContextRuntimeDeps
|
|
66
|
+
): string {
|
|
67
|
+
if (capsule.schemaVersion === "1.0")
|
|
68
|
+
throw new Error("Capsule lineage unavailable");
|
|
69
|
+
const names = [
|
|
70
|
+
...new Set([
|
|
71
|
+
...capsule.scope.collections,
|
|
72
|
+
...capsule.evidence.map((item) => item.collection),
|
|
73
|
+
...capsule.egressLineage.sources.map((item) => item.collection),
|
|
74
|
+
]),
|
|
75
|
+
];
|
|
76
|
+
if (
|
|
77
|
+
names.some(
|
|
78
|
+
(name) =>
|
|
79
|
+
!deps.config.collections.some((collection) => collection.name === name)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
throw new CompiledContextStateError(
|
|
83
|
+
"unverifiable",
|
|
84
|
+
"Collection scope is no longer configured; rebuild within eligible scope"
|
|
85
|
+
);
|
|
86
|
+
const sources = currentEgressSources(deps.config, names);
|
|
87
|
+
const decision = evaluateEgressPolicy({
|
|
88
|
+
collections: sources,
|
|
89
|
+
action: "export",
|
|
90
|
+
destination: { zone: deps.destinationZone ?? "local_process" },
|
|
91
|
+
caller: deps.caller ?? { authenticated: true, operationAuthorized: true },
|
|
92
|
+
contentClass: "capsule",
|
|
93
|
+
});
|
|
94
|
+
if (!decision.allowed)
|
|
95
|
+
throw new CompiledContextStateError(
|
|
96
|
+
"unverifiable",
|
|
97
|
+
"Current collection policy or caller authority denies this context"
|
|
98
|
+
);
|
|
99
|
+
const lineage = resolveEgressLineage(sources, names);
|
|
100
|
+
if (lineage.digest !== capsule.egressLineage.digest)
|
|
101
|
+
throw new CompiledContextStateError(
|
|
102
|
+
"stale",
|
|
103
|
+
"Collection policy changed; rebuild and verify the Capsule"
|
|
104
|
+
);
|
|
105
|
+
return lineage.digest;
|
|
106
|
+
}
|
|
107
|
+
async function verified(
|
|
108
|
+
capsule: ContextCapsuleV1,
|
|
109
|
+
deps: CompiledContextRuntimeDeps
|
|
110
|
+
): Promise<{ verificationDigest: string; lineageDigest: string }> {
|
|
111
|
+
const lineageDigest = authorize(capsule, deps);
|
|
112
|
+
const receipt = await verifyContextCapsuleRuntime(capsule, deps);
|
|
113
|
+
if (
|
|
114
|
+
receipt.contentStatus !== "unchanged" ||
|
|
115
|
+
receipt.fingerprintStatus !== "unchanged"
|
|
116
|
+
)
|
|
117
|
+
throw new CompiledContextStateError(
|
|
118
|
+
"stale",
|
|
119
|
+
`Capsule requires refresh: ${[receipt.contentCode, ...receipt.fingerprintReasons].join(", ")}`
|
|
120
|
+
);
|
|
121
|
+
authorize(capsule, deps);
|
|
122
|
+
return {
|
|
123
|
+
verificationDigest: sha256Text(
|
|
124
|
+
canonicalVerifiedContextCapsuleJson(receipt)
|
|
125
|
+
),
|
|
126
|
+
lineageDigest,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function previewCompiledContext(
|
|
130
|
+
input: unknown,
|
|
131
|
+
deps: CompiledContextRuntimeDeps
|
|
132
|
+
): Promise<CompiledContextPreview> {
|
|
133
|
+
const settings = compiledContextPreviewInputSchema.parse(input);
|
|
134
|
+
const capsule = parse(settings.capsule, deps);
|
|
135
|
+
const identity = await verified(capsule, deps);
|
|
136
|
+
const preview = renderCompiledContext(
|
|
137
|
+
capsule,
|
|
138
|
+
settings,
|
|
139
|
+
identity.verificationDigest,
|
|
140
|
+
identity.lineageDigest,
|
|
141
|
+
deps.countTokens
|
|
142
|
+
);
|
|
143
|
+
const finalIdentity = await verified(capsule, deps);
|
|
144
|
+
if (finalIdentity.verificationDigest !== identity.verificationDigest)
|
|
145
|
+
throw new CompiledContextStateError(
|
|
146
|
+
"stale",
|
|
147
|
+
"Index changed during compilation; retry after indexing completes"
|
|
148
|
+
);
|
|
149
|
+
return preview;
|
|
150
|
+
}
|
|
151
|
+
export async function checkCompiledContext(
|
|
152
|
+
input: unknown,
|
|
153
|
+
deps: CompiledContextRuntimeDeps
|
|
154
|
+
): Promise<CompiledContextCheck> {
|
|
155
|
+
const result = (
|
|
156
|
+
status: CompiledContextCheck["status"],
|
|
157
|
+
reasons: string[],
|
|
158
|
+
digest: string | null = null,
|
|
159
|
+
capsuleId: string | null = null
|
|
160
|
+
): CompiledContextCheck => ({
|
|
161
|
+
schemaVersion: "1.0",
|
|
162
|
+
status,
|
|
163
|
+
reasons,
|
|
164
|
+
digest,
|
|
165
|
+
capsuleId,
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
const { capsule: raw, markdown } =
|
|
169
|
+
compiledContextCheckInputSchema.parse(input);
|
|
170
|
+
const capsule = parse(raw, deps);
|
|
171
|
+
// Authorize before reporting identifiers from supplied private evidence.
|
|
172
|
+
authorize(capsule, deps);
|
|
173
|
+
const meta = readCompiledContextMetadata(markdown);
|
|
174
|
+
if (!compiledContextBodyMatches(markdown))
|
|
175
|
+
return result("conflict", [
|
|
176
|
+
"Output bytes were edited; preserve edits before recompiling",
|
|
177
|
+
]);
|
|
178
|
+
const expected = await previewCompiledContext(
|
|
179
|
+
{
|
|
180
|
+
capsule,
|
|
181
|
+
budgetTokens: meta.budgetTokens,
|
|
182
|
+
budgetBytes: meta.budgetBytes,
|
|
183
|
+
},
|
|
184
|
+
deps
|
|
185
|
+
);
|
|
186
|
+
if (expected.markdown !== markdown)
|
|
187
|
+
return result("conflict", [
|
|
188
|
+
"Artifact metadata, renderer settings, or Capsule identity differs",
|
|
189
|
+
]);
|
|
190
|
+
return result("current", [], expected.digest, capsule.capsuleId);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof CompiledContextStateError)
|
|
193
|
+
return result(error.state, [error.message]);
|
|
194
|
+
// Do not echo parser paths/values or supplied private source material.
|
|
195
|
+
return result("unverifiable", [
|
|
196
|
+
"Invalid or unavailable Capsule, artifact metadata, index, or recorded tokenizer; rebuild or supply the original matching inputs",
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function compiledContextRefreshRequest(
|
|
201
|
+
input: unknown,
|
|
202
|
+
authority: Pick<
|
|
203
|
+
CompiledContextRuntimeDeps,
|
|
204
|
+
"countTokens" | "tokenizerFingerprint"
|
|
205
|
+
> = {}
|
|
206
|
+
): ContextCapsuleBuildInput {
|
|
207
|
+
// Refresh cannot trust stale fingerprints, but must preserve the saved request exactly.
|
|
208
|
+
const capsule = parseCanonicalContextCapsuleForVerification(input, authority);
|
|
209
|
+
if (capsule.schemaVersion === "1.0")
|
|
210
|
+
throw new Error("Rebuild legacy Capsule before refresh");
|
|
211
|
+
const request = capsule.retrieval.request;
|
|
212
|
+
return {
|
|
213
|
+
goal: capsule.goal,
|
|
214
|
+
query: capsule.query,
|
|
215
|
+
indexName: capsule.scope.indexName,
|
|
216
|
+
collections: capsule.scope.collections,
|
|
217
|
+
uriPrefix: capsule.scope.uriPrefix,
|
|
218
|
+
tagsAll: capsule.scope.tagsAll,
|
|
219
|
+
tagsAny: capsule.scope.tagsAny,
|
|
220
|
+
categories: capsule.scope.categories,
|
|
221
|
+
since: capsule.scope.since ?? undefined,
|
|
222
|
+
until: capsule.scope.until ?? undefined,
|
|
223
|
+
filter: "filter" in capsule.scope ? capsule.scope.filter : undefined,
|
|
224
|
+
author: request.author ?? undefined,
|
|
225
|
+
lang: request.lang ?? undefined,
|
|
226
|
+
intent: request.intent ?? undefined,
|
|
227
|
+
exclude: request.exclude,
|
|
228
|
+
minScore: request.minScore ?? undefined,
|
|
229
|
+
queryModes: request.queryModes,
|
|
230
|
+
limit: request.limit,
|
|
231
|
+
candidateLimit: request.candidateLimit,
|
|
232
|
+
graph: request.graphRequested,
|
|
233
|
+
noRerank: request.rerankRequested === false,
|
|
234
|
+
depthPolicy: capsule.retrieval.depthPolicy,
|
|
235
|
+
budgetTokens: capsule.budget.requestedTokens,
|
|
236
|
+
budgetBytes: capsule.budget.requestedBytes,
|
|
237
|
+
safetyMarginTokens: capsule.budget.safetyMarginTokens,
|
|
238
|
+
safetyMarginBytes: capsule.budget.safetyMarginBytes,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -5,7 +5,10 @@ import { z } from "zod";
|
|
|
5
5
|
import type { ContextCapsuleBuildInput } from "./context-runtime-types";
|
|
6
6
|
|
|
7
7
|
import { ContextCapsuleContractError } from "../core/context-capsule";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
metadataPredicateSchema,
|
|
10
|
+
METADATA_FILTER_DESCRIPTION,
|
|
11
|
+
} from "../core/typed-metadata";
|
|
9
12
|
|
|
10
13
|
const queryModeSchema = z
|
|
11
14
|
.object({
|
|
@@ -29,7 +32,9 @@ export const contextBuildSurfaceSchema = z
|
|
|
29
32
|
tagsAll: stringList.optional(),
|
|
30
33
|
tagsAny: stringList.optional(),
|
|
31
34
|
categories: stringList.optional(),
|
|
32
|
-
filter: metadataPredicateSchema
|
|
35
|
+
filter: metadataPredicateSchema
|
|
36
|
+
.optional()
|
|
37
|
+
.describe(METADATA_FILTER_DESCRIPTION),
|
|
33
38
|
author: z.string().optional(),
|
|
34
39
|
lang: z.string().optional(),
|
|
35
40
|
intent: z.string().optional(),
|
package/src/cli/commands/ask.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "../../llm/types";
|
|
16
16
|
import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
|
|
17
17
|
|
|
18
|
+
import { decorateUriForIndex } from "../../app/constants";
|
|
18
19
|
import { buildVerifiedAsk } from "../../app/verified-ask";
|
|
19
20
|
import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
|
|
20
21
|
import {
|
|
@@ -96,7 +97,7 @@ export async function ask(
|
|
|
96
97
|
const globals = getGlobals();
|
|
97
98
|
|
|
98
99
|
const initResult = await initStore({
|
|
99
|
-
configPath: options.configPath,
|
|
100
|
+
configPath: options.configPath ?? globals.config,
|
|
100
101
|
indexName: globals.index,
|
|
101
102
|
collection: options.collection,
|
|
102
103
|
syncConfig: false,
|
|
@@ -434,9 +435,20 @@ export async function ask(
|
|
|
434
435
|
return { success: false, error: finalized.error.message };
|
|
435
436
|
}
|
|
436
437
|
}
|
|
438
|
+
// URIs carry `?index=` like search and Capsule results on a named index.
|
|
437
439
|
return {
|
|
438
440
|
success: true,
|
|
439
|
-
data:
|
|
441
|
+
data: {
|
|
442
|
+
...askResult,
|
|
443
|
+
results: askResult.results.map((result) => ({
|
|
444
|
+
...result,
|
|
445
|
+
uri: decorateUriForIndex(result.uri, globals.index),
|
|
446
|
+
})),
|
|
447
|
+
citations: askResult.citations?.map((citation) => ({
|
|
448
|
+
...citation,
|
|
449
|
+
uri: decorateUriForIndex(citation.uri, globals.index),
|
|
450
|
+
})),
|
|
451
|
+
},
|
|
440
452
|
metadata: traceSession?.metadata(),
|
|
441
453
|
};
|
|
442
454
|
} catch (cause) {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { runWorkspaceAudit } from "../../core/audit-workspace";
|
|
22
22
|
import { normalizeTag, validateTag } from "../../core/tags";
|
|
23
23
|
import { normalizeCollectionName } from "../../core/validation";
|
|
24
|
+
import { windowsPrivatePath } from "../../core/windows-private-path";
|
|
24
25
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
25
26
|
|
|
26
27
|
export interface AuditCommandOptions {
|
|
@@ -218,12 +219,15 @@ export const writeAuditReport = async (
|
|
|
218
219
|
);
|
|
219
220
|
const temporaryPath = join(temporaryDirectory, "report");
|
|
220
221
|
try {
|
|
222
|
+
if (process.platform === "win32")
|
|
223
|
+
await windowsPrivatePath(temporaryDirectory, true);
|
|
221
224
|
await Bun.write(temporaryPath, `${formatAuditReport(report, options)}\n`, {
|
|
222
225
|
createPath: false,
|
|
223
226
|
mode: 0o600,
|
|
224
227
|
});
|
|
225
228
|
await chmod(temporaryPath, 0o600);
|
|
226
229
|
await rename(temporaryPath, path);
|
|
230
|
+
if (process.platform === "win32") await windowsPrivatePath(path);
|
|
227
231
|
} finally {
|
|
228
232
|
await unlink(temporaryPath).catch(() => undefined);
|
|
229
233
|
await rmdir(temporaryDirectory).catch(() => undefined);
|