@gmickel/gno 2.5.1 → 2.7.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 +60 -5
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +80 -4
- package/assets/skill/cli-reference.md +132 -2
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +54 -1
- 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.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +449 -35
- package/spec/mcp.md +234 -4
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +4 -1
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
- package/spec/output-schemas/memory-remember.schema.json +10 -4
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- 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/spec/output-schemas/status.schema.json +98 -0
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/query.ts +5 -0
- 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/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +441 -2
- 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 +12 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +234 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +8 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/embed/backlog.ts +124 -18
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +15 -10
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +30 -2
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +17 -0
- package/src/mcp/http-egress.ts +4 -0
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +87 -85
- package/src/mcp/tools/index.ts +77 -4
- package/src/mcp/tools/memory-remember.ts +8 -1
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/sdk/client.ts +180 -84
- package/src/sdk/index.ts +6 -0
- package/src/sdk/types.ts +54 -2
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/app.tsx +4 -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/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +26 -3
- package/src/serve/public/lib/request-intent.ts +77 -0
- package/src/serve/public/lib/snippet.tsx +52 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Dashboard.tsx +22 -9
- package/src/serve/public/pages/DocView.tsx +25 -6
- package/src/serve/public/pages/DocumentEditor.tsx +224 -104
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +69 -4
- package/src/serve/resident-status.ts +13 -1
- 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 +215 -10
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/status-model.ts +16 -0
- package/src/serve/status.ts +2 -0
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- 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/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +76 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared leased capture publication for CLI, MCP, SDK and REST.
|
|
3
|
+
*
|
|
4
|
+
* One critical section under the shared write lease: plan against current
|
|
5
|
+
* state, publish the note, lexically sync it. With a request ID the same
|
|
6
|
+
* steps run through the request-receipt service so a retry replays or
|
|
7
|
+
* finishes the admitted capture instead of planning a second one.
|
|
8
|
+
*
|
|
9
|
+
* @module src/core/capture-publish
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// node:fs/promises mkdir: filesystem structure op, no Bun equivalent
|
|
13
|
+
import { mkdir } from "node:fs/promises";
|
|
14
|
+
// node:path has no Bun path utilities
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import type { Collection, Config } from "../config/types";
|
|
18
|
+
import type { StorePort } from "../store/types";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
buildCaptureReceipt,
|
|
22
|
+
type CaptureInput,
|
|
23
|
+
CaptureSyncError,
|
|
24
|
+
type CapturePlan,
|
|
25
|
+
type CaptureReceipt,
|
|
26
|
+
type CaptureSyncPaths,
|
|
27
|
+
ensureCapturedFileIndexed,
|
|
28
|
+
syncCapturedFile,
|
|
29
|
+
type SyncCapturedFileResult,
|
|
30
|
+
} from "./capture";
|
|
31
|
+
import { writeCapturePlanFile } from "./capture-write";
|
|
32
|
+
import { withWriteLock } from "./file-lock";
|
|
33
|
+
import {
|
|
34
|
+
type RequestCheckpoint,
|
|
35
|
+
requestDigest,
|
|
36
|
+
type RequestPlanState,
|
|
37
|
+
type RequestReceiptInfo,
|
|
38
|
+
runRequestedWrite,
|
|
39
|
+
} from "./request-receipts";
|
|
40
|
+
import { DEFAULT_LOCK_WAIT_MS } from "./write-lease";
|
|
41
|
+
|
|
42
|
+
export interface CaptureRequest {
|
|
43
|
+
ledgerPath: string;
|
|
44
|
+
namespace: string;
|
|
45
|
+
requestId: string;
|
|
46
|
+
/** Semantic capture input: the request digest payload. */
|
|
47
|
+
input: CaptureInput;
|
|
48
|
+
checkpoint?: (stage: RequestCheckpoint) => Promise<void> | void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PublishCaptureInput {
|
|
52
|
+
collection: Collection;
|
|
53
|
+
store: StorePort;
|
|
54
|
+
lockPath: string;
|
|
55
|
+
lockWaitMs?: number;
|
|
56
|
+
config?: Pick<Config, "contentTypes">;
|
|
57
|
+
syncPaths?: CaptureSyncPaths;
|
|
58
|
+
/** Plan against current state. Runs under the lease; must not write. */
|
|
59
|
+
plan: () => Promise<CapturePlan>;
|
|
60
|
+
beforeWrite?: (absPath: string) => void;
|
|
61
|
+
/**
|
|
62
|
+
* CLI/SDK receipt contract without a request ID: a failed lexical sync is
|
|
63
|
+
* reported in `sync` (not thrown) and an unindexed `open_existing` file is
|
|
64
|
+
* returned as `skipped` rather than synced.
|
|
65
|
+
*/
|
|
66
|
+
reportSyncFailure?: boolean;
|
|
67
|
+
/** Runs under the lease once the capture is retrievable. */
|
|
68
|
+
afterSync?: (
|
|
69
|
+
synced: SyncCapturedFileResult,
|
|
70
|
+
receipt: CaptureReceipt
|
|
71
|
+
) => Promise<void> | void;
|
|
72
|
+
request?: CaptureRequest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PublishedCapture {
|
|
76
|
+
receipt: CaptureReceipt;
|
|
77
|
+
request?: RequestReceiptInfo;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Private recovery record: destination, expected file hashes and the
|
|
82
|
+
* pre-sync receipt (metadata only, never the note content).
|
|
83
|
+
*/
|
|
84
|
+
interface CaptureRecoveryPlan {
|
|
85
|
+
openedExisting: boolean;
|
|
86
|
+
fileHash: string;
|
|
87
|
+
baseHash: string | null;
|
|
88
|
+
receipt: CaptureReceipt;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sha256 = (value: string): string =>
|
|
92
|
+
new Bun.CryptoHasher("sha256").update(value).digest("hex");
|
|
93
|
+
|
|
94
|
+
async function readFileHash(absPath: string): Promise<string | null> {
|
|
95
|
+
const file = Bun.file(absPath);
|
|
96
|
+
if (!(await file.exists())) return null;
|
|
97
|
+
return sha256(await file.text());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Pre-sync receipt; `sync` is replaced once the capture is retrievable. */
|
|
101
|
+
function toReceipt(plan: CapturePlan, absPath: string): CaptureReceipt {
|
|
102
|
+
return buildCaptureReceipt({
|
|
103
|
+
plan,
|
|
104
|
+
absPath,
|
|
105
|
+
overwritten: plan.collisionPolicyResult === "overwritten",
|
|
106
|
+
sync: plan.provenanceConflict
|
|
107
|
+
? {
|
|
108
|
+
status: "skipped",
|
|
109
|
+
reason:
|
|
110
|
+
"Existing capture has absent or different browser provenance.",
|
|
111
|
+
}
|
|
112
|
+
: undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function publishCapture(
|
|
117
|
+
options: PublishCaptureInput
|
|
118
|
+
): Promise<PublishedCapture> {
|
|
119
|
+
const absPathFor = (relPath: string) =>
|
|
120
|
+
join(options.collection.path, relPath);
|
|
121
|
+
const syncInput = (plan: Pick<CaptureReceipt, "relPath">) => ({
|
|
122
|
+
collection: options.collection,
|
|
123
|
+
store: options.store,
|
|
124
|
+
relPath: plan.relPath,
|
|
125
|
+
absPath: absPathFor(plan.relPath),
|
|
126
|
+
config: options.config,
|
|
127
|
+
syncPaths: options.syncPaths,
|
|
128
|
+
});
|
|
129
|
+
const write = async (plan: CapturePlan): Promise<void> => {
|
|
130
|
+
const absPath = absPathFor(plan.relPath);
|
|
131
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
132
|
+
options.beforeWrite?.(absPath);
|
|
133
|
+
await writeCapturePlanFile(plan, absPath);
|
|
134
|
+
};
|
|
135
|
+
const finish = async (
|
|
136
|
+
draft: CaptureReceipt,
|
|
137
|
+
openedExisting: boolean
|
|
138
|
+
): Promise<CaptureReceipt> => {
|
|
139
|
+
const synced = openedExisting
|
|
140
|
+
? await ensureCapturedFileIndexed(syncInput(draft))
|
|
141
|
+
: await syncCapturedFile(syncInput(draft));
|
|
142
|
+
await options.afterSync?.(synced, draft);
|
|
143
|
+
return { ...draft, docid: synced.docid, sync: synced.sync };
|
|
144
|
+
};
|
|
145
|
+
const lockWaitMs = options.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS;
|
|
146
|
+
|
|
147
|
+
if (!options.request) {
|
|
148
|
+
const receipt = await withWriteLock(
|
|
149
|
+
options.lockPath,
|
|
150
|
+
async () => {
|
|
151
|
+
const plan = await options.plan();
|
|
152
|
+
if (plan.provenanceConflict) {
|
|
153
|
+
return toReceipt(plan, absPathFor(plan.relPath));
|
|
154
|
+
}
|
|
155
|
+
if (!plan.openedExisting) await write(plan);
|
|
156
|
+
const draft = toReceipt(plan, absPathFor(plan.relPath));
|
|
157
|
+
if (!options.reportSyncFailure) {
|
|
158
|
+
return finish(draft, plan.openedExisting);
|
|
159
|
+
}
|
|
160
|
+
if (plan.openedExisting) {
|
|
161
|
+
const existing = await options.store.getDocument(
|
|
162
|
+
plan.collection,
|
|
163
|
+
plan.relPath
|
|
164
|
+
);
|
|
165
|
+
if (!existing.ok) throw new Error(existing.error.message);
|
|
166
|
+
const opened: CaptureReceipt = {
|
|
167
|
+
...draft,
|
|
168
|
+
docid: existing.value?.docid,
|
|
169
|
+
sync: existing.value
|
|
170
|
+
? { status: "completed" }
|
|
171
|
+
: {
|
|
172
|
+
status: "skipped",
|
|
173
|
+
reason: "Existing file is not indexed yet.",
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return opened;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return await finish(draft, false);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (!(error instanceof CaptureSyncError)) throw error;
|
|
182
|
+
const failed: CaptureReceipt = {
|
|
183
|
+
...draft,
|
|
184
|
+
sync: { status: "failed", error: error.syncError },
|
|
185
|
+
};
|
|
186
|
+
return failed;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
lockWaitMs
|
|
190
|
+
);
|
|
191
|
+
return { receipt };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const outcome = await runRequestedWrite<CaptureRecoveryPlan, CaptureReceipt>({
|
|
195
|
+
ledgerPath: options.request.ledgerPath,
|
|
196
|
+
namespace: options.request.namespace,
|
|
197
|
+
requestId: options.request.requestId,
|
|
198
|
+
operation: "capture",
|
|
199
|
+
digest: requestDigest("capture", options.request.input),
|
|
200
|
+
lockPath: options.lockPath,
|
|
201
|
+
lockWaitMs,
|
|
202
|
+
checkpoint: options.request.checkpoint,
|
|
203
|
+
prepare: async () => {
|
|
204
|
+
const plan = await options.plan();
|
|
205
|
+
if (plan.provenanceConflict) {
|
|
206
|
+
return { result: toReceipt(plan, absPathFor(plan.relPath)) };
|
|
207
|
+
}
|
|
208
|
+
const absPath = absPathFor(plan.relPath);
|
|
209
|
+
const baseHash = plan.overwrite ? await readFileHash(absPath) : null;
|
|
210
|
+
return {
|
|
211
|
+
plan: {
|
|
212
|
+
openedExisting: plan.openedExisting,
|
|
213
|
+
fileHash: sha256(plan.content),
|
|
214
|
+
baseHash,
|
|
215
|
+
receipt: toReceipt(plan, absPath),
|
|
216
|
+
},
|
|
217
|
+
publish: async () => {
|
|
218
|
+
if (!plan.openedExisting) await write(plan);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
inspect: async (recovery): Promise<RequestPlanState> => {
|
|
223
|
+
const onDisk = await readFileHash(absPathFor(recovery.receipt.relPath));
|
|
224
|
+
if (recovery.openedExisting) {
|
|
225
|
+
return onDisk === null ? "absent" : "published";
|
|
226
|
+
}
|
|
227
|
+
if (onDisk === recovery.fileHash) return "published";
|
|
228
|
+
if (onDisk === recovery.baseHash) return "absent";
|
|
229
|
+
return onDisk === null ? "absent" : "unexpected";
|
|
230
|
+
},
|
|
231
|
+
finish: (recovery) => finish(recovery.receipt, recovery.openedExisting),
|
|
232
|
+
resultRef: (receipt) => ({
|
|
233
|
+
uri: receipt.uri,
|
|
234
|
+
docid: receipt.docid,
|
|
235
|
+
contentHash: receipt.contentHash,
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
return { receipt: outcome.result, request: outcome.request };
|
|
239
|
+
}
|
package/src/core/capture-sync.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Collection, Config } from "../config/types";
|
|
|
10
10
|
import type { StorePort } from "../store/types";
|
|
11
11
|
import type { CaptureIndexStatus } from "./capture";
|
|
12
12
|
|
|
13
|
+
import { buildUri } from "../app/constants";
|
|
13
14
|
import {
|
|
14
15
|
type CollectionSyncResult,
|
|
15
16
|
defaultSyncService,
|
|
@@ -27,14 +28,22 @@ export class CaptureSyncError extends Error {
|
|
|
27
28
|
readonly code = CAPTURE_SYNC_FAILED_CODE;
|
|
28
29
|
readonly absPath: string;
|
|
29
30
|
readonly relPath: string;
|
|
31
|
+
/** The sync failure itself, without the write half of the message. */
|
|
32
|
+
readonly syncError: string;
|
|
30
33
|
|
|
31
|
-
constructor(input: {
|
|
34
|
+
constructor(input: {
|
|
35
|
+
absPath: string;
|
|
36
|
+
relPath: string;
|
|
37
|
+
uri: string;
|
|
38
|
+
cause: string;
|
|
39
|
+
}) {
|
|
32
40
|
super(
|
|
33
|
-
`Capture written to ${input.
|
|
41
|
+
`Capture written to ${input.uri} but lexical sync failed: ${input.cause}. Run gno update to retry indexing.`
|
|
34
42
|
);
|
|
35
43
|
this.name = "CaptureSyncError";
|
|
36
44
|
this.absPath = input.absPath;
|
|
37
45
|
this.relPath = input.relPath;
|
|
46
|
+
this.syncError = input.cause;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -79,6 +88,7 @@ export async function syncCapturedFile(
|
|
|
79
88
|
throw new CaptureSyncError({
|
|
80
89
|
absPath: input.absPath,
|
|
81
90
|
relPath: input.relPath,
|
|
91
|
+
uri: buildUri(input.collection.name, input.relPath),
|
|
82
92
|
cause,
|
|
83
93
|
});
|
|
84
94
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host path redaction for callers that are not on the owner's machine.
|
|
3
|
+
*
|
|
4
|
+
* Result payloads name a source file's host location in `absPath`
|
|
5
|
+
* (`source.absPath` on search/get results, top-level on capture, memory and
|
|
6
|
+
* peek receipts). A remote caller identifies documents by `uri` and
|
|
7
|
+
* collection-relative `relPath` instead, so every `absPath` key is removed
|
|
8
|
+
* before a payload leaves a remote-reachable surface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const HOST_PATH_FIELD = "absPath";
|
|
12
|
+
|
|
13
|
+
const isPlainObject = (value: object): boolean => {
|
|
14
|
+
const proto = Object.getPrototypeOf(value) as object | null;
|
|
15
|
+
return proto === Object.prototype || proto === null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Deep copy of a JSON-shaped value with every `absPath` key removed. */
|
|
19
|
+
export function withoutHostPaths<T>(value: T): T {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.map((item: unknown) => withoutHostPaths(item)) as T;
|
|
22
|
+
}
|
|
23
|
+
if (value === null || typeof value !== "object" || !isPlainObject(value)) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
const copy: Record<string, unknown> = {};
|
|
27
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
28
|
+
if (key !== HOST_PATH_FIELD) copy[key] = withoutHostPaths(entry);
|
|
29
|
+
}
|
|
30
|
+
return copy as T;
|
|
31
|
+
}
|