@visual-z/xeno 0.8.3 → 0.9.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/CHANGELOG.md +24 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2066
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1713
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3001
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -200
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/screen.ts +0 -548
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** R7/R8: data_ref is the original receipt, not permission to substitute current process/file state. */
|
|
2
|
-
import type { LogEvent } from "../core/events.ts";
|
|
3
|
-
import { isLogEvent } from "../core/event-validation.ts";
|
|
4
|
-
import { readObservation } from "./observations.ts";
|
|
5
|
-
|
|
6
|
-
// Assumed per-query metadata budget. Exhaustion is explicit unknown, never fallback to older success.
|
|
7
|
-
export const PROCESS_RECEIPT_READ_BYTES = 8 * 1024 * 1024;
|
|
8
|
-
export async function hydrateProcessReceipts(nodes: readonly { node: number; events: readonly LogEvent[] }[], archive: string):
|
|
9
|
-
Promise<{ node: number; events: LogEvent[] }[]> {
|
|
10
|
-
const calls = new Map<string, string>();
|
|
11
|
-
for (const part of nodes) for (const event of part.events) {
|
|
12
|
-
if (event.t === "tool_invocation") calls.set(event.call_id, event.name);
|
|
13
|
-
if (event.t === "assistant") for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, block.name);
|
|
14
|
-
}
|
|
15
|
-
let remaining = PROCESS_RECEIPT_READ_BYTES;
|
|
16
|
-
const result: { node: number; events: LogEvent[] }[] = [];
|
|
17
|
-
for (const part of nodes) {
|
|
18
|
-
const events: LogEvent[] = [];
|
|
19
|
-
for (const event of part.events) {
|
|
20
|
-
if (event.t !== "tool_result" || !event.data_ref || !["exec_command", "write_stdin", "target"].includes(calls.get(event.call_id) ?? "")) { events.push(event); continue; }
|
|
21
|
-
const ref = event.data_ref;
|
|
22
|
-
if (ref.bytes > remaining) throw new Error("process receipt metadata exceeds observation budget; state remains unknown");
|
|
23
|
-
remaining -= ref.bytes;
|
|
24
|
-
const page = await readObservation(archive, ref.sha256, 0, Math.max(1, ref.bytes));
|
|
25
|
-
if (page.bytes !== ref.bytes || page.next !== null) throw new Error("process receipt metadata length mismatch");
|
|
26
|
-
const { data_ref: _ref, ...original } = event;
|
|
27
|
-
const restored: unknown = { ...original, data: JSON.parse(page.text) };
|
|
28
|
-
if (!isLogEvent(restored)) throw new Error("archived process receipt contradicts its original event");
|
|
29
|
-
events.push(restored);
|
|
30
|
-
}
|
|
31
|
-
result.push({ node: part.node, events });
|
|
32
|
-
}
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/** N.3: bind a complete calculation input to confined files and content digests. */
|
|
2
|
-
import { lstat, open } from "node:fs/promises";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { ToolFault, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
7
|
-
import { checkSourceCancellation } from "./source-cancellation.ts";
|
|
8
|
-
|
|
9
|
-
// Assumed working-memory safety budget for the synchronous pure query engine.
|
|
10
|
-
// Exceeding it is an explicit refusal, never a truncated calculation.
|
|
11
|
-
export const MAX_QUERY_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
12
|
-
export async function readQuerySources(root: string, paths: unknown, signal?: AbortSignal): Promise<{
|
|
13
|
-
text: string; sources: { path: string; sha256: string; bytes: number }[];
|
|
14
|
-
}> {
|
|
15
|
-
if (!Array.isArray(paths) || paths.length === 0 || paths.length > 1024 || paths.some((p) => typeof p !== "string" || !p)) throw new ToolFault("invalid_input", "query paths must contain 1..1024 file paths in concatenation order", NOT_STARTED);
|
|
16
|
-
const sources: { path: string; sha256: string; bytes: number }[] = [];
|
|
17
|
-
const chunks: Uint8Array[] = [];
|
|
18
|
-
let bytes = 0;
|
|
19
|
-
for (const path of paths as string[]) {
|
|
20
|
-
checkSourceCancellation(signal);
|
|
21
|
-
const absolute = resolve(root, path), rel = relative(root, absolute);
|
|
22
|
-
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) throw new ToolFault("permission_denied", "query path escapes workspace", READ_ATTEMPTED);
|
|
23
|
-
let current = resolve(root);
|
|
24
|
-
for (const part of rel.split(sep)) {
|
|
25
|
-
current = resolve(current, part);
|
|
26
|
-
if ((await lstat(current)).isSymbolicLink()) throw new ToolFault("permission_denied", "query does not follow symbolic links", READ_ATTEMPTED);
|
|
27
|
-
}
|
|
28
|
-
const file = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
29
|
-
try {
|
|
30
|
-
const info = await file.stat();
|
|
31
|
-
if (!info.isFile()) throw new ToolFault("invalid_input", "query source must be a regular file", READ_ATTEMPTED);
|
|
32
|
-
if (bytes + info.size > MAX_QUERY_SOURCE_BYTES) throw new ToolFault("invalid_input", "query source exceeds 32 MiB resource budget; use a smaller input", READ_ATTEMPTED);
|
|
33
|
-
const hash = createHash("sha256");
|
|
34
|
-
let sourceBytes = 0;
|
|
35
|
-
for (;;) {
|
|
36
|
-
checkSourceCancellation(signal);
|
|
37
|
-
const buffer = Buffer.alloc(Math.min(65536, MAX_QUERY_SOURCE_BYTES - bytes + 1));
|
|
38
|
-
const result = await file.read(buffer, 0, buffer.length, null);
|
|
39
|
-
checkSourceCancellation(signal);
|
|
40
|
-
if (result.bytesRead === 0) break;
|
|
41
|
-
bytes += result.bytesRead; sourceBytes += result.bytesRead;
|
|
42
|
-
if (bytes > MAX_QUERY_SOURCE_BYTES) throw new ToolFault("invalid_input", "query source grew past resource budget", READ_ATTEMPTED);
|
|
43
|
-
const data = buffer.subarray(0, result.bytesRead); hash.update(data); chunks.push(data);
|
|
44
|
-
}
|
|
45
|
-
sources.push({ path: rel.split(sep).join("/"), sha256: hash.digest("hex"), bytes: sourceBytes });
|
|
46
|
-
} finally { await file.close(); }
|
|
47
|
-
}
|
|
48
|
-
return { text: new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)), sources };
|
|
49
|
-
}
|
package/src/store/recovery.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/** Complete an explicitly logged cut; never infer a user's chosen branch. */
|
|
2
|
-
import type { Cursor } from "./log.ts";
|
|
3
|
-
import { closeNode, digestNode, initNode, listNodes, nodePath, readNode, readManifest, writeCursor } from "./log.ts";
|
|
4
|
-
import { replayContext, carriedHandoff } from "./replay.ts";
|
|
5
|
-
import { deriveHandoff } from "../core/derive.ts";
|
|
6
|
-
|
|
7
|
-
export async function recoverCut(dir: string, cursor: Cursor): Promise<Cursor> {
|
|
8
|
-
const source = await readNode(dir, cursor.node);
|
|
9
|
-
const cut = source.events.at(-1);
|
|
10
|
-
if (cut?.t !== "cut" || cut.next_node === undefined) return cursor;
|
|
11
|
-
const rebuilt = await replayContext(dir, cursor.node);
|
|
12
|
-
const manifest = await readManifest(dir);
|
|
13
|
-
if (!manifest.nodes.some((n) => n.node === cursor.node)) {
|
|
14
|
-
const handoff = deriveHandoff({
|
|
15
|
-
prev: rebuilt.fullHandoff, depth: rebuilt.depth, events: rebuilt.events,
|
|
16
|
-
config: { maxUserTurns: Infinity, maxUserTurnBytes: Infinity },
|
|
17
|
-
self: { node: cursor.node, log_ref: nodePath(dir, cursor.node), sha256: await digestNode(dir, cursor.node) },
|
|
18
|
-
});
|
|
19
|
-
await closeNode(dir, cursor.node,
|
|
20
|
-
await carriedHandoff(dir, cursor.node, handoff, cut, manifest), source.events.length, cut.at);
|
|
21
|
-
}
|
|
22
|
-
const successor = await readNode(dir, cut.next_node);
|
|
23
|
-
if (successor.header === null) {
|
|
24
|
-
await initNode(dir, cut.next_node, {
|
|
25
|
-
node: cursor.node, log_ref: nodePath(dir, cursor.node), sha256: await digestNode(dir, cursor.node),
|
|
26
|
-
}, cut.at, "handoff");
|
|
27
|
-
} else if (successor.header.parent?.node !== cursor.node || successor.header.basis !== "handoff") {
|
|
28
|
-
throw new Error(`INTEGRITY: cut successor ${cut.next_node} belongs to another transition`);
|
|
29
|
-
}
|
|
30
|
-
const nodes = await listNodes(dir);
|
|
31
|
-
const next: Cursor = {
|
|
32
|
-
...cursor, node: cut.next_node,
|
|
33
|
-
next_node: Math.max(cursor.next_node, cut.next_node + 1, ...nodes.map((n) => n + 1)),
|
|
34
|
-
model: cut.next_model ?? cursor.model, updated_at: cut.at,
|
|
35
|
-
};
|
|
36
|
-
// Cuts of this session format always record the successor's provider (S.3).
|
|
37
|
-
if (cut.next_provider !== undefined) next.provider = cut.next_provider;
|
|
38
|
-
if (cut.next_thinking !== undefined) next.thinking = cut.next_thinking;
|
|
39
|
-
if (cut.next_context_ref !== undefined) next.context_ref = cut.next_context_ref;
|
|
40
|
-
delete next.at_event;
|
|
41
|
-
delete next.needs_model_checkpoint;
|
|
42
|
-
await writeCursor(dir, next);
|
|
43
|
-
return next;
|
|
44
|
-
}
|
package/src/store/replay.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rebuild the effective context, including every preceding handoff boundary.
|
|
3
|
-
*
|
|
4
|
-
* D3/G.5: physical node files are storage units, not context units. Resume,
|
|
5
|
-
* rewind and cut must agree on inherited events and accumulated state.
|
|
6
|
-
*/
|
|
7
|
-
import { deriveHandoff } from "../core/derive.ts";
|
|
8
|
-
import type { Handoff } from "../core/handoff.ts";
|
|
9
|
-
import type { CutEvent, LogEvent } from "../core/events.ts";
|
|
10
|
-
import { boundHandoff, boundRecoveryHandoff, boundRecoveryHandoffV3 } from "../core/history.ts";
|
|
11
|
-
import { retainedContext } from "../core/context-window.ts";
|
|
12
|
-
import { digestNode, nodePath, readAncestry, readManifest, type Manifest } from "./log.ts";
|
|
13
|
-
import { readNodeHistory } from "./restore.ts";
|
|
14
|
-
|
|
15
|
-
export interface ReplayedContext {
|
|
16
|
-
/** Events not already included in fullHandoff; use these for derivation. */
|
|
17
|
-
events: LogEvent[];
|
|
18
|
-
/** Model view also includes explicitly retained original exchanges. */
|
|
19
|
-
contextEvents: LogEvent[];
|
|
20
|
-
handoff: Handoff | null;
|
|
21
|
-
/** Full derivation state; only the bounded handoff is sent to the model. */
|
|
22
|
-
fullHandoff: Handoff | null;
|
|
23
|
-
depth: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** A live cut, cold replay and interrupted-cut repair must select the same version. */
|
|
27
|
-
export async function carriedHandoff(dir: string, node: number, full: Handoff,
|
|
28
|
-
policy: Pick<CutEvent, "handoff_tokens" | "working_set_version" | "guidance_tools">, manifest?: Manifest): Promise<Handoff> {
|
|
29
|
-
if (policy.handoff_tokens === undefined) return full;
|
|
30
|
-
// The recorded version selects the algorithm; a new default never reinterprets an old cut.
|
|
31
|
-
if (policy.working_set_version === 2 || policy.working_set_version === 3) {
|
|
32
|
-
const history = await readNodeHistory(dir, node, manifest, "context");
|
|
33
|
-
if (!history.ok) throw new Error(history.error);
|
|
34
|
-
return (policy.working_set_version === 3 ? boundRecoveryHandoffV3 : boundRecoveryHandoff)(full, history.nodes, policy.handoff_tokens, policy.guidance_tools);
|
|
35
|
-
}
|
|
36
|
-
return boundHandoff(full, policy.handoff_tokens);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function replayContext(
|
|
40
|
-
dir: string, node: number, manifest?: Manifest,
|
|
41
|
-
): Promise<ReplayedContext> {
|
|
42
|
-
const index = manifest ?? await readManifest(dir);
|
|
43
|
-
const seen = new Set<number>();
|
|
44
|
-
// Iterative even across handoff boundaries: a valid long session must not
|
|
45
|
-
// consume the JS call stack just because it has many compacted segments.
|
|
46
|
-
const parts: { node: number; events: LogEvent[]; depth: number }[] = [];
|
|
47
|
-
let current: number | null = node;
|
|
48
|
-
while (current !== null) {
|
|
49
|
-
if (seen.has(current)) throw new Error(`CYCLE: handoff ancestry revisits node ${current}`);
|
|
50
|
-
seen.add(current);
|
|
51
|
-
const walk = await readAncestry(dir, current, index);
|
|
52
|
-
if (!walk.ok) throw new Error(`${walk.error.code}: ${walk.error.detail}`);
|
|
53
|
-
parts.push({ node: current, events: walk.events, depth: walk.visited.length });
|
|
54
|
-
current = walk.basis?.node ?? null;
|
|
55
|
-
}
|
|
56
|
-
let handoff: Handoff | null = null;
|
|
57
|
-
let depth = 0;
|
|
58
|
-
let carried: Handoff | null = null;
|
|
59
|
-
let retained: LogEvent[] = [];
|
|
60
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
61
|
-
const part = parts[i]!;
|
|
62
|
-
depth += part.depth;
|
|
63
|
-
const contextEvents = [...retained, ...part.events];
|
|
64
|
-
if (i === 0) return { handoff: carried, fullHandoff: handoff, depth, events: part.events, contextEvents };
|
|
65
|
-
handoff = deriveHandoff({
|
|
66
|
-
prev: handoff, depth, events: part.events,
|
|
67
|
-
config: { maxUserTurns: Infinity, maxUserTurnBytes: Infinity },
|
|
68
|
-
self: { node: part.node, log_ref: nodePath(dir, part.node), sha256: await digestNode(dir, part.node) },
|
|
69
|
-
});
|
|
70
|
-
const tail = part.events.at(-1);
|
|
71
|
-
carried = tail?.t === "cut" ? await carriedHandoff(dir, part.node, handoff, tail, index) : handoff;
|
|
72
|
-
retained = tail?.t === "cut" && tail.retained_from !== undefined
|
|
73
|
-
? retainedContext(contextEvents, tail.retained_from) : [];
|
|
74
|
-
}
|
|
75
|
-
throw new Error(`MISSING_HEADER: no context for node ${node}`);
|
|
76
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/** One resource inventory for loading and configuration; paths carry their declaring scope. */
|
|
2
|
-
import { readdir, realpath, stat, mkdir } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
-
import { configDir, readJsonFile } from "./config.ts";
|
|
6
|
-
import { atomicWrite } from "./atomic.ts";
|
|
7
|
-
import { serialized } from "./serial.ts";
|
|
8
|
-
import { parseSettings, type Settings } from "../core/settings.ts";
|
|
9
|
-
import { parseResourcePattern, resourceOverride, updateResourcePatterns, type ResourceEntry, type ResourceKind, type ResourceOverride } from "../core/resource-selection.ts";
|
|
10
|
-
import { packageResourceEnabled, packageConfig } from "../core/packages.ts";
|
|
11
|
-
import { packageRoots, packageManifest, packageSource, configuredPackages, savePackageDeclaration, type PackageRoot } from "./packages.ts";
|
|
12
|
-
|
|
13
|
-
export interface ResourceOptions {
|
|
14
|
-
workspace: string; trusted: boolean; configDir?: string;
|
|
15
|
-
noSkills?: boolean; noPromptTemplates?: boolean;
|
|
16
|
-
noThemes?: boolean; themePaths?: readonly string[];
|
|
17
|
-
skillPaths?: readonly string[]; promptTemplatePaths?: readonly string[];
|
|
18
|
-
noExtensions?: boolean; extensionPaths?: readonly string[];
|
|
19
|
-
packagePaths?: readonly { source: string; path: string }[];
|
|
20
|
-
local?: boolean;
|
|
21
|
-
}
|
|
22
|
-
export interface ResourceIndex {
|
|
23
|
-
entries: ResourceEntry[]; diagnostics: string[]; user: Settings; project: Settings;
|
|
24
|
-
userDir: string; projectDir: string; trusted: boolean;
|
|
25
|
-
}
|
|
26
|
-
export function resourcePath(path: string, base: string): string {
|
|
27
|
-
const expanded = path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
28
|
-
return resolve(base, expanded);
|
|
29
|
-
}
|
|
30
|
-
function matches(path: string, pattern: string, base: string): boolean {
|
|
31
|
-
const absolute = resourcePath(pattern, base);
|
|
32
|
-
return path === absolute || path.startsWith(absolute + sep) || new Bun.Glob(absolute.replace(/\\/g, "/")).match(path.replace(/\\/g, "/"));
|
|
33
|
-
}
|
|
34
|
-
export async function discoverResources(opts: ResourceOptions): Promise<ResourceIndex> {
|
|
35
|
-
const userDir = opts.configDir ?? configDir(), projectDir = join(opts.workspace, ".xeno");
|
|
36
|
-
const index: ResourceIndex = { entries: [], diagnostics: [], user: {}, project: {}, userDir, projectDir, trusted: opts.trusted };
|
|
37
|
-
for (const scope of ["user", "project"] as const) {
|
|
38
|
-
const file = join(scope === "user" ? userDir : projectDir, "settings.json");
|
|
39
|
-
const read = await readJsonFile<unknown>(file);
|
|
40
|
-
if (read.error) index.diagnostics.push(read.error);
|
|
41
|
-
const parsed = parseSettings(read.value);
|
|
42
|
-
index.diagnostics.push(...parsed.diagnostics.map(message => `${file}: ${message}`));
|
|
43
|
-
// Global-only policy cannot be granted by the project it would trust.
|
|
44
|
-
const { defaultProjectTrust: _ignored, ...project } = parsed.settings;
|
|
45
|
-
index[scope] = scope === "project" ? project : parsed.settings;
|
|
46
|
-
}
|
|
47
|
-
const found = new Map<string, ResourceEntry>();
|
|
48
|
-
const packages=await packageRoots(index.user.packages??[],index.project.packages??[],index,index.diagnostics);
|
|
49
|
-
const explicitPackageRoots=new Set<string>();
|
|
50
|
-
for(const source of opts.extensionPaths??[]) {
|
|
51
|
-
const path=resourcePath(source,opts.workspace);
|
|
52
|
-
const manifest=await readJsonFile<{pi?:unknown;xeno?:unknown}>(join(path,"package.json"));
|
|
53
|
-
const conventional=(await Promise.all(["extensions","skills","prompts","themes"].map(kind=>stat(join(path,kind)).then(s=>s.isDirectory(),()=>false)))).some(Boolean);
|
|
54
|
-
if(manifest.value?.pi||manifest.value?.xeno||conventional) {
|
|
55
|
-
explicitPackageRoots.add(path); packages.push({source:packageSource(source,opts.workspace),declaration:{source},scope:"cli",base:opts.workspace,root:path,trusted:true});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
for(const entry of opts.packagePaths??[])packages.push({source:packageSource(entry.source,opts.workspace),declaration:{source:entry.source},scope:"cli",base:opts.workspace,root:entry.path,trusted:true});
|
|
59
|
-
for (const kind of ["skills", "prompts", "themes", "extensions"] as const) {
|
|
60
|
-
const disabled = kind === "skills" ? opts.noSkills : kind === "prompts" ? opts.noPromptTemplates : kind === "extensions" ? opts.noExtensions : opts.noThemes;
|
|
61
|
-
for (const scope of ["user", "project"] as const) {
|
|
62
|
-
const base = scope === "user" ? userDir : projectDir;
|
|
63
|
-
const sources = [join(base, kind), ...(index[scope][kind] ?? []).map(parseResourcePattern).filter(p => p.enabled).map(p => resourcePath(p.pattern, base))];
|
|
64
|
-
for (const source of [...new Set(sources)]) for (const path of await resourceFiles(source, kind, index.diagnostics, source !== join(base, kind))) {
|
|
65
|
-
// A project override of an inherited path does not transfer resource ownership.
|
|
66
|
-
if (found.has(kind + ":" + path)) continue;
|
|
67
|
-
const global = resourceOverride(index.user[kind] ?? [], pattern => matches(path, pattern, userDir));
|
|
68
|
-
const local = resourceOverride(index.project[kind] ?? [], pattern => matches(path, pattern, projectDir));
|
|
69
|
-
const inherited = !disabled && global !== "skip";
|
|
70
|
-
const trusted = scope === "user" || opts.trusted;
|
|
71
|
-
const enabled = trusted && !disabled && (opts.trusted && local !== "inherit" ? local === "load" : inherited);
|
|
72
|
-
found.set(kind + ":" + path, { kind, path, name: kind === "skills" ? basename(dirname(path)) : basename(path).replace(/\.[^.]+$/, ""),
|
|
73
|
-
scope, source, enabled, inherited, override: local, trusted });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const cli = kind === "skills" ? opts.skillPaths : kind === "prompts" ? opts.promptTemplatePaths : kind === "extensions" ? opts.extensionPaths : opts.themePaths;
|
|
77
|
-
for (const source of cli ?? []) for (const path of explicitPackageRoots.has(resourcePath(source,opts.workspace)) ? [] : await resourceFiles(resourcePath(source, opts.workspace), kind, index.diagnostics, true)) {
|
|
78
|
-
found.set(kind + ":" + path, { kind, path, name: kind === "skills" ? basename(dirname(path)) : basename(path).replace(/\.[^.]+$/, ""),
|
|
79
|
-
scope: "cli", source, enabled: true, inherited: true, override: "inherit", trusted: true });
|
|
80
|
-
}
|
|
81
|
-
for(const pkg of packages) await addPackageResources(found,index,pkg,kind,!!disabled);
|
|
82
|
-
}
|
|
83
|
-
index.entries = [...found.values()];
|
|
84
|
-
return index;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function addPackageResources(found:Map<string,ResourceEntry>,index:ResourceIndex,pkg:PackageRoot,kind:ResourceKind,disabled:boolean):Promise<void> {
|
|
88
|
-
if(!pkg.root)return;
|
|
89
|
-
try {
|
|
90
|
-
const manifest=await packageManifest(pkg.root),patterns=manifest[kind]??[],candidates=new Set<string>();
|
|
91
|
-
for(const pattern of patterns.map(parseResourcePattern).filter(p=>p.enabled)) {
|
|
92
|
-
for(const path of await resourceFiles(resourcePath(pattern.pattern,pkg.root),kind,index.diagnostics))candidates.add(path);
|
|
93
|
-
}
|
|
94
|
-
for(const path of candidates) {
|
|
95
|
-
const match=(pattern:string,exact=false)=>exact?path===resourcePath(pattern,pkg.root!):matches(path,pattern,pkg.root!);
|
|
96
|
-
// The package manifest is the outer boundary; settings cannot expose undeclared files.
|
|
97
|
-
if(!packageResourceEnabled(patterns,match))continue;
|
|
98
|
-
const inherited=packageResourceEnabled(pkg.declaration[kind],match)&&pkg.declaration.autoload!==false;
|
|
99
|
-
const delta=packageResourceEnabled(pkg.delta?.[kind],match,inherited);
|
|
100
|
-
const global=resourceOverride(index.user[kind]??[],pattern=>matches(path,pattern,index.userDir));
|
|
101
|
-
const flat=resourceOverride(index.project[kind]??[],pattern=>matches(path,pattern,index.projectDir));
|
|
102
|
-
const local=flat!=="inherit"?flat:resourceOverride(pkg.delta?.[kind]??[],match);
|
|
103
|
-
const base=inherited&&global!=="skip",enabled=pkg.scope==="cli"||pkg.trusted&&!disabled&&(index.trusted&&local!=="inherit"?local==="load":delta&&global!=="skip");
|
|
104
|
-
if(found.get(kind+":"+path)?.scope==="cli")continue;
|
|
105
|
-
found.set(kind+":"+path,{kind,path,name:kind==="skills"?basename(dirname(path)):basename(path).replace(/\.[^.]+$/,""),scope:pkg.scope,source:pkg.source.source,
|
|
106
|
-
enabled,inherited:base,override:local,trusted:pkg.trusted,package:{source:pkg.source.source,identity:pkg.source.id,root:pkg.root}});
|
|
107
|
-
}
|
|
108
|
-
}catch(error){index.diagnostics.push(`${pkg.source.source}: ${String(error)}`);}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Iterative realpath tracking allows linked resource folders without following cycles. */
|
|
112
|
-
async function resourceFiles(source: string, kind: ResourceKind, diagnostics: string[], required = false): Promise<string[]> {
|
|
113
|
-
const files: string[] = [], seen = new Set<string>(), stack = [source];
|
|
114
|
-
if (/[*?{[]/.test(source)) {
|
|
115
|
-
try { return [...new Bun.Glob(source).scanSync({ absolute: true, onlyFiles: true })].sort(); }
|
|
116
|
-
catch (error) { diagnostics.push(`${source}: ${String(error)}`); return []; }
|
|
117
|
-
}
|
|
118
|
-
while (stack.length) {
|
|
119
|
-
const path = stack.pop()!;
|
|
120
|
-
try {
|
|
121
|
-
const canonical = await realpath(path); if (seen.has(canonical)) continue; seen.add(canonical);
|
|
122
|
-
const info = await stat(path);
|
|
123
|
-
if (info.isDirectory()) {
|
|
124
|
-
if (kind === "extensions" && path !== source) {
|
|
125
|
-
const manifest = await readJsonFile<{ xeno?: { extensions?: string[] }; pi?: { extensions?: string[] } }>(join(path, "package.json"));
|
|
126
|
-
const declared = manifest.value?.xeno?.extensions ?? manifest.value?.pi?.extensions;
|
|
127
|
-
if (declared) { for (const entry of declared) stack.push(resolve(path, entry)); continue; }
|
|
128
|
-
for (const ext of ["ts", "js", "mts", "mjs"]) {
|
|
129
|
-
const entry = join(path, `index.${ext}`);
|
|
130
|
-
if (await stat(entry).then(s => s.isFile(), () => false)) { stack.push(entry); break; }
|
|
131
|
-
}
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const entries = (await readdir(path, { withFileTypes: true })).sort((a, b) => b.name.localeCompare(a.name));
|
|
135
|
-
for (const entry of entries) if (!entry.name.startsWith(".") && entry.name !== "node_modules" && (kind === "skills" || kind === "extensions" || !entry.isDirectory())) stack.push(join(path, entry.name));
|
|
136
|
-
} else if (info.isFile() && (kind === "skills" ? basename(path) === "SKILL.md" || path === source : kind === "extensions" ? /\.[cm]?[jt]s$/.test(path) : path.endsWith(kind === "themes" ? ".json" : ".md"))) files.push(resolve(path));
|
|
137
|
-
} catch (error) { if ((error as { code?: string }).code !== "ENOENT" || required && path === source) diagnostics.push(`${path}: ${String(error)}`); }
|
|
138
|
-
}
|
|
139
|
-
return files;
|
|
140
|
-
}
|
|
141
|
-
export async function setResourceOverride(index: ResourceIndex, entry: ResourceEntry, scope: "user" | "project", state: ResourceOverride): Promise<void> {
|
|
142
|
-
if (entry.scope === "cli") throw new Error("Explicit command-line resources are controlled by their flags");
|
|
143
|
-
if (scope === "user" && entry.scope !== "user") throw new Error("Project resources are configured in project scope");
|
|
144
|
-
const base = scope === "user" ? index.userDir : index.projectDir;
|
|
145
|
-
if(entry.package) {
|
|
146
|
-
const metadata=entry.package,previous=(await configuredPackages(base)).find(value=>packageSource(packageConfig(value).source,base).id===metadata.identity);
|
|
147
|
-
const source=metadata.identity.startsWith("local:")?metadata.root:metadata.source;
|
|
148
|
-
const config=previous?{...packageConfig(previous)}:{source,...(scope==="project"&&entry.scope==="user"?{autoload:false}:{})};
|
|
149
|
-
const old=config[entry.kind],patterns=old?.length===0&&state!=="inherit"?["!**/*",...(old??[])]:old??[];
|
|
150
|
-
const selected=updateResourcePatterns(patterns,entry.path,state,pattern=>resourcePath(pattern,metadata.root)===entry.path);
|
|
151
|
-
if(selected.length)config[entry.kind]=selected;else delete config[entry.kind];
|
|
152
|
-
await savePackageDeclaration(base,packageSource(source,base),config);return;
|
|
153
|
-
}
|
|
154
|
-
const file = join(base, "settings.json");
|
|
155
|
-
await serialized(file, async () => {
|
|
156
|
-
const { value, error } = await readJsonFile<Record<string, unknown>>(file);
|
|
157
|
-
if (error) throw new Error(error);
|
|
158
|
-
if (value !== null && (typeof value !== "object" || Array.isArray(value))) throw new Error("Settings must be an object");
|
|
159
|
-
const previous = value?.[entry.kind];
|
|
160
|
-
if (previous !== undefined && (!Array.isArray(previous) || previous.some(p => typeof p !== "string"))) throw new Error("Resource paths must be strings");
|
|
161
|
-
const patterns = updateResourcePatterns((previous ?? []) as string[], entry.path, state, pattern => resourcePath(pattern, base) === entry.path);
|
|
162
|
-
await mkdir(base, { recursive: true });
|
|
163
|
-
await atomicWrite(file, JSON.stringify({ ...value, [entry.kind]: patterns }, null, 2) + "\n");
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function themeResourcePath(index: ResourceIndex, name: string): string | undefined {
|
|
168
|
-
const candidates = index.entries.filter(entry => entry.kind === "themes" && (entry.name === name || entry.path === name));
|
|
169
|
-
return candidates.findLast(entry => entry.enabled)?.path ?? (candidates.length ? "dark" : undefined);
|
|
170
|
-
}
|
package/src/store/resources.ts
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Discover and read UI resources: settings, theme, keybindings, skills,
|
|
3
|
-
* prompt templates.
|
|
4
|
-
*
|
|
5
|
-
* I/O lives here; validation is core. Reads diagnose corrupt files and fall
|
|
6
|
-
* back so startup survives a missing comma. Writes preserve them and report failure.
|
|
7
|
-
*
|
|
8
|
-
* Project-level files follow the same trust rule as `.xeno/settings.json`.
|
|
9
|
-
* Theme and keymap are UI state and are not logged.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
-
import { watch } from "node:fs";
|
|
14
|
-
import { basename, dirname, join } from "node:path";
|
|
15
|
-
import { configDir, readJsonFile } from "./config.ts";
|
|
16
|
-
import { editUserSettings } from "./user-settings.ts";
|
|
17
|
-
import { mergePreferenceJson } from "../core/model-preferences.ts";
|
|
18
|
-
import { discoverResources, themeResourcePath, type ResourceIndex } from "./resource-index.ts";
|
|
19
|
-
import { parseSkill, type Skill } from "../core/skills.ts";
|
|
20
|
-
import { parsePromptTemplate, type PromptTemplate } from "../core/prompts.ts";
|
|
21
|
-
import {
|
|
22
|
-
builtinTheme,
|
|
23
|
-
parseTheme,
|
|
24
|
-
type Theme,
|
|
25
|
-
} from "../core/theme.ts";
|
|
26
|
-
import {
|
|
27
|
-
defaultKeymap,
|
|
28
|
-
parseKeymap,
|
|
29
|
-
type Keymap,
|
|
30
|
-
} from "../core/keymap.ts";
|
|
31
|
-
import {
|
|
32
|
-
defaultSettings,
|
|
33
|
-
mergeSettings,
|
|
34
|
-
type Settings,
|
|
35
|
-
} from "../core/settings.ts";
|
|
36
|
-
|
|
37
|
-
export interface LoadUiOptions {
|
|
38
|
-
readonly workspace: string;
|
|
39
|
-
readonly trusted: boolean;
|
|
40
|
-
/** `--theme <name|path>`, wins over settings. */
|
|
41
|
-
readonly theme?: string;
|
|
42
|
-
readonly noThemes?: boolean;
|
|
43
|
-
readonly themePaths?: readonly string[];
|
|
44
|
-
readonly tuiMode?: "regular" | "fullscreen";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface UiResources {
|
|
48
|
-
readonly settings: Settings;
|
|
49
|
-
readonly theme: Theme;
|
|
50
|
-
readonly keymap: Keymap;
|
|
51
|
-
readonly diagnostics: readonly string[];
|
|
52
|
-
readonly userOverrides: Partial<Settings>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function keybindingsPath(): string {
|
|
56
|
-
return join(configDir(), "keybindings.json");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function projectKeybindingsPath(workspace: string): string {
|
|
60
|
-
return join(workspace, ".xeno", "keybindings.json");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function userThemePath(name: string): string {
|
|
64
|
-
return join(configDir(), "themes", `${name}.json`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function loadUiResources(opts: LoadUiOptions, resources?: ResourceIndex): Promise<UiResources> {
|
|
68
|
-
const index = resources ?? await discoverResources(opts);
|
|
69
|
-
const diagnostics = [...index.diagnostics];
|
|
70
|
-
const userOverrides = index.user;
|
|
71
|
-
|
|
72
|
-
const cli: Settings = { ...(opts.theme ? { theme: opts.theme } : {}), ...(opts.tuiMode ? { tuiMode: opts.tuiMode } : {}) };
|
|
73
|
-
const settings = mergeSettings({
|
|
74
|
-
builtin: defaultSettings(),
|
|
75
|
-
user: index.user,
|
|
76
|
-
project: opts.trusted ? index.project : {},
|
|
77
|
-
cli,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const theme = await resolveTheme(selectedThemePath(index, settings.theme ?? "dark", opts.noThemes), diagnostics);
|
|
81
|
-
const keymap = await resolveKeymap(opts.workspace, opts.trusted, diagnostics);
|
|
82
|
-
|
|
83
|
-
return { settings, theme, keymap, diagnostics, userOverrides };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Loading and watching share enablement; a disabled resource must not reactivate on file changes. */
|
|
87
|
-
export function selectedThemePath(index: ResourceIndex, name: string, noThemes = false): string {
|
|
88
|
-
const source = themeResourcePath(index, name);
|
|
89
|
-
const disabled = !source && (noThemes || index.entries.some(entry => entry.kind === "themes" && (entry.name === name || entry.path === name) && !entry.enabled));
|
|
90
|
-
return disabled && !["dark", "light", "system", "auto"].includes(name) ? "dark" : source ?? name;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function writeUserSettings(overrides: Partial<Settings>): Promise<void> {
|
|
94
|
-
const patch = structuredClone(overrides);
|
|
95
|
-
await editUserSettings(previous => mergePreferenceJson(previous, patch));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Remove only the user's override; project defaults and unrelated keys retain their ownership. */
|
|
99
|
-
export async function removeModelThinkingOverride(identity: string): Promise<void> {
|
|
100
|
-
await editUserSettings(previous => {
|
|
101
|
-
const levels = previous["modelThinkingLevels"];
|
|
102
|
-
if (!levels || typeof levels !== "object" || Array.isArray(levels)) return null;
|
|
103
|
-
const next = Object.fromEntries(Object.entries(levels).filter(([key]) => key !== identity));
|
|
104
|
-
return { ...previous, modelThinkingLevels: next };
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function resolveTheme(spec: string, diagnostics: string[]): Promise<Theme> {
|
|
109
|
-
if (spec === "dark" || spec === "light") return builtinTheme(spec);
|
|
110
|
-
if (spec === "system" || spec === "auto") return { ...builtinTheme("dark"), name: spec };
|
|
111
|
-
|
|
112
|
-
const path = looksLikePath(spec) ? spec : userThemePath(spec);
|
|
113
|
-
const { value, error } = await readJsonFile<unknown>(path);
|
|
114
|
-
if (error) {
|
|
115
|
-
diagnostics.push(error);
|
|
116
|
-
return builtinTheme("dark");
|
|
117
|
-
}
|
|
118
|
-
if (value === null) {
|
|
119
|
-
diagnostics.push(`theme "${spec}" not found at ${path}; using dark`);
|
|
120
|
-
return builtinTheme("dark");
|
|
121
|
-
}
|
|
122
|
-
const parsed = parseTheme(value);
|
|
123
|
-
diagnostics.push(...parsed.diagnostics);
|
|
124
|
-
return parsed.theme;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Directory watching survives atomic editor saves; malformed edits keep the last valid palette. */
|
|
128
|
-
export function watchTheme(spec: string, apply: (theme: Theme) => void, report: (message: string) => void): () => void {
|
|
129
|
-
if (["dark", "light", "system", "auto"].includes(spec)) return () => {};
|
|
130
|
-
const file = looksLikePath(spec) ? spec : userThemePath(spec);
|
|
131
|
-
let timer: ReturnType<typeof setTimeout> | undefined, closed = false;
|
|
132
|
-
const changed = () => {
|
|
133
|
-
if (timer) clearTimeout(timer);
|
|
134
|
-
timer = setTimeout(() => { void (async () => {
|
|
135
|
-
const diagnostics: string[] = [];
|
|
136
|
-
const theme = await resolveTheme(spec, diagnostics);
|
|
137
|
-
if (closed) return;
|
|
138
|
-
if (diagnostics.length) report(diagnostics.join("\n")); else apply(theme);
|
|
139
|
-
})(); }, 80);
|
|
140
|
-
};
|
|
141
|
-
try {
|
|
142
|
-
const watcher = watch(dirname(file), { persistent: false }, (_event, name) => { if (!name || String(name) === basename(file)) changed(); });
|
|
143
|
-
watcher.on("error", error => { if (!closed) report(String(error)); });
|
|
144
|
-
return () => { closed = true; if (timer) clearTimeout(timer); watcher.close(); };
|
|
145
|
-
} catch (error) { report(String(error)); return () => { closed = true; }; }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export async function discoverThemes(index?: ResourceIndex): Promise<string[]> {
|
|
149
|
-
if (index) return [...new Set(["dark", "light", "system", "auto", ...index.entries.filter(entry => entry.kind === "themes" && entry.enabled).map(entry => entry.name)])];
|
|
150
|
-
let names: string[] = [];
|
|
151
|
-
try { names = (await readdir(join(configDir(), "themes"))).filter(name => name.endsWith(".json")).map(name => name.slice(0, -5)).sort(); }
|
|
152
|
-
catch (error) { if (!isNotFound(error)) throw error; }
|
|
153
|
-
return [...new Set(["dark", "light", "system", "auto", ...names])];
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function resolveKeymap(workspace: string, trusted: boolean, diagnostics: string[]): Promise<Keymap> {
|
|
157
|
-
let map = defaultKeymap(process.platform === "win32" ? "win32" : process.env["WSL_DISTRO_NAME"] || process.env["WSL_INTEROP"] ? "wsl" : "posix");
|
|
158
|
-
|
|
159
|
-
const userRead = await readJsonFile<unknown>(keybindingsPath());
|
|
160
|
-
if (userRead.error) diagnostics.push(userRead.error);
|
|
161
|
-
if (userRead.value !== null) {
|
|
162
|
-
map = parseKeymap(userRead.value, map);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (trusted) {
|
|
166
|
-
const projectRead = await readJsonFile<unknown>(projectKeybindingsPath(workspace));
|
|
167
|
-
if (projectRead.error) diagnostics.push(projectRead.error);
|
|
168
|
-
if (projectRead.value !== null) {
|
|
169
|
-
map = parseKeymap(projectRead.value, map);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
diagnostics.push(...map.diagnostics);
|
|
174
|
-
return map;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function looksLikePath(spec: string): boolean {
|
|
178
|
-
return spec.includes("/") || spec.includes("\\") || spec.endsWith(".json");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export interface LoadTextOptions {
|
|
182
|
-
readonly workspace: string;
|
|
183
|
-
readonly configDir: string;
|
|
184
|
-
readonly trusted: boolean;
|
|
185
|
-
readonly noSkills?: boolean;
|
|
186
|
-
readonly noPromptTemplates?: boolean;
|
|
187
|
-
/** `--skill <path>`, loaded even when `noSkills` is set. */
|
|
188
|
-
readonly skillPaths?: readonly string[];
|
|
189
|
-
/** `--prompt-template <path>`, loaded even when discovery is off. */
|
|
190
|
-
readonly promptTemplatePaths?: readonly string[];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface LoadedTextResources {
|
|
194
|
-
readonly skills: readonly Skill[];
|
|
195
|
-
readonly prompts: readonly PromptTemplate[];
|
|
196
|
-
readonly diagnostics: readonly string[];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Discover skills and prompt templates. Project directories load only when
|
|
201
|
-
* trusted, matching `.xeno/settings.json`. A bad file is named and skipped.
|
|
202
|
-
*/
|
|
203
|
-
export async function loadTextResources(opts: LoadTextOptions, resources?: ResourceIndex): Promise<LoadedTextResources> {
|
|
204
|
-
const diagnostics: string[] = [];
|
|
205
|
-
const skills = new Map<string, Skill>();
|
|
206
|
-
const prompts = new Map<string, PromptTemplate>();
|
|
207
|
-
|
|
208
|
-
const addSkill = (skill: Skill): void => {
|
|
209
|
-
const prev = skills.get(skill.name);
|
|
210
|
-
if (prev !== undefined) {
|
|
211
|
-
diagnostics.push(
|
|
212
|
-
`skill "${skill.name}" from ${skill.filePath} replaces ${prev.filePath}`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
skills.set(skill.name, skill);
|
|
216
|
-
};
|
|
217
|
-
const addPrompt = (prompt: PromptTemplate): void => {
|
|
218
|
-
const prev = prompts.get(prompt.name);
|
|
219
|
-
if (prev !== undefined) {
|
|
220
|
-
diagnostics.push(
|
|
221
|
-
`prompt template "${prompt.name}" from ${prompt.filePath} replaces ${prev.filePath}`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
prompts.set(prompt.name, prompt);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const index = resources ?? await discoverResources(opts);
|
|
228
|
-
diagnostics.push(...index.diagnostics);
|
|
229
|
-
for (const entry of index.entries) if (entry.enabled) {
|
|
230
|
-
if (entry.kind === "skills") await absorbSkill(entry.path, addSkill, diagnostics);
|
|
231
|
-
if (entry.kind === "prompts") await absorbPrompt(entry.path, addPrompt, diagnostics);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
skills: [...skills.values()],
|
|
236
|
-
prompts: [...prompts.values()],
|
|
237
|
-
diagnostics,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function absorbSkill(
|
|
242
|
-
filePath: string,
|
|
243
|
-
add: (skill: Skill) => void,
|
|
244
|
-
diagnostics: string[],
|
|
245
|
-
): Promise<void> {
|
|
246
|
-
const text = await readUtf8(filePath);
|
|
247
|
-
if (text === null) {
|
|
248
|
-
diagnostics.push(`${filePath}: could not read`);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const parsed = parseSkill({
|
|
252
|
-
filePath,
|
|
253
|
-
parentDirName: basename(dirname(filePath)),
|
|
254
|
-
text,
|
|
255
|
-
});
|
|
256
|
-
diagnostics.push(...parsed.diagnostics);
|
|
257
|
-
if (parsed.skill) add(parsed.skill);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function absorbPrompt(
|
|
261
|
-
filePath: string,
|
|
262
|
-
add: (prompt: PromptTemplate) => void,
|
|
263
|
-
diagnostics: string[],
|
|
264
|
-
): Promise<void> {
|
|
265
|
-
const text = await readUtf8(filePath);
|
|
266
|
-
if (text === null) {
|
|
267
|
-
diagnostics.push(`${filePath}: could not read`);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const parsed = parsePromptTemplate({ filePath, text });
|
|
271
|
-
diagnostics.push(...parsed.diagnostics);
|
|
272
|
-
if (parsed.template) add(parsed.template);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function readUtf8(path: string): Promise<string | null> {
|
|
276
|
-
try {
|
|
277
|
-
return await readFile(path, "utf8");
|
|
278
|
-
} catch {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function isNotFound(err: unknown): boolean {
|
|
284
|
-
return (err as { code?: string }).code === "ENOENT";
|
|
285
|
-
}
|