@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/core/tool-failure.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/** R3 / D11: failure policy follows observed execution facts, never output keywords. */
|
|
2
|
-
import type { ToolDiagnostic } from "./tool-contract.ts";
|
|
3
|
-
|
|
4
|
-
export type FailureFacts = { execution: "not_started" | "started" | "unknown"; effects: "none" | "possible" };
|
|
5
|
-
export const NOT_STARTED: FailureFacts = { execution: "not_started", effects: "none" };
|
|
6
|
-
export const READ_ATTEMPTED: FailureFacts = { execution: "started", effects: "none" };
|
|
7
|
-
export const UNKNOWN_EFFECT: FailureFacts = { execution: "unknown", effects: "possible" };
|
|
8
|
-
|
|
9
|
-
const guidance: Record<ToolDiagnostic["code"], string> = {
|
|
10
|
-
invalid_input: "Correct the named argument using the tool schema; example values are placeholders.",
|
|
11
|
-
version_mismatch: "Read the current resource and derive a new edit from that version.",
|
|
12
|
-
not_found: "Verify the resource path or identifier before changing the request.",
|
|
13
|
-
ambiguous_edit: "Read the indicated range and choose a unique match before editing.",
|
|
14
|
-
permission_denied: "This operation was denied. Do not bypass it through another tool or wrapper; a user policy change is required.",
|
|
15
|
-
execution_failed: "Inspect the failure and change its cause before retrying.",
|
|
16
|
-
environment_missing: "Verify the working directory and required executable in this environment before retrying. If available and permitted, use environment with the exact program; use its python probe for actual Python API support.",
|
|
17
|
-
transient: "Retry with bounded backoff only if no side effect is possible.",
|
|
18
|
-
cancelled: "Respect cancellation; inspect any partial effects before a user-authorized continuation.",
|
|
19
|
-
timeout: "Inspect partial output and effects before deciding whether a new invocation is needed.",
|
|
20
|
-
unknown_effect: "Execution or effects are uncertain. Inspect existing state and output before any replay.",
|
|
21
|
-
command_failed: "Use the actual exit and output to diagnose the command; a nonzero exit alone does not identify its cause.",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The refusal for `.git` and `.xeno` paths. It is a fixed rule protecting the
|
|
26
|
-
* session's own evidence, not a user policy, so the generic denial ("a user
|
|
27
|
-
* policy change is required") would send the model to ask for a setting that
|
|
28
|
-
* does not exist -- which is what a real session did before this text.
|
|
29
|
-
*/
|
|
30
|
-
/*
|
|
31
|
-
* The diagnostic code stays permission_denied because codes are a closed set
|
|
32
|
-
* validated on every logged tool_result (event-validation.ts); a new code
|
|
33
|
-
* would change the session format. A model still read that code as "wait for
|
|
34
|
-
* approval" and stopped (Horizyn mv3nsske), so the text has to say outright
|
|
35
|
-
* that approval is neither needed nor able to help.
|
|
36
|
-
*/
|
|
37
|
-
const STATE_PATH_RULE = "This path is repository or xeno session state, which edit tools never change. " +
|
|
38
|
-
"This is not a permission problem and no setting changes this: no user approval allows it, so do not wait for one. ";
|
|
39
|
-
export const STATE_PATH_GUIDANCE = STATE_PATH_RULE +
|
|
40
|
-
"To work in an isolated checkout, use the worktree tool instead of creating one under .git or .xeno.";
|
|
41
|
-
const STATE_PATH_GUIDANCE_WITHOUT_WORKTREE = STATE_PATH_RULE + "Edit files in a directory outside .git and .xeno.";
|
|
42
|
-
|
|
43
|
-
export function failureDiagnostic(code: ToolDiagnostic["code"], message: string, facts: FailureFacts,
|
|
44
|
-
extra: Pick<ToolDiagnostic, "recover" | "example" | "next"> = {}): ToolDiagnostic {
|
|
45
|
-
const retry = code === "permission_denied" || code === "cancelled" ? "never"
|
|
46
|
-
: code === "unknown_effect" || facts.effects === "possible" ? "after_inspection"
|
|
47
|
-
: code === "transient" ? "backoff" : "after_change";
|
|
48
|
-
const { next = guidance[code], ...rest } = extra;
|
|
49
|
-
return { code, message, ...facts, retry, next, ...(code === "permission_denied" ? {} : rest) };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** R10: only new advice is scoped; execution facts and historical records are never rewritten. */
|
|
53
|
-
export function scopeFailureGuidance(diagnostic: ToolDiagnostic, tools: readonly string[], recoverAllowed = true): ToolDiagnostic {
|
|
54
|
-
const { recover, ...facts } = diagnostic;
|
|
55
|
-
const next = diagnostic.execution !== undefined && diagnostic.code === "environment_missing" && !tools.includes("environment")
|
|
56
|
-
? { next: "Verify the working directory and required executable in the current executor using permitted observations. If the needed capability is unavailable, report that evidence gap before retrying." }
|
|
57
|
-
: diagnostic.next === STATE_PATH_GUIDANCE && !tools.includes("worktree") ? { next: STATE_PATH_GUIDANCE_WITHOUT_WORKTREE } : {};
|
|
58
|
-
return { ...facts, ...next, ...(recover && recoverAllowed && tools.includes(recover.tool) && diagnostic.code !== "permission_denied" ? { recover } : {}) };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Runtime and store throw only facts they own; the outer boundary must retain these facts. */
|
|
62
|
-
export class ToolFault extends Error {
|
|
63
|
-
readonly diagnostic: ToolDiagnostic;
|
|
64
|
-
constructor(code: ToolDiagnostic["code"], message: string, facts: FailureFacts, extra: Pick<ToolDiagnostic, "recover" | "next"> = {}) {
|
|
65
|
-
super(message); this.name = "ToolFault";
|
|
66
|
-
this.diagnostic = failureDiagnostic(code, message, facts, extra);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** OS codes are facts; words like "denied" in arbitrary output are not classification evidence. */
|
|
71
|
-
export function failureFromError(error: unknown, facts: FailureFacts): ToolDiagnostic {
|
|
72
|
-
if (error instanceof ToolFault) return error.diagnostic;
|
|
73
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
-
const os = error !== null && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
75
|
-
const code = os === "ENOENT" || os === "ENOTDIR" ? "not_found"
|
|
76
|
-
: os === "EACCES" || os === "EPERM" ? "permission_denied"
|
|
77
|
-
: os === "EISDIR" ? "invalid_input"
|
|
78
|
-
: ["EAGAIN", "ETIMEDOUT", "ECONNRESET", "EMFILE"].includes(String(os)) ? "transient"
|
|
79
|
-
: facts.effects === "possible" ? "unknown_effect" : "execution_failed";
|
|
80
|
-
return failureDiagnostic(code, message, facts);
|
|
81
|
-
}
|
package/src/core/tool-input.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Argument repair: the shapes models actually send, mapped onto the schema
|
|
3
|
-
* before validation.
|
|
4
|
-
*
|
|
5
|
-
* Every rule here is a drift observed in other harnesses, not a guess: pi's
|
|
6
|
-
* edit tool accepts `edits` sent as a JSON string or a single object
|
|
7
|
-
* (`coding-agent/src/core/tools/edit.ts` prepareArguments), and ForgeCode
|
|
8
|
-
* reports fewer call errors after aligning names with what models are trained
|
|
9
|
-
* on. A repaired call is still the model's call -- the log keeps the input
|
|
10
|
-
* as sent; this only decides how it is interpreted. Pure and idempotent, so
|
|
11
|
-
* admission and execution can both apply it.
|
|
12
|
-
*/
|
|
13
|
-
type Input = Record<string, unknown>;
|
|
14
|
-
|
|
15
|
-
/** O.3: incomplete wire JSON remains the original input so admission can refuse it without effects. */
|
|
16
|
-
export function completedToolInput(raw: string): unknown {
|
|
17
|
-
try { return JSON.parse(raw) as unknown; }
|
|
18
|
-
catch { return raw; }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parsedJson(value: unknown): unknown {
|
|
22
|
-
if (typeof value !== "string") return value;
|
|
23
|
-
const trimmed = value.trim();
|
|
24
|
-
if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) return value;
|
|
25
|
-
try { return JSON.parse(trimmed); } catch { return value; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function editEntry(value: unknown): unknown {
|
|
29
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
30
|
-
const entry = { ...(value as Input) };
|
|
31
|
-
if (entry["old_string"] === undefined && entry["old"] !== undefined) { entry["old_string"] = entry["old"]; delete entry["old"]; }
|
|
32
|
-
if (entry["new_string"] === undefined && entry["new"] !== undefined) { entry["new_string"] = entry["new"]; delete entry["new"]; }
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function repairToolInput(name: string, input: unknown): unknown {
|
|
37
|
-
if (name !== "edit" || input === null || typeof input !== "object" || Array.isArray(input)) return input;
|
|
38
|
-
const repaired = editEntry(input) as Input;
|
|
39
|
-
if (repaired["edits"] !== undefined) {
|
|
40
|
-
const edits = parsedJson(repaired["edits"]);
|
|
41
|
-
repaired["edits"] = (Array.isArray(edits) ? edits : [edits]).map(editEntry);
|
|
42
|
-
}
|
|
43
|
-
return repaired;
|
|
44
|
-
}
|
package/src/core/transcript.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/** Human history is a separate pure log projection from the model's bounded context (D1, D8). */
|
|
2
|
-
import type { CounterCheckEvent, LogEvent } from "./events.ts";
|
|
3
|
-
import { counterVerdict } from "./acceptance.ts";
|
|
4
|
-
import { assertNever } from "./exhaustive.ts";
|
|
5
|
-
import type { ImageRef } from "./images.ts";
|
|
6
|
-
|
|
7
|
-
type Identity = { id: string; revision: string };
|
|
8
|
-
export type TranscriptItem = Identity & (
|
|
9
|
-
| { kind: "user"; text: string; interrupted: boolean; images?: readonly ImageRef[] }
|
|
10
|
-
| { kind: "assistant" | "thinking" | "notice"; text: string }
|
|
11
|
-
| { kind: "custom"; text: string; customType: string; data?: unknown; entry: boolean; timestamp: string }
|
|
12
|
-
| { kind: "tool"; callId: string; name: string; input: unknown; result: { text: string; ok: boolean; data?: unknown; images?: readonly ImageRef[] } | null }
|
|
13
|
-
| { kind: "shell"; command: string; output: string; exit: number; excluded: boolean }
|
|
14
|
-
| { kind: "segment"; reason: string; handoff: string; nextNode?: number; nextModel?: string }
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
export function projectTranscript(events: readonly LogEvent[], entryTypes: ReadonlySet<string> = new Set()): TranscriptItem[] {
|
|
18
|
-
const items: TranscriptItem[] = [];
|
|
19
|
-
const calls = new Map<string, number>();
|
|
20
|
-
const users = new Map<string, number>();
|
|
21
|
-
for (const event of events) {
|
|
22
|
-
const identity = { id: event.id, revision: event.id };
|
|
23
|
-
switch (event.t) {
|
|
24
|
-
case "extension":
|
|
25
|
-
if (event.display || !event.inContext && entryTypes.has(event.customType)) items.push({ ...identity, kind: "custom", text: event.content, customType: event.customType,
|
|
26
|
-
entry: !event.inContext, timestamp: event.at, ...(event.data === undefined ? {} : { data: event.data }) });
|
|
27
|
-
break;
|
|
28
|
-
case "user":
|
|
29
|
-
users.set(event.id, items.length);
|
|
30
|
-
items.push({ ...identity, kind: "user", text: event.text, interrupted: false, ...(event.images?.length ? { images: event.images } : {}) });
|
|
31
|
-
break;
|
|
32
|
-
case "user_bash": items.push({ ...identity, kind: "shell", command: event.command, output: event.content, exit: event.exit, excluded: event.excludeFromContext }); break;
|
|
33
|
-
case "outcome": items.push({ ...identity, kind: "notice",
|
|
34
|
-
text: `${event.outcome.status} · ${event.outcome.reason} · ${event.reason}` }); break;
|
|
35
|
-
case "assistant":
|
|
36
|
-
event.blocks.forEach((block, i) => {
|
|
37
|
-
const id = `${event.id}:${i}`;
|
|
38
|
-
if (block.type === "tool_use") {
|
|
39
|
-
calls.set(block.call_id, items.length);
|
|
40
|
-
items.push({ id, revision: id, kind: "tool", callId: block.call_id, name: block.name, input: block.input, result: null });
|
|
41
|
-
} else items.push({ id, revision: id, kind: block.type === "text" ? "assistant" : "thinking", text: block.text });
|
|
42
|
-
});
|
|
43
|
-
break;
|
|
44
|
-
case "tool_invocation": {
|
|
45
|
-
if (!event.parent_call_id && !event.origin) break;
|
|
46
|
-
calls.set(event.call_id, items.length);
|
|
47
|
-
items.push({ ...identity, kind: "tool", callId: event.call_id, name: event.name, input: event.input ?? { artifact: event.input_ref.sha256 }, result: null });
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
case "tool_result": {
|
|
51
|
-
const index = calls.get(event.call_id);
|
|
52
|
-
const call = index === undefined ? undefined : items[index];
|
|
53
|
-
if (call?.kind === "tool" && index !== undefined) {
|
|
54
|
-
items[index] = { ...call, revision: event.id, result: { text: event.content, ok: event.ok, ...(event.data === undefined ? {} : { data: event.data }), ...(event.images?.length ? { images: event.images } : {}) } };
|
|
55
|
-
} else items.push({ ...identity, kind: "notice", text: `Unpaired tool result ${event.call_id}: ${event.content}` });
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
case "interrupt": {
|
|
59
|
-
const index = users.get(event.user_id);
|
|
60
|
-
const user = index === undefined ? undefined : items[index];
|
|
61
|
-
if (user?.kind === "user" && index !== undefined) items[index] = { ...user, revision: event.id, interrupted: true };
|
|
62
|
-
items.push({ ...identity, kind: "notice", text: "Turn interrupted" });
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
// A segment boundary is the log's own structure; the view draws it as a divider (D20).
|
|
66
|
-
case "cut": items.push({ ...identity, kind: "segment", reason: event.reason, handoff: event.handoff_ref,
|
|
67
|
-
...(event.next_node === undefined ? {} : { nextNode: event.next_node }), ...(event.next_model === undefined ? {} : { nextModel: event.next_model }) }); break;
|
|
68
|
-
case "rewind": items.push({ ...identity, kind: "notice", text: `Navigated context (${event.mode}) · ${event.prompt}` }); break;
|
|
69
|
-
case "spawn": items.push({ ...identity, kind: "notice", text: `Branch ${event.branch.node} · ${event.purpose}` }); break;
|
|
70
|
-
case "joined": items.push({ ...identity, kind: "notice", text: `Branch ${event.branch.node} finished · ${event.summary}` }); break;
|
|
71
|
-
case "adopt": items.push({ ...identity, kind: "notice", text: `Adopted branch ${event.branch.node}` }); break;
|
|
72
|
-
case "discard": items.push({ ...identity, kind: "notice", text: `Discarded branch ${event.branch.node} · ${event.reason}` }); break;
|
|
73
|
-
case "skill_loaded": items.push({ ...identity, kind: "notice", text: `Skill ${event.name} · ${event.mode} · ${event.sha256}` }); break;
|
|
74
|
-
case "process_exit": items.push({ ...identity, kind: "notice", text: `Process ${event.session} exited ${event.exit} · output ${event.output.sha256}` }); break;
|
|
75
|
-
case "acceptance_contract": items.push({ ...identity, kind: "notice", text: `Acceptance contract · ${event.contract.id}` }); break;
|
|
76
|
-
case "target_observation": items.push({ ...identity, kind: "notice", text: `Target ${event.observation.target} · ${event.observation.state} · ${event.observation.reason}` }); break;
|
|
77
|
-
case "acceptance_decision": items.push({ ...identity, kind: "notice", text: `User accepted · ${event.criterion}` }); break;
|
|
78
|
-
case "counter_check": items.push({ ...identity, kind: "notice", text: counterNotice(event) }); break;
|
|
79
|
-
case "blind_check": items.push({ ...identity, kind: "notice", text: "reason" in event ? `Blind check ${event.check} could not run: ${event.reason}`
|
|
80
|
-
: `Blind check ${event.check} ${event.exit === 0 ? "passed" : `failed (exit ${event.exit})`}${event.exposed ? " · the model touched the blind checks, so this is not independent" : ""}` }); break;
|
|
81
|
-
case "goal": items.push({ ...identity, kind: "notice", text: event.action === "set"
|
|
82
|
-
? `Goal set · ${event.accepted.length} accepted${event.rejected.length ? `, ${event.rejected.length} rejected` : ""} · ${event.objective}`
|
|
83
|
-
: event.reason === "met" ? "Goal met · every accepted criterion has current evidence" : "Goal cleared" }); break;
|
|
84
|
-
// Approvals answer "why was this allowed" in /export and history; the person
|
|
85
|
-
// already saw the prompt, and a notice per auto-allowed command would bury the chat.
|
|
86
|
-
case "approval":
|
|
87
|
-
case "checkpoint": case "workspace_observation":
|
|
88
|
-
case "guidance": case "claim": case "decision": case "pitfall": case "blocked": case "question": case "next_action": case "work_status": case "work_owner": case "work_dependencies": case "evidence_link":
|
|
89
|
-
// These remain inspectable via /state and /export; they are not chat messages.
|
|
90
|
-
break;
|
|
91
|
-
default: assertNever(event, "projectTranscript");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return items;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** One counter-check in the words of the criterion's role (S.7); the verdict is derived, never stored. */
|
|
98
|
-
function counterNotice(event: CounterCheckEvent): string {
|
|
99
|
-
const verdict = counterVerdict(event);
|
|
100
|
-
switch (verdict) {
|
|
101
|
-
case "unavailable": return `Check ${event.criterion} could not be counter-checked: ${"reason" in event ? event.reason : "no observation"}`;
|
|
102
|
-
case "vacuous": return `Check ${event.criterion} also passes without this request's changes; it does not test them`;
|
|
103
|
-
case "discriminating": return `Check ${event.criterion} fails without this request's changes; it tests them`;
|
|
104
|
-
case "held": return `Guard ${event.criterion} also held before this request's changes, as a guard should`;
|
|
105
|
-
case "not_held": return `Guard ${event.criterion} did not hold before this request's changes; it tests them rather than guarding`;
|
|
106
|
-
default: return assertNever(verdict, "counterNotice");
|
|
107
|
-
}
|
|
108
|
-
}
|
package/src/core/tui-mode.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which screen an interactive session uses. Pure: flag, environment and
|
|
3
|
-
* setting go in, a concrete mode and the reason come out.
|
|
4
|
-
*
|
|
5
|
-
* Fullscreen is the default (OpenCode, grok-build and Pi 1.0 all default to
|
|
6
|
-
* the alternate screen). `auto` is Codex's / grok-build's policy rather than
|
|
7
|
-
* "always", because the alternate screen has known dead ends: Zellij keeps
|
|
8
|
-
* no scrollback for it (zellij#1032, Codex turns it off there), and a dumb
|
|
9
|
-
* terminal cannot address the screen at all. Every route back to native
|
|
10
|
-
* scrollback stays: `--tui-mode regular`, `XENO_TUI_MODE`, the setting.
|
|
11
|
-
*/
|
|
12
|
-
export type TuiModeSetting = "auto" | "regular" | "fullscreen";
|
|
13
|
-
export type TuiMode = "regular" | "fullscreen";
|
|
14
|
-
|
|
15
|
-
export interface TuiModeInput {
|
|
16
|
-
readonly flag?: TuiMode | undefined;
|
|
17
|
-
readonly setting?: TuiModeSetting | undefined;
|
|
18
|
-
readonly env: Readonly<Record<string, string | undefined>>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resolveTuiMode(input: TuiModeInput): { mode: TuiMode; reason: string } {
|
|
22
|
-
if (input.flag) return { mode: input.flag, reason: "--tui-mode" };
|
|
23
|
-
const fromEnv = input.env["XENO_TUI_MODE"];
|
|
24
|
-
if (fromEnv === "regular" || fromEnv === "fullscreen") return { mode: fromEnv, reason: "XENO_TUI_MODE" };
|
|
25
|
-
if (input.setting === "regular" || input.setting === "fullscreen") return { mode: input.setting, reason: "settings" };
|
|
26
|
-
if (input.env["TERM"] === "dumb") return { mode: "regular", reason: "TERM=dumb cannot draw a fullscreen UI" };
|
|
27
|
-
if (input.env["ZELLIJ"] !== undefined) return { mode: "regular", reason: "Zellij keeps no scrollback for the alternate screen" };
|
|
28
|
-
return { mode: "fullscreen", reason: "default" };
|
|
29
|
-
}
|
package/src/core/update-check.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether to look for a newer xeno and what to say about it (docs/tasks/feat-update-check.md). Pure.
|
|
3
|
-
*
|
|
4
|
-
* Notify only: xeno names the command for the way it was installed and never
|
|
5
|
-
* runs it. Automatic checks run at most once a day, recorded even when they fail,
|
|
6
|
-
* and no automatic requests offline, in CI, when turned off, or from a source checkout
|
|
7
|
-
* (where the right update is `git pull`, not a package manager). Clock,
|
|
8
|
-
* environment and the cached answer are all arguments, so every rule is a
|
|
9
|
-
* plain function of its inputs. Explicit checks bypass automatic scheduling
|
|
10
|
-
* and opt-outs, while retaining the offline and source-checkout boundaries.
|
|
11
|
-
* An explicit `/update` also installs (docs/tasks/fix-update-installs.md):
|
|
12
|
-
* typing the command is the consent automatic installation never has.
|
|
13
|
-
*/
|
|
14
|
-
import { compareVersions, isUpgrade, parseVersion } from "./version.ts";
|
|
15
|
-
|
|
16
|
-
export const PACKAGE_NAME = "@visual-z/xeno";
|
|
17
|
-
/** One request a day, like update-notifier; Codex uses 20 hours. Assumed. */
|
|
18
|
-
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
-
|
|
20
|
-
export type InstallKind = "bun" | "npm" | "other" | "checkout";
|
|
21
|
-
|
|
22
|
-
export interface UpdateCache {
|
|
23
|
-
/** Epoch ms of the last attempt, successful or not. */
|
|
24
|
-
readonly checkedAt: number;
|
|
25
|
-
/** The newest published version seen, when the last success had one. */
|
|
26
|
-
readonly latest?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface UpdateInput {
|
|
30
|
-
readonly current: string;
|
|
31
|
-
readonly now: number;
|
|
32
|
-
readonly cache: UpdateCache | null;
|
|
33
|
-
readonly env: Readonly<Record<string, string | undefined>>;
|
|
34
|
-
readonly offline: boolean;
|
|
35
|
-
/** Settings `updateCheck`; undefined means on. */
|
|
36
|
-
readonly enabled: boolean | undefined;
|
|
37
|
-
readonly install: InstallKind;
|
|
38
|
-
/** Explicit checks bypass only automatic scheduling and opt-outs (D16 manual-check extension). */
|
|
39
|
-
readonly manual?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface UpdatePlan {
|
|
43
|
-
/** Ask the registry now. */
|
|
44
|
-
readonly check: boolean;
|
|
45
|
-
/** What to tell the person, if anything is newer. */
|
|
46
|
-
readonly notice?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Shared by every background network opt-out, so one variable means the same everywhere. */
|
|
50
|
-
export function truthy(value: string | undefined): boolean {
|
|
51
|
-
return value !== undefined && value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Reasons not to send a request at all, in the order they are checked. */
|
|
55
|
-
export function updateDisabled(input: Pick<UpdateInput, "env" | "offline" | "enabled" | "install" | "manual">): string | undefined {
|
|
56
|
-
if (input.install === "checkout") return "running from a source checkout";
|
|
57
|
-
if (input.offline || truthy(input.env["XENO_OFFLINE"])) return "offline";
|
|
58
|
-
if (input.manual) return undefined;
|
|
59
|
-
if (input.enabled === false) return "updateCheck is off";
|
|
60
|
-
if (truthy(input.env["XENO_NO_UPDATE_CHECK"]) || truthy(input.env["NO_UPDATE_NOTIFIER"])) return "turned off by environment";
|
|
61
|
-
if (truthy(input.env["CI"]) || truthy(input.env["CONTINUOUS_INTEGRATION"])) return "running in CI";
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function updatePlan(input: UpdateInput): UpdatePlan {
|
|
66
|
-
if (updateDisabled(input)) return { check: false };
|
|
67
|
-
if (input.manual) return { check: true };
|
|
68
|
-
const due = !input.cache || !Number.isFinite(input.cache.checkedAt) || input.now - input.cache.checkedAt >= CHECK_INTERVAL_MS
|
|
69
|
-
|| input.cache.checkedAt > input.now; // a clock set back must not silence checks for a year
|
|
70
|
-
const latest = input.cache?.latest;
|
|
71
|
-
return { check: due, ...(latest && isUpgrade(latest, input.current) ? { notice: updateNotice(latest, input.current, input.install) } : {}) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function upgradeCommand(install: InstallKind): string | undefined {
|
|
75
|
-
switch (install) {
|
|
76
|
-
case "bun": return `bun add -g ${PACKAGE_NAME}@latest`;
|
|
77
|
-
case "npm": return `npm i -g ${PACKAGE_NAME}@latest`;
|
|
78
|
-
case "other": case "checkout": return undefined;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* What an explicit `/update` runs: the package manager the notice names,
|
|
84
|
-
* pinned to the version just checked so `latest` cannot move between the
|
|
85
|
-
* check and the install. Argv, never a shell string. Undefined where xeno
|
|
86
|
-
* cannot know the right command and only the notice applies.
|
|
87
|
-
*/
|
|
88
|
-
export function installArgv(install: InstallKind, version: string): readonly string[] | undefined {
|
|
89
|
-
switch (install) {
|
|
90
|
-
case "bun": return ["bun", "add", "-g", `${PACKAGE_NAME}@${version}`];
|
|
91
|
-
case "npm": return ["npm", "i", "-g", `${PACKAGE_NAME}@${version}`];
|
|
92
|
-
case "other": case "checkout": return undefined;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export type InstallOutcome =
|
|
97
|
-
| { readonly kind: "installed" }
|
|
98
|
-
| { readonly kind: "exit"; readonly code: number; readonly output: readonly string[] }
|
|
99
|
-
| { readonly kind: "timeout"; readonly ms: number; readonly output: readonly string[] }
|
|
100
|
-
| { readonly kind: "unavailable"; readonly reason: string }
|
|
101
|
-
| { readonly kind: "cancelled" };
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* How an install ended, in words a person can act on. A bare exit code hides
|
|
105
|
-
* ENOTEMPTY and EACCES (codex #5015, #21897), so failures carry the package
|
|
106
|
-
* manager's last lines and always the command to run by hand.
|
|
107
|
-
*/
|
|
108
|
-
export function installResult(outcome: InstallOutcome, argv: readonly string[], current: string, latest: string): { readonly ok: boolean; readonly text: string } {
|
|
109
|
-
const manual = `Run it yourself: ${argv.join(" ")}`;
|
|
110
|
-
const tail = (output: readonly string[]) => output.length ? `\n${output.map(line => ` ${line}`).join("\n")}` : "";
|
|
111
|
-
switch (outcome.kind) {
|
|
112
|
-
case "installed": return { ok: true, text: `Updated xeno ${current} → ${latest}. Restart xeno to use it.` };
|
|
113
|
-
case "exit": return { ok: false, text: `Could not update xeno: ${argv[0]} exited ${outcome.code}.${tail(outcome.output)}\n${manual}` };
|
|
114
|
-
case "timeout": return { ok: false, text: `Could not update xeno: ${argv[0]} did not finish within ${Math.round(outcome.ms / 1000)}s and was stopped.${tail(outcome.output)}\n${manual}` };
|
|
115
|
-
case "unavailable": return { ok: false, text: `Could not update xeno: ${outcome.reason}.\n${manual}` };
|
|
116
|
-
case "cancelled": return { ok: false, text: "xeno update cancelled." };
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Two lines: what is new, then the command on its own line so it can be copied whole. */
|
|
121
|
-
export function updateNotice(latest: string, current: string, install: InstallKind): string {
|
|
122
|
-
const command = upgradeCommand(install);
|
|
123
|
-
return `xeno ${latest} is available (you have ${current})\n ${command ?? "see docs/install.md to update"}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** A manual result describes this request, never an older cached answer. */
|
|
127
|
-
export function manualUpdateResult(latest: string | null, current: string, install: InstallKind): {
|
|
128
|
-
readonly status: "available" | "current" | "ahead" | "ineligible" | "failed";
|
|
129
|
-
readonly text: string;
|
|
130
|
-
} {
|
|
131
|
-
const next = latest === null ? null : parseVersion(latest), now = parseVersion(current);
|
|
132
|
-
if (!next || !now || latest === null) return { status: "failed", text: `Could not check for xeno updates (current ${current}). Check your connection or registry setting and try again.` };
|
|
133
|
-
if (isUpgrade(latest, current)) return { status: "available", text: updateNotice(latest, current, install) };
|
|
134
|
-
const order = compareVersions(next, now);
|
|
135
|
-
if (order === 0) return { status: "current", text: `xeno is up to date (${current}).` };
|
|
136
|
-
if (order < 0) return { status: "ahead", text: `xeno ${current} is newer than the registry's latest release (${latest}).` };
|
|
137
|
-
return { status: "ineligible", text: `No newer stable xeno release is available (current ${current}; registry latest ${latest}).` };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* How this copy was installed, from where its package root really lives.
|
|
142
|
-
* Detected from the path rather than by running package managers, which
|
|
143
|
-
* costs seconds at startup (OpenCode runs up to seven list commands).
|
|
144
|
-
*/
|
|
145
|
-
export function installKind(packageRoot: string, hasGit: boolean): InstallKind {
|
|
146
|
-
const path = packageRoot.replaceAll("\\", "/");
|
|
147
|
-
if (hasGit || !path.includes("/node_modules/")) return "checkout";
|
|
148
|
-
if (path.includes("/.bun/install/global/")) return "bun";
|
|
149
|
-
if (path.includes("/lib/node_modules/") || /\/npm\/node_modules\//i.test(path)) return "npm";
|
|
150
|
-
return "other";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** The registry's dist-tags for the package, honouring npm's registry setting. */
|
|
154
|
-
export function distTagsUrl(registry: string | undefined): string {
|
|
155
|
-
const base = (registry && /^https?:\/\//.test(registry) ? registry : "https://registry.npmjs.org/").replace(/\/+$/, "");
|
|
156
|
-
return `${base}/-/package/${PACKAGE_NAME.replace("/", "%2f")}/dist-tags`;
|
|
157
|
-
}
|
package/src/core/version.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Semantic version comparison (semver.org §11), without a dependency. Pure.
|
|
3
|
-
*
|
|
4
|
-
* Used to decide whether a published version is newer than the running one
|
|
5
|
-
* (docs/tasks/feat-update-check.md). Anything that does not parse is not a
|
|
6
|
-
* version, and an unparseable version never produces an update notice.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface Version {
|
|
10
|
-
readonly major: number;
|
|
11
|
-
readonly minor: number;
|
|
12
|
-
readonly patch: number;
|
|
13
|
-
/** Dot-separated pre-release identifiers; empty for a release. */
|
|
14
|
-
readonly prerelease: readonly (number | string)[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const PATTERN = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
18
|
-
|
|
19
|
-
export function parseVersion(text: string): Version | null {
|
|
20
|
-
const match = PATTERN.exec(text.trim());
|
|
21
|
-
if (!match) return null;
|
|
22
|
-
const numbers = [match[1], match[2], match[3]].map(Number);
|
|
23
|
-
if (numbers.some(n => !Number.isSafeInteger(n))) return null;
|
|
24
|
-
const prerelease = match[4] ? match[4].split(".").map(part => /^\d+$/.test(part) ? Number(part) : part) : [];
|
|
25
|
-
return { major: numbers[0]!, minor: numbers[1]!, patch: numbers[2]!, prerelease };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** -1, 0 or 1, by §11: numbers, then a release outranks its pre-releases, then identifiers left to right. Build metadata is ignored. */
|
|
29
|
-
export function compareVersions(a: Version, b: Version): -1 | 0 | 1 {
|
|
30
|
-
for (const key of ["major", "minor", "patch"] as const) if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
31
|
-
const x = a.prerelease, y = b.prerelease;
|
|
32
|
-
if (!x.length || !y.length) return x.length === y.length ? 0 : x.length ? -1 : 1;
|
|
33
|
-
for (let i = 0; i < Math.min(x.length, y.length); i++) {
|
|
34
|
-
const p = x[i]!, q = y[i]!;
|
|
35
|
-
if (p === q) continue;
|
|
36
|
-
if (typeof p === "number" && typeof q === "number") return p < q ? -1 : 1;
|
|
37
|
-
// Numeric identifiers have lower precedence than alphanumeric ones.
|
|
38
|
-
if (typeof p === "number") return -1;
|
|
39
|
-
if (typeof q === "number") return 1;
|
|
40
|
-
return p < q ? -1 : 1;
|
|
41
|
-
}
|
|
42
|
-
return x.length === y.length ? 0 : x.length < y.length ? -1 : 1;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Whether `latest` should be offered to someone running `current`. Never a
|
|
47
|
-
* downgrade, never something unparseable, and a release is not nudged
|
|
48
|
-
* towards a pre-release (a pre-release user may move to the next release).
|
|
49
|
-
*/
|
|
50
|
-
export function isUpgrade(latest: string, current: string): boolean {
|
|
51
|
-
const next = parseVersion(latest), now = parseVersion(current);
|
|
52
|
-
if (!next || !now) return false;
|
|
53
|
-
if (next.prerelease.length && !now.prerelease.length) return false;
|
|
54
|
-
return compareVersions(next, now) > 0;
|
|
55
|
-
}
|
package/src/core/web-content.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/** Network observations remain evidence-bearing projections, never executable instructions (D11). */
|
|
2
|
-
export type WebFormat = "text" | "markdown" | "html";
|
|
3
|
-
export type WebSearchProvider = "brave" | "exa";
|
|
4
|
-
export interface WebSearchConfig { provider: WebSearchProvider; apiKeyEnv?: string; endpoint?: string }
|
|
5
|
-
export interface WebSearchHit { title: string; url: string; description: string; published?: string }
|
|
6
|
-
|
|
7
|
-
// Assumed admission bounds, not measured optima: bound malformed URLs and paid search requests.
|
|
8
|
-
export const MAX_WEB_URL_LENGTH = 8_192;
|
|
9
|
-
export const MAX_WEB_QUERY_LENGTH = 600;
|
|
10
|
-
export const MAX_WEB_RESULTS = 20;
|
|
11
|
-
export function webUrl(value: unknown): string {
|
|
12
|
-
if (typeof value !== "string" || value.length === 0 || value.length > MAX_WEB_URL_LENGTH || /[\u0000-\u0020\u007f]/.test(value))
|
|
13
|
-
throw Error("url must be a nonempty HTTP(S) URL without whitespace and at most 8192 characters");
|
|
14
|
-
let parsed: URL;
|
|
15
|
-
try { parsed = new URL(value); } catch { throw Error("url must be an absolute HTTP(S) URL"); }
|
|
16
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw Error("url must use HTTP or HTTPS");
|
|
17
|
-
if (parsed.username || parsed.password) throw Error("URL credentials are not supported; configure credentials outside model arguments");
|
|
18
|
-
parsed.hash = "";
|
|
19
|
-
return parsed.href;
|
|
20
|
-
}
|
|
21
|
-
export function webFetchArguments(input: Record<string, unknown>): { url: string; format: WebFormat; timeoutMs: number } {
|
|
22
|
-
const url = webUrl(input["url"]), format = input["format"] ?? "markdown";
|
|
23
|
-
if (format !== "text" && format !== "markdown" && format !== "html") throw Error("format must be text, markdown or html");
|
|
24
|
-
const timeout = input["timeout"] ?? 30;
|
|
25
|
-
if (!Number.isSafeInteger(timeout) || Number(timeout) < 1 || Number(timeout) > 120) throw Error("timeout must be an integer from 1 to 120 seconds");
|
|
26
|
-
return { url, format, timeoutMs: Number(timeout) * 1_000 };
|
|
27
|
-
}
|
|
28
|
-
export function webSearchArguments(input: Record<string, unknown>): { query: string; count: number } {
|
|
29
|
-
const query = input["query"], count = input["count"] ?? 8;
|
|
30
|
-
if (typeof query !== "string" || !query.trim() || query.length > MAX_WEB_QUERY_LENGTH || query.trim().split(/\s+/).length > 75)
|
|
31
|
-
throw Error("query must contain 1 to 600 characters and at most 75 words");
|
|
32
|
-
if (!Number.isSafeInteger(count) || Number(count) < 1 || Number(count) > MAX_WEB_RESULTS) throw Error("count must be an integer from 1 to 20");
|
|
33
|
-
return { query: query.trim(), count: Number(count) };
|
|
34
|
-
}
|
|
35
|
-
export function parseWebSearchConfig(value: unknown): WebSearchConfig | undefined {
|
|
36
|
-
if (value === undefined || value === null) return undefined;
|
|
37
|
-
if (typeof value !== "object" || Array.isArray(value)) throw Error("webSearch must be an object");
|
|
38
|
-
const record = value as Record<string, unknown>;
|
|
39
|
-
if (Object.keys(record).some(key => !["provider", "apiKeyEnv", "endpoint"].includes(key))) throw Error("webSearch has an unsupported field");
|
|
40
|
-
if (record["provider"] !== "brave" && record["provider"] !== "exa") throw Error("webSearch.provider must be brave or exa");
|
|
41
|
-
const key = record["apiKeyEnv"], endpoint = record["endpoint"];
|
|
42
|
-
if (key !== undefined && (typeof key !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))) throw Error("webSearch.apiKeyEnv must name an environment variable");
|
|
43
|
-
return { provider: record["provider"], ...(typeof key === "string" ? { apiKeyEnv: key } : {}),
|
|
44
|
-
...(endpoint !== undefined ? { endpoint: webUrl(endpoint) } : {}) };
|
|
45
|
-
}
|
|
46
|
-
function object(value: unknown): Record<string, unknown> | undefined {
|
|
47
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
48
|
-
}
|
|
49
|
-
export function webSearchHits(provider: WebSearchProvider, value: unknown, count: number): WebSearchHit[] {
|
|
50
|
-
const result = object(value), source = provider === "brave" ? object(result?.["web"])?.["results"] : result?.["results"];
|
|
51
|
-
if (!Array.isArray(source)) throw Error("Search provider response did not contain a results array");
|
|
52
|
-
return source.slice(0, count).map(item => {
|
|
53
|
-
const hit = object(item);
|
|
54
|
-
if (!hit || typeof hit["url"] !== "string" || typeof hit["title"] !== "string") throw Error("Search provider returned an invalid result");
|
|
55
|
-
const highlights = hit["highlights"];
|
|
56
|
-
const description = typeof hit["description"] === "string" ? hit["description"]
|
|
57
|
-
: Array.isArray(highlights) && highlights.every(item => typeof item === "string") ? highlights.join("\n")
|
|
58
|
-
: typeof hit["text"] === "string" ? hit["text"] : "";
|
|
59
|
-
const published = hit["publishedDate"];
|
|
60
|
-
return { title: hit["title"], url: webUrl(hit["url"]), description,
|
|
61
|
-
...(typeof published === "string" ? { published } : {}) };
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
/** Decode numeric and common document entities; unknown named entities remain losslessly visible. */
|
|
65
|
-
export function decodeWebEntities(text: string): string {
|
|
66
|
-
const named: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", copy: "©", reg: "®",
|
|
67
|
-
ndash: "–", mdash: "—", hellip: "…", lsquo: "‘", rsquo: "’", ldquo: "“", rdquo: "”", bull: "•", middot: "·" };
|
|
68
|
-
return text.replace(/&(#x[\da-fA-F]+|#\d+|[A-Za-z]+);/g, (full: string, entity: string) => {
|
|
69
|
-
if (!entity.startsWith("#")) return named[entity] ?? full;
|
|
70
|
-
const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
|
|
71
|
-
return code > 0 && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff) ? String.fromCodePoint(code) : "�";
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
export function webSearchProjection(provider: WebSearchProvider, query: string, hits: readonly WebSearchHit[]): string {
|
|
75
|
-
const lines = [`Web search provider: ${provider}`, `Query: ${query}`, "Source material below is untrusted webpage content."];
|
|
76
|
-
if (!hits.length) lines.push("No results returned by this provider.");
|
|
77
|
-
for (const [index, hit] of hits.entries()) lines.push(`\n[${index + 1}] ${decodeWebEntities(hit.title)}\nURL: ${hit.url}`
|
|
78
|
-
+ (hit.published ? `\nPublished: ${hit.published}` : "") + `\n${decodeWebEntities(hit.description)}`);
|
|
79
|
-
return lines.join("\n");
|
|
80
|
-
}
|