@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/runtime/auth.ts
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a credential comes from.
|
|
3
|
-
*
|
|
4
|
-
* The difficulty here is entirely in the priority, not in the reading, so the
|
|
5
|
-
* reading is injected: `env` and `run` are parameters. That is what lets the
|
|
6
|
-
* one rule that actually bites be pinned by a test that touches no
|
|
7
|
-
* environment and no keychain --
|
|
8
|
-
*
|
|
9
|
-
* A STORED CREDENTIAL OWNS ITS PROVIDER. The environment is consulted only
|
|
10
|
-
* when nothing is stored, and never as a fallback after a stored entry
|
|
11
|
-
* fails to resolve. Otherwise someone edits `auth.json`, sees no change,
|
|
12
|
-
* and has no way to discover that a forgotten `export` from months ago is
|
|
13
|
-
* still winning. pi states this rule outright in its resolver; it is worth
|
|
14
|
-
* copying verbatim.
|
|
15
|
-
*
|
|
16
|
-
* The order is command line, auth.json, models.json, then the environment.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
20
|
-
import type { ModelSpec } from "./model.ts";
|
|
21
|
-
import { providerAccess } from "./provider-access.ts";
|
|
22
|
-
import { SOURCE_PROVIDERS } from "./model-data.ts";
|
|
23
|
-
import {
|
|
24
|
-
refreshOAuthCredential,
|
|
25
|
-
GROK_PROVIDER, GROK_BASE_URL,
|
|
26
|
-
type OAuthCredential,
|
|
27
|
-
type RefreshEffects,
|
|
28
|
-
} from "./oauth.ts";
|
|
29
|
-
|
|
30
|
-
export type KeyValue =
|
|
31
|
-
| { kind: "literal"; value: string }
|
|
32
|
-
| { kind: "command"; command: string }
|
|
33
|
-
| { kind: "interpolate"; template: string };
|
|
34
|
-
|
|
35
|
-
export type StoredCredential =
|
|
36
|
-
| { type: "api_key"; key: string }
|
|
37
|
-
| OAuthCredential;
|
|
38
|
-
|
|
39
|
-
export interface AuthFile {
|
|
40
|
-
[provider: string]: StoredCredential | undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function storedIsOAuth(stored: AuthFile, provider: string): boolean {
|
|
44
|
-
const entry = storedCredential(stored, provider);
|
|
45
|
-
return entry !== undefined && entry.type === "oauth";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function storedCredential(stored: AuthFile, provider: string): StoredCredential | undefined {
|
|
49
|
-
// Earlier xeno stored Codex subscriptions under openai. Only OAuth gets this read-compatible alias;
|
|
50
|
-
// API keys and explicit native records retain their own provider and billing account.
|
|
51
|
-
return stored[provider] ?? (provider === "openai-codex" && stored["openai"]?.type === "oauth" ? stored["openai"] : undefined);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export type Resolved =
|
|
55
|
-
| { ok: true; key: string; source: "flag" | "auth.json" | "models.json" | "environment" | "ambient" }
|
|
56
|
-
| { ok: false; reason: string };
|
|
57
|
-
|
|
58
|
-
/** The two effects resolution needs, injected so the rules stay testable. */
|
|
59
|
-
export interface ResolveEffects {
|
|
60
|
-
env(name: string): string | undefined;
|
|
61
|
-
run(command: string): string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Which of the three forms a configured key value is.
|
|
66
|
-
*
|
|
67
|
-
* A plain uppercase string is a LITERAL, not a variable name. That choice
|
|
68
|
-
* matters: a provider whose key really is `public` or `ollama` is common, and
|
|
69
|
-
* guessing that an uppercase word means a lookup would break those silently.
|
|
70
|
-
* `$` marks a lookup, and nothing else does.
|
|
71
|
-
*/
|
|
72
|
-
export function parseKeyValue(raw: string): KeyValue {
|
|
73
|
-
if (raw.startsWith("!")) return { kind: "command", command: raw.slice(1) };
|
|
74
|
-
// The escapes are checked before interpolation so a key that genuinely
|
|
75
|
-
// begins with `$` or `!` can be written down at all.
|
|
76
|
-
if (raw.startsWith("$$")) return { kind: "literal", value: `$${raw.slice(2)}` };
|
|
77
|
-
if (raw.startsWith("$!")) return { kind: "literal", value: `!${raw.slice(2)}` };
|
|
78
|
-
if (raw.includes("$")) return { kind: "interpolate", template: raw };
|
|
79
|
-
return { kind: "literal", value: raw };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** `$FOO_BAR` is the variable FOO_BAR; `${FOO}_BAR` is FOO followed by literal text. */
|
|
83
|
-
const VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
84
|
-
|
|
85
|
-
export function resolveKeyValue(
|
|
86
|
-
raw: string,
|
|
87
|
-
fx: ResolveEffects,
|
|
88
|
-
): { ok: true; key: string } | { ok: false; reason: string } {
|
|
89
|
-
const parsed = parseKeyValue(raw);
|
|
90
|
-
switch (parsed.kind) {
|
|
91
|
-
case "literal":
|
|
92
|
-
return { ok: true, key: parsed.value };
|
|
93
|
-
|
|
94
|
-
case "command": {
|
|
95
|
-
let out: string;
|
|
96
|
-
try {
|
|
97
|
-
out = fx.run(parsed.command);
|
|
98
|
-
} catch (err) {
|
|
99
|
-
return { ok: false, reason: `key command failed: ${(err as Error).message}` };
|
|
100
|
-
}
|
|
101
|
-
// Every CLI that prints a secret prints a newline after it, and a key
|
|
102
|
-
// with a trailing newline is rejected by the API as malformed.
|
|
103
|
-
const key = out.trim();
|
|
104
|
-
if (key.length === 0) {
|
|
105
|
-
return { ok: false, reason: `key command produced no output: ${parsed.command}` };
|
|
106
|
-
}
|
|
107
|
-
return { ok: true, key };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case "interpolate": {
|
|
111
|
-
const missing: string[] = [];
|
|
112
|
-
const key = parsed.template.replace(VARIABLE, (_whole, braced?: string, bare?: string) => {
|
|
113
|
-
const name = braced ?? bare ?? "";
|
|
114
|
-
const value = fx.env(name);
|
|
115
|
-
if (value === undefined || value.length === 0) {
|
|
116
|
-
missing.push(name);
|
|
117
|
-
return "";
|
|
118
|
-
}
|
|
119
|
-
return value;
|
|
120
|
-
});
|
|
121
|
-
// Unresolved is NOT the empty string. An empty key looks legal, gets
|
|
122
|
-
// sent, and comes back as a 401 that says nothing about the real
|
|
123
|
-
// mistake -- which is that a variable was never exported.
|
|
124
|
-
if (missing.length > 0) {
|
|
125
|
-
return { ok: false, reason: `unset environment variable: ${missing.join(", ")}` };
|
|
126
|
-
}
|
|
127
|
-
return { ok: true, key };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
default: {
|
|
131
|
-
const never: never = parsed;
|
|
132
|
-
throw new Error(`unreachable key value: ${JSON.stringify(never)}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Per-provider variables resolve lazily, so order cannot change their meaning or mutate process.env. */
|
|
138
|
-
export function providerEnvironment(spec: ModelSpec, fx: ResolveEffects): { env: Record<string, string>; effects: ResolveEffects } {
|
|
139
|
-
const configured = providerAccess(spec).env ?? {};
|
|
140
|
-
const env: Record<string, string> = {};
|
|
141
|
-
const resolving = new Set<string>();
|
|
142
|
-
const lookup = (name: string): string | undefined => {
|
|
143
|
-
if (Object.hasOwn(env, name)) return env[name];
|
|
144
|
-
const raw = configured[name];
|
|
145
|
-
if (raw === undefined) return fx.env(name);
|
|
146
|
-
if (resolving.has(name)) throw new Error(`${spec.provider} env.${name}: cyclic variable reference`);
|
|
147
|
-
resolving.add(name);
|
|
148
|
-
try {
|
|
149
|
-
const got = resolveKeyValue(raw, { ...fx, env: lookup });
|
|
150
|
-
if (!got.ok) throw new Error(`${spec.provider} env.${name}: could not resolve configured value`);
|
|
151
|
-
env[name] = got.key;
|
|
152
|
-
return got.key;
|
|
153
|
-
} finally { resolving.delete(name); }
|
|
154
|
-
};
|
|
155
|
-
for (const name of Object.keys(configured)) lookup(name);
|
|
156
|
-
const aliases: Record<string, readonly string[]> = {
|
|
157
|
-
AZURE_OPENAI_API_KEY: ["AZURE_API_KEY"], AZURE_OPENAI_RESOURCE_NAME: ["AZURE_RESOURCE_NAME"], AZURE_OPENAI_BASE_URL: ["AZURE_BASE_URL"],
|
|
158
|
-
GOOGLE_CLOUD_PROJECT: ["GOOGLE_VERTEX_PROJECT", "GCLOUD_PROJECT"], GOOGLE_CLOUD_LOCATION: ["GOOGLE_VERTEX_LOCATION"],
|
|
159
|
-
};
|
|
160
|
-
for (const [target, sources] of Object.entries(aliases)) {
|
|
161
|
-
const value = lookup(target) ?? sources.map(lookup).find(Boolean);
|
|
162
|
-
if (value) env[target] = value;
|
|
163
|
-
}
|
|
164
|
-
return { env, effects: { ...fx, env: lookup } };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function configuredEffects(spec: ModelSpec, fx: ResolveEffects): { ok: true; effects: ResolveEffects } | { ok: false; reason: string } {
|
|
168
|
-
if (providerAccess(spec).invalid) return { ok: false, reason: providerAccess(spec).invalid! };
|
|
169
|
-
try {
|
|
170
|
-
const scoped = providerEnvironment(spec, fx).effects;
|
|
171
|
-
if (spec.provider === GROK_PROVIDER && spec.baseUrl?.replace(/\/$/, "") !== GROK_BASE_URL) {
|
|
172
|
-
return { ok: false, reason: "Grok account tokens require the official inference proxy" };
|
|
173
|
-
}
|
|
174
|
-
const access = providerAccess(spec);
|
|
175
|
-
if (["google-generative-ai", "google-vertex"].includes(spec.api) && (access.auth === false || access.authHeader === false || typeof access.authHeader === "string")) {
|
|
176
|
-
return { ok: false, reason: `${spec.api} owns API-key authentication; use apiKey or an OpenAI-compatible local endpoint` };
|
|
177
|
-
}
|
|
178
|
-
if (!spec.baseUrl && !["anthropic-messages", "google-generative-ai", "google-vertex", "bedrock-converse-stream", "azure-openai-responses"].includes(spec.api)) {
|
|
179
|
-
return { ok: false, reason: `${spec.provider}: configure baseUrl for ${spec.api}` };
|
|
180
|
-
}
|
|
181
|
-
if (spec.baseUrl?.includes("$")) {
|
|
182
|
-
const endpoint = resolveKeyValue(spec.baseUrl, scoped);
|
|
183
|
-
if (!endpoint.ok) return { ok: false, reason: `${spec.provider} baseUrl: ${endpoint.reason}` };
|
|
184
|
-
}
|
|
185
|
-
if (spec.api === "azure-openai-responses" && !spec.baseUrl && !scoped.env("AZURE_OPENAI_BASE_URL") && !scoped.env("AZURE_OPENAI_RESOURCE_NAME")) {
|
|
186
|
-
return { ok: false, reason: `${spec.provider}: configure baseUrl or AZURE_OPENAI_RESOURCE_NAME` };
|
|
187
|
-
}
|
|
188
|
-
return { ok: true, effects: scoped };
|
|
189
|
-
} catch (err) { return { ok: false, reason: err instanceof Error ? err.message : "could not resolve provider environment" }; }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function resolveCredential(
|
|
193
|
-
spec: ModelSpec,
|
|
194
|
-
stored: AuthFile,
|
|
195
|
-
flagKey: string | undefined,
|
|
196
|
-
fx: ResolveEffects,
|
|
197
|
-
): Resolved {
|
|
198
|
-
const access = providerAccess(spec);
|
|
199
|
-
const configured = configuredEffects(spec, fx);
|
|
200
|
-
if (!configured.ok) return configured;
|
|
201
|
-
fx = configured.effects;
|
|
202
|
-
if (flagKey !== undefined && flagKey.length > 0) {
|
|
203
|
-
const got = resolveKeyValue(flagKey, fx);
|
|
204
|
-
return got.ok ? { ok: true, key: got.key, source: "flag" } : got;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const entry = storedCredential(stored, spec.provider);
|
|
208
|
-
if (entry !== undefined) {
|
|
209
|
-
// Deliberately no fallback to the environment on failure. See the header.
|
|
210
|
-
if (entry.type === "oauth") {
|
|
211
|
-
if (spec.provider === "openai" && spec.api !== "openai-codex-responses") return { ok: false, reason: "ChatGPT OAuth belongs to openai-codex; choose an openai-codex model or configure an OpenAI API key" };
|
|
212
|
-
return { ok: true, key: entry.access, source: "auth.json" };
|
|
213
|
-
}
|
|
214
|
-
if (entry.type === "api_key") {
|
|
215
|
-
const got = resolveKeyValue(entry.key, fx);
|
|
216
|
-
return got.ok
|
|
217
|
-
? { ok: true, key: got.key, source: "auth.json" }
|
|
218
|
-
: { ok: false, reason: `${spec.provider} in auth.json: ${got.reason}` };
|
|
219
|
-
}
|
|
220
|
-
return assertNever(entry, "stored credential");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (access.apiKey !== undefined) {
|
|
224
|
-
const got = resolveKeyValue(access.apiKey, fx);
|
|
225
|
-
return got.ok ? { ok: true, key: got.key, source: "models.json" }
|
|
226
|
-
: { ok: false, reason: `${spec.provider} in models.json: could not resolve apiKey` };
|
|
227
|
-
}
|
|
228
|
-
if (access.auth === false || access.authHeader === false) return { ok: true, key: "", source: "models.json" };
|
|
229
|
-
if (access.auth === "ambient") {
|
|
230
|
-
if (spec.api !== "bedrock-converse-stream" && spec.api !== "google-vertex") return { ok: false, reason: `${spec.api} does not support ambient credentials` };
|
|
231
|
-
if (spec.api === "google-vertex" && (!fx.env("GOOGLE_CLOUD_PROJECT") || !fx.env("GOOGLE_CLOUD_LOCATION"))) return { ok: false, reason: `${spec.provider}: ambient Vertex authentication requires GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION` };
|
|
232
|
-
return { ok: true, key: "", source: "ambient" };
|
|
233
|
-
}
|
|
234
|
-
for (const variable of envVarsOf(spec.provider, spec.credentialEnv)) {
|
|
235
|
-
const value = fx.env(variable);
|
|
236
|
-
if (value !== undefined && value.length > 0) {
|
|
237
|
-
return { ok: true, key: value, source: "environment" };
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (spec.api === "bedrock-converse-stream" && (fx.env("AWS_PROFILE")
|
|
242
|
-
|| (fx.env("AWS_ACCESS_KEY_ID") && fx.env("AWS_SECRET_ACCESS_KEY"))
|
|
243
|
-
|| fx.env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") || fx.env("AWS_CONTAINER_CREDENTIALS_FULL_URI") || fx.env("AWS_WEB_IDENTITY_TOKEN_FILE"))) {
|
|
244
|
-
return { ok: true, key: "", source: "ambient" };
|
|
245
|
-
}
|
|
246
|
-
if (spec.api === "google-vertex" && fx.env("GOOGLE_APPLICATION_CREDENTIALS")
|
|
247
|
-
&& (fx.env("GOOGLE_CLOUD_PROJECT") || fx.env("GOOGLE_VERTEX_PROJECT") || fx.env("GCLOUD_PROJECT"))
|
|
248
|
-
&& (fx.env("GOOGLE_CLOUD_LOCATION") || fx.env("GOOGLE_VERTEX_LOCATION"))) return { ok: true, key: "", source: "ambient" };
|
|
249
|
-
|
|
250
|
-
return { ok: false, reason: describeMissingCredential(spec.provider) };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Environment variable each provider reads. Same names pi uses, so a machine
|
|
255
|
-
* already set up for one works for the other without re-exporting anything.
|
|
256
|
-
*/
|
|
257
|
-
export const ENV_VAR: Readonly<Record<string, string | readonly string[]>> = {
|
|
258
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
259
|
-
openai: "OPENAI_API_KEY",
|
|
260
|
-
google: ["GOOGLE_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
|
|
261
|
-
deepseek: "DEEPSEEK_API_KEY",
|
|
262
|
-
xai: "XAI_API_KEY",
|
|
263
|
-
groq: "GROQ_API_KEY",
|
|
264
|
-
cerebras: "CEREBRAS_API_KEY",
|
|
265
|
-
mistral: "MISTRAL_API_KEY",
|
|
266
|
-
openrouter: "OPENROUTER_API_KEY",
|
|
267
|
-
zai: "ZHIPU_API_KEY",
|
|
268
|
-
moonshotai: "MOONSHOT_API_KEY",
|
|
269
|
-
together: "TOGETHER_API_KEY",
|
|
270
|
-
"amazon-bedrock": "AWS_BEARER_TOKEN_BEDROCK",
|
|
271
|
-
"azure-openai-responses": ["AZURE_OPENAI_API_KEY", "AZURE_API_KEY"],
|
|
272
|
-
"google-vertex": "GOOGLE_CLOUD_API_KEY",
|
|
273
|
-
"kimi-coding": ["KIMI_CODING_API_KEY", "KIMI_API_KEY"],
|
|
274
|
-
"openai-codex": "OPENAI_CODEX_ACCESS_TOKEN",
|
|
275
|
-
"xai-grok": "GROK_ACCESS_TOKEN",
|
|
276
|
-
"ant-ling": "ANT_LING_API_KEY",
|
|
277
|
-
"qwen-token-plan": "QWEN_TOKEN_PLAN_API_KEY",
|
|
278
|
-
"qwen-token-plan-cn": "QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
279
|
-
"qwen-token-plan-individual": "QWEN_TOKEN_PLAN_API_KEY",
|
|
280
|
-
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
|
|
281
|
-
"zai-coding-cn": "ZAI_CODING_CN_API_KEY",
|
|
282
|
-
"fireworks": "FIREWORKS_API_KEY",
|
|
283
|
-
"typesafe": "TYPESAFE_API_KEY",
|
|
284
|
-
"radius": "RADIUS_API_KEY",
|
|
285
|
-
"opencode-go": "OPENCODE_API_KEY",
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
export function envVarsOf(provider: string, additional: readonly string[] = []): readonly string[] {
|
|
289
|
-
const v = ENV_VAR[provider];
|
|
290
|
-
// A provider name is free text in a user catalog; an environment variable
|
|
291
|
-
// is not. `kimi-coding` must become KIMI_CODING, or no shell can set it.
|
|
292
|
-
const declared = [...additional, ...(SOURCE_PROVIDERS[provider]?.env ?? [])].filter(name => /(?:API_KEY|TOKEN|_KEY)$/.test(name)
|
|
293
|
-
&& !/(?:SECRET_ACCESS_KEY|ACCESS_KEY_ID|RESOURCE|PROJECT|LOCATION)/.test(name));
|
|
294
|
-
const known = typeof v === "string" ? [v] : v ?? [];
|
|
295
|
-
const names = [...new Set([...known, ...declared])];
|
|
296
|
-
return names.length ? names : [`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`];
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export function envLabel(provider: string): string {
|
|
300
|
-
return envVarsOf(provider).join(" or ");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* What to say when there is no credential.
|
|
305
|
-
*
|
|
306
|
-
* Both routes, named concretely. The failure a person hits on first run
|
|
307
|
-
* should carry its own fix; sending them to read the docs for something this
|
|
308
|
-
* small is the difference between trying the tool and closing it.
|
|
309
|
-
*/
|
|
310
|
-
export function describeMissingCredential(provider: string): string {
|
|
311
|
-
const subscription = provider === "openai-codex" ? "codex" : provider === GROK_PROVIDER ? "grok" : provider === "github-copilot" ? "github-copilot" : undefined;
|
|
312
|
-
if (subscription) return `no credential for ${provider}. Run /provider ${subscription} to sign in, or export ${envLabel(provider)}`;
|
|
313
|
-
return (
|
|
314
|
-
`no credential for ${provider}. Run /provider ${provider} to add a key, ` +
|
|
315
|
-
`or export ${envLabel(provider)}`
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export interface FreshEffects extends RefreshEffects {
|
|
320
|
-
persist: (auth: AuthFile) => Promise<void>;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Resolve, refreshing an expired OAuth access token first.
|
|
325
|
-
*
|
|
326
|
-
* A dead refresh fails here and names `/provider <provider>`. It does not fall
|
|
327
|
-
* through to the environment: that is the same rule as a stored API key that
|
|
328
|
-
* cannot resolve. Otherwise a forgotten export quietly starts billing a
|
|
329
|
-
* different account.
|
|
330
|
-
*/
|
|
331
|
-
export async function ensureFreshCredential(
|
|
332
|
-
spec: ModelSpec,
|
|
333
|
-
stored: AuthFile,
|
|
334
|
-
flagKey: string | undefined,
|
|
335
|
-
fx: ResolveEffects,
|
|
336
|
-
oauth: FreshEffects,
|
|
337
|
-
): Promise<Resolved> {
|
|
338
|
-
const configured = configuredEffects(spec, fx);
|
|
339
|
-
if (!configured.ok) return configured;
|
|
340
|
-
if (flagKey !== undefined && flagKey.length > 0) {
|
|
341
|
-
return resolveCredential(spec, stored, flagKey, fx);
|
|
342
|
-
}
|
|
343
|
-
const entry = storedCredential(stored, spec.provider);
|
|
344
|
-
if (entry !== undefined && entry.type === "oauth") {
|
|
345
|
-
const resolved = resolveCredential(spec, stored, flagKey, fx);
|
|
346
|
-
if (!resolved.ok) return resolved;
|
|
347
|
-
if (oauth.now() < entry.expires) {
|
|
348
|
-
return { ok: true, key: entry.access, source: "auth.json" };
|
|
349
|
-
}
|
|
350
|
-
const refreshOpts: RefreshEffects = {
|
|
351
|
-
fetch: oauth.fetch,
|
|
352
|
-
now: oauth.now,
|
|
353
|
-
};
|
|
354
|
-
if (oauth.signal !== undefined) refreshOpts.signal = oauth.signal;
|
|
355
|
-
const refreshed = await refreshOAuthCredential(spec.provider, entry, refreshOpts);
|
|
356
|
-
if (!refreshed.ok) {
|
|
357
|
-
return {
|
|
358
|
-
ok: false,
|
|
359
|
-
reason: `${spec.provider} OAuth expired. Run /provider ${spec.provider} to sign in again.`,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
const next: AuthFile = { ...stored, [spec.provider]: refreshed.credential };
|
|
363
|
-
await oauth.persist(next);
|
|
364
|
-
return { ok: true, key: refreshed.credential.access, source: "auth.json" };
|
|
365
|
-
}
|
|
366
|
-
return resolveCredential(spec, stored, flagKey, fx);
|
|
367
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Running blind checks (feat-blind-checks): in a disposable clone of the
|
|
3
|
-
* final workspace, with the blind files copied in under `.xeno-blind/`. The
|
|
4
|
-
* files are verified against the digests frozen when the session began, so a
|
|
5
|
-
* directory edited since then is reported rather than silently run.
|
|
6
|
-
*/
|
|
7
|
-
import { cp, readFile } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import type { BlindChecks } from "../core/blind-checks.ts";
|
|
10
|
-
import { observeCommand } from "../store/command.ts";
|
|
11
|
-
import { cloneWorkspace } from "../store/workspace-clone.ts";
|
|
12
|
-
import { blobAddress } from "../store/blobs.ts";
|
|
13
|
-
|
|
14
|
-
export type BlindCheckResult = { id: string; command: string } & ({ exit: number } | { reason: string });
|
|
15
|
-
|
|
16
|
-
export async function runBlindChecks(input: { root: string; blind: BlindChecks; timeoutMs: number; maxOutputBytes: number;
|
|
17
|
-
outputStore: string; signal?: AbortSignal }): Promise<BlindCheckResult[]> {
|
|
18
|
-
const checks = input.blind.manifest.checks.map(check => ({ id: check.id, command: check.command }));
|
|
19
|
-
const unavailable = (reason: string) => checks.map(check => ({ ...check, reason }));
|
|
20
|
-
for (const [path, sha] of Object.entries(input.blind.files)) {
|
|
21
|
-
const bytes = await readFile(join(input.blind.dir, path)).catch(() => null);
|
|
22
|
-
if (bytes === null || blobAddress(bytes) !== sha) return unavailable(`blind check file ${path} changed or disappeared since the session began`);
|
|
23
|
-
}
|
|
24
|
-
const clone = await cloneWorkspace(input.root, input.signal);
|
|
25
|
-
if (!clone.ok) return unavailable(clone.reason);
|
|
26
|
-
try {
|
|
27
|
-
await cp(input.blind.dir, join(clone.path, ".xeno-blind"), { recursive: true });
|
|
28
|
-
const results: BlindCheckResult[] = [];
|
|
29
|
-
for (const check of checks) {
|
|
30
|
-
const captured = await observeCommand({ command: check.command, cwd: clone.path, outputStore: input.outputStore,
|
|
31
|
-
maxOutputBytes: input.maxOutputBytes, timeoutMs: input.timeoutMs, ...(input.signal ? { signal: input.signal } : {}) });
|
|
32
|
-
results.push(captured.stopped ? { ...check, reason: `the check did not finish (${captured.stopped})` } : { ...check, exit: captured.exit });
|
|
33
|
-
}
|
|
34
|
-
return results;
|
|
35
|
-
} finally { await clone.dispose(); }
|
|
36
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/** O.14: volatile ownership, never a replacement for the durable branch log. */
|
|
2
|
-
export class BranchJobs<T> {
|
|
3
|
-
private readonly jobs = new Map<number, { controller: AbortController; done: Promise<void>; value?: T; error?: string; detach(): void; ready: boolean }>();
|
|
4
|
-
|
|
5
|
-
get active(): number { return [...this.jobs.values()].filter(job => !job.ready).length; }
|
|
6
|
-
state(node: number): "running" | "ready" | undefined {
|
|
7
|
-
const job = this.jobs.get(node); return job ? job.ready ? "ready" : "running" : undefined;
|
|
8
|
-
}
|
|
9
|
-
nodes(): number[] { return [...this.jobs.keys()]; }
|
|
10
|
-
start(node: number, controller: AbortController, run: () => Promise<T>, detach: () => void): void {
|
|
11
|
-
if (this.jobs.has(node)) throw new Error("branch already owned");
|
|
12
|
-
const job: { controller: AbortController; done: Promise<void>; value?: T; error?: string; detach(): void; ready: boolean } = {
|
|
13
|
-
controller, ready: false, detach, done: Promise.resolve(),
|
|
14
|
-
};
|
|
15
|
-
this.jobs.set(node, job);
|
|
16
|
-
job.done = run().then(value => { job.value = value; }, () => { job.error = "branch finalization failed; inspect its original log"; })
|
|
17
|
-
.finally(() => { job.ready = true; job.detach(); });
|
|
18
|
-
}
|
|
19
|
-
cancel(node?: number): void {
|
|
20
|
-
for (const [id, job] of this.jobs) if (node === undefined || node === id) job.controller.abort();
|
|
21
|
-
}
|
|
22
|
-
async drain(node?: number): Promise<void> { await Promise.all([...this.jobs].filter(([id]) => node === undefined || node === id).map(([, job]) => job.done)); }
|
|
23
|
-
async wait(node: number, ms: number): Promise<{ value?: T; error?: string; running: boolean } | undefined> {
|
|
24
|
-
const job = this.jobs.get(node); if (!job) return undefined;
|
|
25
|
-
if (!Number.isFinite(ms) || ms < 0 || ms > 60_000) throw new RangeError("wait_ms must be between 0 and 60000");
|
|
26
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
-
try {
|
|
28
|
-
if (!job.ready && ms > 0) await Promise.race([job.done, new Promise<void>(resolve => { timer = setTimeout(resolve, ms); })]);
|
|
29
|
-
} finally { clearTimeout(timer); }
|
|
30
|
-
return { ...(job.value !== undefined ? { value: job.value } : {}), ...(job.error ? { error: job.error } : {}), running: !job.ready };
|
|
31
|
-
}
|
|
32
|
-
forget(node: number): void {
|
|
33
|
-
if (this.state(node) === "running") throw new Error("cannot forget a running branch");
|
|
34
|
-
this.jobs.delete(node);
|
|
35
|
-
}
|
|
36
|
-
}
|