@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
package/src/store/trust.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project trust: whether to load `.xeno/settings.json` for a directory.
|
|
3
|
-
*
|
|
4
|
-
* Trust does not affect tool execution. Path tools stay confined to the
|
|
5
|
-
* workspace root regardless, and bash still goes through the permission
|
|
6
|
-
* gate. This file only records which directories may contribute project
|
|
7
|
-
* settings. AGENTS.md is loaded whether the project is trusted or not.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { dirname, join, resolve } from "node:path";
|
|
11
|
-
import { configDir, readJsonFile, writeJsonFile } from "./config.ts";
|
|
12
|
-
import { parseSettings } from "../core/settings.ts";
|
|
13
|
-
|
|
14
|
-
export type TrustFile = { readonly [dir: string]: boolean };
|
|
15
|
-
|
|
16
|
-
export function trustPath(): string {
|
|
17
|
-
return join(configDir(), "trust.json");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function projectSettingsPath(workspace: string): string {
|
|
21
|
-
return join(workspace, ".xeno", "settings.json");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Closest saved decision on `dir` or a parent. Undefined if none.
|
|
26
|
-
*
|
|
27
|
-
* A parent applies to its children so trusting a repo root covers the
|
|
28
|
-
* packages underneath; a closer entry wins so a single untrusted nested
|
|
29
|
-
* project can opt out.
|
|
30
|
-
*/
|
|
31
|
-
export function lookupTrust(decisions: TrustFile, dir: string): boolean | undefined {
|
|
32
|
-
let current = resolve(dir);
|
|
33
|
-
for (;;) {
|
|
34
|
-
const value = decisions[current];
|
|
35
|
-
if (value !== undefined) return value;
|
|
36
|
-
const parent = dirname(current);
|
|
37
|
-
if (parent === current) return undefined;
|
|
38
|
-
current = parent;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function projectTrust(workspace: string, temporary?: boolean): Promise<{ trusted: boolean; saved?: { path: string; trusted: boolean }; policy: "ask" | "always" | "never" }> {
|
|
43
|
-
const [saved, settings] = await Promise.all([readTrust(), readJsonFile<unknown>(join(configDir(), "settings.json"))]);
|
|
44
|
-
const policy = parseSettings(settings.value).settings.defaultProjectTrust ?? "ask";
|
|
45
|
-
let path = resolve(workspace);
|
|
46
|
-
for (;;) {
|
|
47
|
-
const decision = saved.value[path];
|
|
48
|
-
if (decision !== undefined) return { trusted: temporary ?? decision, saved: { path, trusted: decision }, policy };
|
|
49
|
-
const parent = dirname(path); if (parent === path) break; path = parent;
|
|
50
|
-
}
|
|
51
|
-
return { trusted: temporary ?? policy === "always", policy };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function readTrust(): Promise<{ value: TrustFile; error: string | null }> {
|
|
55
|
-
const { value, error } = await readJsonFile<unknown>(trustPath());
|
|
56
|
-
if (error) return { value: {}, error };
|
|
57
|
-
if (value === null) return { value: {}, error: null };
|
|
58
|
-
if (!isTrustFile(value)) {
|
|
59
|
-
return { value: {}, error: `${trustPath()} is not a directory-to-boolean map` };
|
|
60
|
-
}
|
|
61
|
-
return { value, error: null };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function writeTrust(decisions: TrustFile): Promise<void> {
|
|
65
|
-
await writeJsonFile(trustPath(), decisions, false);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function saveTrust(dir: string, trusted: boolean): Promise<void> {
|
|
69
|
-
const { value } = await readTrust();
|
|
70
|
-
const next: { [dir: string]: boolean } = { ...value };
|
|
71
|
-
next[resolve(dir)] = trusted;
|
|
72
|
-
await writeTrust(next);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isTrustFile(value: unknown): value is TrustFile {
|
|
76
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
77
|
-
const rec = value as { readonly [k: string]: unknown };
|
|
78
|
-
for (const key of Object.keys(rec)) {
|
|
79
|
-
if (typeof rec[key] !== "boolean") return false;
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The last update check, kept in the config directory (docs/tasks/feat-update-check.md).
|
|
3
|
-
*
|
|
4
|
-
* `{ checkedAt, latest? }` and nothing else: no identifiers, no history. A
|
|
5
|
-
* missing or unreadable file means "never checked"; it is rewritten whole.
|
|
6
|
-
*/
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import type { UpdateCache } from "../core/update-check.ts";
|
|
12
|
-
import { atomicWrite } from "./atomic.ts";
|
|
13
|
-
import { configDir } from "./config.ts";
|
|
14
|
-
|
|
15
|
-
export function updateCachePath(): string { return join(configDir(), "update-check.json"); }
|
|
16
|
-
|
|
17
|
-
export async function readUpdateCache(): Promise<UpdateCache | null> {
|
|
18
|
-
try {
|
|
19
|
-
const value = JSON.parse(await readFile(updateCachePath(), "utf8")) as Record<string, unknown>;
|
|
20
|
-
if (typeof value["checkedAt"] !== "number") return null;
|
|
21
|
-
return { checkedAt: value["checkedAt"], ...(typeof value["latest"] === "string" ? { latest: value["latest"] } : {}) };
|
|
22
|
-
} catch { return null; }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function writeUpdateCache(cache: UpdateCache): Promise<void> {
|
|
26
|
-
try { await atomicWrite(updateCachePath(), JSON.stringify(cache) + "\n"); } catch { /* a read-only config dir only means checking again next time */ }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** This copy's package root, resolved through symlinks, and whether it is a git checkout. */
|
|
30
|
-
export function packageLocation(): { root: string; git: boolean } {
|
|
31
|
-
// src/store/update-cache.ts → the package root two directories up.
|
|
32
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
-
let root = join(here, "..", "..");
|
|
34
|
-
try { root = realpathSync(root); } catch { /* keep the unresolved path */ }
|
|
35
|
-
return { root, git: existsSync(join(root, ".git")) };
|
|
36
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/** One read/modify/replace owner prevents automatic selections from racing other settings (Q.25). */
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { settingsPath } from "./config.ts";
|
|
4
|
-
import { atomicWrite } from "./atomic.ts";
|
|
5
|
-
import { serialized } from "./serial.ts";
|
|
6
|
-
|
|
7
|
-
async function readSettings(path: string): Promise<Record<string, unknown>> {
|
|
8
|
-
let text: string;
|
|
9
|
-
try { text = await readFile(path, "utf8"); }
|
|
10
|
-
catch (error) {
|
|
11
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return {};
|
|
12
|
-
throw error;
|
|
13
|
-
}
|
|
14
|
-
let value: unknown;
|
|
15
|
-
try { value = JSON.parse(text); }
|
|
16
|
-
catch { throw new Error(`${path} is not valid JSON; repair it before saving settings`); }
|
|
17
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must contain a settings object`);
|
|
18
|
-
return value as Record<string, unknown>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function editUserSettings(edit: (previous: Record<string, unknown>) => Record<string, unknown> | null): Promise<void> {
|
|
22
|
-
// Capture the destination before any await, including when another profile changes configDir().
|
|
23
|
-
const path = settingsPath();
|
|
24
|
-
// Packages and resource overrides already coordinate this file through serialized().
|
|
25
|
-
return serialized(path, async () => {
|
|
26
|
-
const next = edit(await readSettings(path));
|
|
27
|
-
if (next === null) return;
|
|
28
|
-
await atomicWrite(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A disposable copy of a workspace, for running a check somewhere it cannot
|
|
3
|
-
* touch the real files (feat-counter-check).
|
|
4
|
-
*
|
|
5
|
-
* Copy-on-write where the filesystem offers it -- APFS clonefile through
|
|
6
|
-
* `cp -c`, reflinks through `cp --reflink=auto` -- so a workspace with large
|
|
7
|
-
* dependency trees costs metadata, not bytes. Where it does not, cp falls back
|
|
8
|
-
* to a real copy, bounded by the deadline below.
|
|
9
|
-
*/
|
|
10
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Assumed, not measured: a clone is metadata-only on APFS and reflink
|
|
16
|
-
* filesystems; a full copy that takes longer than this is better reported as
|
|
17
|
-
* unavailable than waited on.
|
|
18
|
-
*/
|
|
19
|
-
export const CLONE_TIMEOUT_MS = 60_000;
|
|
20
|
-
|
|
21
|
-
export type WorkspaceClone = { ok: true; path: string; dispose: () => Promise<void> } | { ok: false; reason: string };
|
|
22
|
-
|
|
23
|
-
export async function cloneWorkspace(root: string, signal?: AbortSignal): Promise<WorkspaceClone> {
|
|
24
|
-
const parent = await mkdtemp(join(tmpdir(), "xeno-counter-check-")), path = join(parent, "workspace");
|
|
25
|
-
const dispose = () => rm(parent, { recursive: true, force: true });
|
|
26
|
-
const attempts = process.platform === "darwin" ? [["cp", "-cR", root, path], ["cp", "-R", root, path]] : [["cp", "-a", "--reflink=auto", root, path]];
|
|
27
|
-
let reason = "the workspace could not be copied";
|
|
28
|
-
for (const argv of attempts) {
|
|
29
|
-
if (signal?.aborted) { await dispose(); return { ok: false, reason: "cancelled" }; }
|
|
30
|
-
await rm(path, { recursive: true, force: true });
|
|
31
|
-
const child = Bun.spawn(argv, { stdout: "ignore", stderr: "pipe" });
|
|
32
|
-
const timer = setTimeout(() => child.kill(), CLONE_TIMEOUT_MS);
|
|
33
|
-
const abort = () => child.kill();
|
|
34
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
35
|
-
const [exit, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
36
|
-
clearTimeout(timer); signal?.removeEventListener("abort", abort);
|
|
37
|
-
if (exit === 0) return { ok: true, path, dispose };
|
|
38
|
-
reason = child.signalCode ? `copying the workspace exceeded ${CLONE_TIMEOUT_MS / 1000}s` : `the workspace could not be copied: ${stderr.trim().split("\n").at(-1) ?? `exit ${exit}`}`;
|
|
39
|
-
}
|
|
40
|
-
await dispose();
|
|
41
|
-
return { ok: false, reason };
|
|
42
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/** P.3: explicit source inventory for verification and optional Git checkpoints. */
|
|
2
|
-
import { readdir, lstat } from "node:fs/promises";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
4
|
-
import { blobAddress, digestFile } from "./blobs.ts";
|
|
5
|
-
|
|
6
|
-
// Conservative resource budgets, not measured performance targets. Exceeding them is unknown, never an empty workspace.
|
|
7
|
-
const MAX_FILES = 20_000;
|
|
8
|
-
const MAX_BYTES = 256 * 1024 * 1024;
|
|
9
|
-
export const MAX_UNTRACKED_BYTES = 2 * 1024 * 1024;
|
|
10
|
-
export const SOURCE_IGNORE = [".git", ".xeno", "node_modules", "target", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox", ".gradle", ".next", ".turbo", ".cache"];
|
|
11
|
-
const ignored = (path: string) => path.split("/").some(part => SOURCE_IGNORE.includes(part));
|
|
12
|
-
|
|
13
|
-
/** Bounded subprocess; private-repository callers override only Git's storage, never the user's index. */
|
|
14
|
-
export async function gitBytes(root: string, args: readonly string[], input?: Uint8Array, signal?: AbortSignal,
|
|
15
|
-
environment: Record<string, string> = {}): Promise<Uint8Array> {
|
|
16
|
-
if (signal?.aborted) throw new Error("Git observation cancelled");
|
|
17
|
-
const env: Record<string, string | undefined> = { ...process.env, GIT_OPTIONAL_LOCKS: "0", ...environment };
|
|
18
|
-
for (const key of ["GIT_INDEX_FILE", "GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"]) {
|
|
19
|
-
if (!(key in environment)) delete env[key];
|
|
20
|
-
}
|
|
21
|
-
const child = Bun.spawn(["git", "--no-optional-locks", ...args], { cwd: root, env, stdin: input ?? "ignore", stdout: "pipe", stderr: "pipe" });
|
|
22
|
-
const abort = () => child.kill();
|
|
23
|
-
const timer = setTimeout(abort, 10_000);
|
|
24
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
25
|
-
const read = async (stream: ReadableStream<Uint8Array>) => {
|
|
26
|
-
const reader = stream.getReader(), chunks: Uint8Array[] = [];
|
|
27
|
-
let bytes = 0;
|
|
28
|
-
for (;;) {
|
|
29
|
-
const item = await reader.read();
|
|
30
|
-
if (item.done) break;
|
|
31
|
-
bytes += item.value.length;
|
|
32
|
-
if (bytes > MAX_BYTES) { child.kill(); throw new Error("Git observation exceeds resource budget"); }
|
|
33
|
-
chunks.push(item.value);
|
|
34
|
-
}
|
|
35
|
-
return Buffer.concat(chunks);
|
|
36
|
-
};
|
|
37
|
-
try {
|
|
38
|
-
const [stdout, , code] = await Promise.all([read(child.stdout), read(child.stderr), child.exited]);
|
|
39
|
-
if (code !== 0 || signal?.aborted) throw new Error("Git observation unavailable or cancelled");
|
|
40
|
-
return stdout;
|
|
41
|
-
} finally { clearTimeout(timer); signal?.removeEventListener("abort", abort); }
|
|
42
|
-
}
|
|
43
|
-
const text = (bytes: Uint8Array) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
44
|
-
|
|
45
|
-
export async function sourceInventory(root: string, signal?: AbortSignal, requireGit = false, ownedPaths: readonly string[] = []): Promise<{ paths: string[]; git: boolean }> {
|
|
46
|
-
const owned = ownedPaths.map(path => relative(resolve(root), resolve(path))).filter(path => path && !path.startsWith("../"));
|
|
47
|
-
const excluded = (path: string) => ignored(path) || owned.some(directory => path === directory || path.startsWith(directory + "/"));
|
|
48
|
-
let git = false;
|
|
49
|
-
try { await gitBytes(root, ["rev-parse", "--show-toplevel"], undefined, signal); git = true; }
|
|
50
|
-
catch { if (requireGit || signal?.aborted) throw new Error("Git checkpoints require a Git working directory"); }
|
|
51
|
-
let candidates: { path: string; tracked: boolean }[] = [];
|
|
52
|
-
if (git) {
|
|
53
|
-
const tracked = text(await gitBytes(root, ["ls-files", "-z", "--cached"], undefined, signal)).split("\0").filter(Boolean);
|
|
54
|
-
const untracked = text(await gitBytes(root, ["ls-files", "-z", "--others", "--exclude-standard"], undefined, signal)).split("\0").filter(Boolean);
|
|
55
|
-
candidates = [...new Set(tracked)].map(path => ({ path, tracked: true }));
|
|
56
|
-
const known = new Set(tracked);
|
|
57
|
-
candidates.push(...untracked.filter(path => !known.has(path)).map(path => ({ path, tracked: false })));
|
|
58
|
-
} else {
|
|
59
|
-
let entries = 0;
|
|
60
|
-
const walk = async (directory: string, prefix: string): Promise<void> => {
|
|
61
|
-
if (signal?.aborted) throw new Error("workspace observation cancelled");
|
|
62
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
63
|
-
if (++entries > MAX_FILES) throw new Error("source inventory exceeds file budget");
|
|
64
|
-
const path = prefix + entry.name;
|
|
65
|
-
if (excluded(path)) continue;
|
|
66
|
-
if (entry.isDirectory()) await walk(join(directory, entry.name), path + "/");
|
|
67
|
-
else if (entry.isFile()) candidates.push({ path, tracked: true });
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
await walk(root, "");
|
|
71
|
-
}
|
|
72
|
-
candidates = candidates.filter(candidate => !excluded(candidate.path));
|
|
73
|
-
if (candidates.length > MAX_FILES) throw new Error("source inventory exceeds file budget");
|
|
74
|
-
const paths: string[] = [];
|
|
75
|
-
let bytes = 0;
|
|
76
|
-
for (const candidate of candidates.sort((a, b) => a.path < b.path ? -1 : 1)) {
|
|
77
|
-
if (signal?.aborted) throw new Error("workspace observation cancelled");
|
|
78
|
-
const path = candidate.path;
|
|
79
|
-
if (excluded(path)) continue;
|
|
80
|
-
const parts = path.split("/");
|
|
81
|
-
if (parts.includes("..") || path.startsWith("/")) throw new Error("unsafe source inventory");
|
|
82
|
-
let safe = true;
|
|
83
|
-
for (let i = 1; i <= parts.length; i++) {
|
|
84
|
-
try {
|
|
85
|
-
const info = await lstat(join(root, ...parts.slice(0, i)));
|
|
86
|
-
if (info.isSymbolicLink() || i === parts.length && !info.isFile()) { safe = false; break; }
|
|
87
|
-
if (i === parts.length) {
|
|
88
|
-
if (!candidate.tracked && info.size > MAX_UNTRACKED_BYTES) { safe = false; break; }
|
|
89
|
-
bytes += info.size;
|
|
90
|
-
}
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if ((error as { code?: string }).code !== "ENOENT") throw error;
|
|
93
|
-
safe = false; break;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (bytes > MAX_BYTES) throw new Error("source inventory exceeds byte budget");
|
|
97
|
-
if (safe) paths.push(path);
|
|
98
|
-
}
|
|
99
|
-
return { paths, git };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type WorkspaceObservation = { ok: true; fingerprint: string } | { ok: false; reason: string };
|
|
103
|
-
export async function observeWorkspace(root: string, signal?: AbortSignal, ownedPaths: readonly string[] = []): Promise<WorkspaceObservation> {
|
|
104
|
-
try {
|
|
105
|
-
const inventory = await sourceInventory(root, signal, false, ownedPaths);
|
|
106
|
-
const entries: [string, string, number][] = [];
|
|
107
|
-
for (const path of inventory.paths) {
|
|
108
|
-
if (signal?.aborted) throw new Error("workspace observation cancelled");
|
|
109
|
-
entries.push([path, await digestFile(join(root, path)), (await lstat(join(root, path))).mode & 0o777]);
|
|
110
|
-
}
|
|
111
|
-
return { ok: true, fingerprint: blobAddress(JSON.stringify(["source-v1", inventory.git ? "git" : "files", entries])) };
|
|
112
|
-
} catch (error) { return { ok: false, reason: error instanceof Error ? error.message : "workspace observation unavailable" }; }
|
|
113
|
-
}
|
package/src/store/workspace.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A bounded content scan of a workspace: relative path -> digest, and one
|
|
3
|
-
* version digest over the whole set.
|
|
4
|
-
*
|
|
5
|
-
* Session file history no longer comes from per-turn scans. P.3 replaced them
|
|
6
|
-
* with journaled checkpoints and source fingerprints (workspace-observation.ts,
|
|
7
|
-
* WorkspaceLifecycle), and S.3 removed the compatibility mode that still
|
|
8
|
-
* scanned. What remains is the scanner itself and the default ignore list the
|
|
9
|
-
* search and file index share.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readdir, stat } from "node:fs/promises";
|
|
13
|
-
import type { Dirent } from "node:fs";
|
|
14
|
-
import { join, relative, sep } from "node:path";
|
|
15
|
-
import { blobAddress, digestFile } from "./blobs.ts";
|
|
16
|
-
|
|
17
|
-
export interface ScanLimits {
|
|
18
|
-
maxFiles: number;
|
|
19
|
-
maxFileBytes: number;
|
|
20
|
-
maxTotalBytes: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Hashing and blob capture stream regular files, so file size has no default
|
|
25
|
-
* product cap. Callers may impose explicit resource limits. Exceeding one,
|
|
26
|
-
* losing a directory, or meeting an unsupported file type fails the whole scan;
|
|
27
|
-
* a partial listing could make a restore delete files it never recorded.
|
|
28
|
-
*/
|
|
29
|
-
export const DEFAULT_SCAN_LIMITS: ScanLimits = {
|
|
30
|
-
maxFiles: Infinity,
|
|
31
|
-
maxFileBytes: Infinity,
|
|
32
|
-
maxTotalBytes: Infinity,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Directories never scanned, snapshotted or restored.
|
|
37
|
-
*
|
|
38
|
-
* One constant rather than a default repeated at each call site: the snapshot
|
|
39
|
-
* and the restore have to agree exactly. A path the snapshot skipped is absent
|
|
40
|
-
* from every target file set, and applyRestore deletes whatever is not in the
|
|
41
|
-
* target -- so a restore using a shorter list than the snapshot would delete
|
|
42
|
-
* the log, the git directory, or the user's dependencies.
|
|
43
|
-
*/
|
|
44
|
-
export const DEFAULT_IGNORE: readonly string[] = [".xeno", ".git", "node_modules"];
|
|
45
|
-
|
|
46
|
-
/** Relative path -> content digest. Same address space as the blob store. */
|
|
47
|
-
export type FileSet = Record<string, string>;
|
|
48
|
-
|
|
49
|
-
export function workspaceVersion(files: FileSet): string {
|
|
50
|
-
return blobAddress(JSON.stringify(Object.entries(files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type ScanResult =
|
|
54
|
-
| { ok: true; files: FileSet }
|
|
55
|
-
| { ok: false; error: string };
|
|
56
|
-
|
|
57
|
-
function isIgnored(rel: string, ignore: readonly string[]): boolean {
|
|
58
|
-
const parts = rel.split(sep);
|
|
59
|
-
return ignore.some((i) => parts.includes(i));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function scanWorkspace(
|
|
63
|
-
root: string,
|
|
64
|
-
ignore: readonly string[],
|
|
65
|
-
limits: ScanLimits = DEFAULT_SCAN_LIMITS,
|
|
66
|
-
): Promise<ScanResult> {
|
|
67
|
-
const files: FileSet = {};
|
|
68
|
-
let count = 0;
|
|
69
|
-
let total = 0;
|
|
70
|
-
|
|
71
|
-
async function walk(dir: string): Promise<string | null> {
|
|
72
|
-
let entries: Dirent[];
|
|
73
|
-
try {
|
|
74
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
75
|
-
} catch (err) {
|
|
76
|
-
return `cannot scan ${dir}: ${String(err)}`;
|
|
77
|
-
}
|
|
78
|
-
for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
79
|
-
const abs = join(dir, entry.name);
|
|
80
|
-
const rel = relative(root, abs);
|
|
81
|
-
if (isIgnored(rel, ignore)) continue;
|
|
82
|
-
if (entry.isDirectory()) {
|
|
83
|
-
const err = await walk(abs);
|
|
84
|
-
if (err) return err;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
// A symlink is not workspace content: it can point anywhere, and search
|
|
88
|
-
// and the file index already refuse to follow one. It is left out of the
|
|
89
|
-
// file set, so snapshots never read through it and a restore never
|
|
90
|
-
// deletes or replaces it (/back does not restore symlinks). Anything
|
|
91
|
-
// else that is not a regular file still fails the whole scan.
|
|
92
|
-
if (entry.isSymbolicLink()) continue;
|
|
93
|
-
if (!entry.isFile()) return `cannot snapshot unsupported file type: ${rel}`;
|
|
94
|
-
|
|
95
|
-
const info = await stat(abs);
|
|
96
|
-
if (info.size > limits.maxFileBytes) {
|
|
97
|
-
return `${rel} is ${info.size} bytes, over maxFileBytes (${limits.maxFileBytes})`;
|
|
98
|
-
}
|
|
99
|
-
if (++count > limits.maxFiles) {
|
|
100
|
-
return `workspace has more than maxFiles (${limits.maxFiles}) files`;
|
|
101
|
-
}
|
|
102
|
-
total += info.size;
|
|
103
|
-
if (total > limits.maxTotalBytes) {
|
|
104
|
-
return `workspace exceeds maxTotalBytes (${limits.maxTotalBytes})`;
|
|
105
|
-
}
|
|
106
|
-
files[rel.split(sep).join("/")] = await digestFile(abs);
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const error = await walk(root);
|
|
113
|
-
return error ? { ok: false, error } : { ok: true, files };
|
|
114
|
-
} catch (error) { return { ok: false, error: `incomplete workspace scan: ${String(error)}` }; }
|
|
115
|
-
}
|