@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/apps/startup.ts
DELETED
|
@@ -1,1677 +0,0 @@
|
|
|
1
|
-
import { loadBlindChecks } from "../store/blind-checks.ts";
|
|
2
|
-
import type { BlindChecks } from "../core/blind-checks.ts";
|
|
3
|
-
import { appSessionFields } from "./session-lifecycle.ts";
|
|
4
|
-
import { release } from "node:os";
|
|
5
|
-
import { codingWorkingProtocol } from "../core/continuation.ts";
|
|
6
|
-
import { taskGuidance } from "../core/prompt-policy.ts";
|
|
7
|
-
import { environmentSection, type SessionEnvironment } from "../core/environment.ts";
|
|
8
|
-
/**
|
|
9
|
-
* From argv to a session that can run. Credential order, model selection,
|
|
10
|
-
* --fresh and the leftover-log notice all live here so the CLI entry can
|
|
11
|
-
* stay a few calls -- and so those rules can be tested without starting
|
|
12
|
-
* a REPL.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { mkdir } from "node:fs/promises";
|
|
16
|
-
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
|
-
import { Session, systemClock } from "../runtime/loop.ts";
|
|
18
|
-
import { DEFAULT_PROVIDER, type ProviderConfig } from "../runtime/provider.ts";
|
|
19
|
-
import { createProvider, providerSessionId, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
|
|
20
|
-
import { ExtensionRegistry } from "../runtime/extensions.ts";
|
|
21
|
-
import { DISCOVERY_TOOL_NAME } from "../runtime/tool-discovery.ts";
|
|
22
|
-
import { declaredToolNames } from "../core/tool-discovery.ts";
|
|
23
|
-
import { CATALOG, outputTokenBudget, type ModelSpec } from "../runtime/model.ts";
|
|
24
|
-
import { asThinkingLevel, clampThinkingLevel, supportedThinkingLevels } from "../core/thinking.ts";
|
|
25
|
-
import { modelKey } from "../core/session-position.ts";
|
|
26
|
-
import { extensionFlagValue, type ExtensionFlag, type FlagInput } from "../core/extension-flags.ts";
|
|
27
|
-
import { scopedModels } from "../runtime/model-scope.ts";
|
|
28
|
-
import {
|
|
29
|
-
resolveCredential, envLabel, storedIsOAuth,
|
|
30
|
-
type AuthFile, type ResolveEffects, type Resolved, describeMissingCredential } from "../runtime/auth.ts";
|
|
31
|
-
import { authPath, configDir, readJsonFile } from "../store/config.ts";
|
|
32
|
-
import { findRipgrep, provisionRipgrep } from "../store/tool-binaries.ts";
|
|
33
|
-
import { packageLocation } from "../store/update-cache.ts";
|
|
34
|
-
import { installKind } from "../core/update-check.ts";
|
|
35
|
-
import { toolInstallAllowed } from "../core/ripgrep-release.ts";
|
|
36
|
-
import { loadUiResources, loadTextResources, type UiResources } from "../store/resources.ts";
|
|
37
|
-
import { discoverResources, type ResourceIndex } from "../store/resource-index.ts";
|
|
38
|
-
import { pickProjectTrust, pickStartupSession, runResourceConfig } from "./ui/standalone.ts";
|
|
39
|
-
import { PackageManager, type PackageCommand } from "../runtime/package-manager.ts";
|
|
40
|
-
import { loadAcceptanceContract } from "../store/acceptance.ts";
|
|
41
|
-
import type { AcceptanceContract } from "../core/acceptance.ts";
|
|
42
|
-
import { appendSkillsToSystem, type Skill } from "../core/skills.ts";
|
|
43
|
-
import { COMMANDS, resolveSlashCatalog, type SlashCatalog } from "./commands.ts";
|
|
44
|
-
import { createStyle, IDENTITY, type Style } from "../tui/style.ts";
|
|
45
|
-
import type { Settings } from "../core/settings.ts";
|
|
46
|
-
import { readModelSources, readCatalogCacheText, writeCatalogCache, catalogCachePath } from "../store/models.ts";
|
|
47
|
-
import { refreshRemoteCatalog } from "../runtime/models-fetch.ts";
|
|
48
|
-
import { readCursor, writeCursor, type Cursor } from "../store/log.ts";
|
|
49
|
-
import { getBlob } from "../store/blobs.ts";
|
|
50
|
-
import { parseSessionContext } from "../core/session-format.ts";
|
|
51
|
-
import { resolveLogDir, findSessionByPrefix, newestConversation, sessionsDir, type ResolveLogOpts } from "../store/sessions.ts";
|
|
52
|
-
import { recoverCut } from "../store/recovery.ts";
|
|
53
|
-
import { planSessionEntry, restoreSessionModel } from "../core/session-entry.ts";
|
|
54
|
-
import { loadContext, assembleSystemPrompt, formatNotice, type LoadedContext } from "../store/context-files.ts";
|
|
55
|
-
import {
|
|
56
|
-
projectTrust,
|
|
57
|
-
} from "../store/trust.ts";
|
|
58
|
-
import { stat } from "node:fs/promises";
|
|
59
|
-
import {
|
|
60
|
-
parseToolList, rulesWithAllowTools, selectTools, isToolProfile,
|
|
61
|
-
type ToolProfile,
|
|
62
|
-
type PermissionRequest, type PermissionRule,
|
|
63
|
-
} from "../core/permissions.ts";
|
|
64
|
-
import {
|
|
65
|
-
TOOL_SCHEMAS, TOOL_DEFINITIONS, modelToolSchema, DEFAULT_TOOL_CONTEXT, workspaceRoot,
|
|
66
|
-
type ApprovalChoice, type ToolContext,
|
|
67
|
-
} from "../runtime/tools.ts";
|
|
68
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
69
|
-
import {
|
|
70
|
-
contextLimits, DEFAULT_CONTEXT_POLICY, isContextPolicy, type ContextLimits, type ContextPolicy,
|
|
71
|
-
} from "../core/context-policy.ts";
|
|
72
|
-
import { colorEnabled, createPalette, describeError, stdinIsTty, stdoutIsTty, type Palette } from "./render.ts";
|
|
73
|
-
import { type ActionId, type Keymap } from "../core/keymap.ts";
|
|
74
|
-
import type { StartupInput } from "./startup-input.ts";
|
|
75
|
-
import { FoundationRuntime } from "../runtime/foundation.ts";
|
|
76
|
-
import { readFoundationConfig, readOutputSchema } from "../store/foundation-config.ts";
|
|
77
|
-
import { outputValidator } from "../runtime/output-schema.ts";
|
|
78
|
-
import { loadAgentProfileLayers } from "../runtime/agent-profiles.ts";
|
|
79
|
-
import { resolveAgentProfile, builtinAgentProfileLayer, type ResolvedAgentProfile } from "../core/agent-profile.ts";
|
|
80
|
-
import { workflowToolNames, workflowInstructions, isWorkflowMode, type WorkflowMode } from "../core/workflow-mode.ts";
|
|
81
|
-
import { resolveContextWorkspace } from "../store/context-files.ts";
|
|
82
|
-
|
|
83
|
-
export const SYSTEM_PROMPT = systemPromptFor(TOOL_SCHEMAS.map((tool) => tool.name));
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Tool schemas are the only capability inventory. A branch may inherit this
|
|
87
|
-
* stable policy while narrowing its tools; no parent tool recipe can leak.
|
|
88
|
-
*/
|
|
89
|
-
export function systemPromptFor(toolNames: readonly string[], env?: SessionEnvironment): string {
|
|
90
|
-
return [
|
|
91
|
-
"You are an agent working in a terminal.",
|
|
92
|
-
"The declared tool schemas define available capabilities, input units and execution lifecycles. Do not assume additional tools or infer permission from a tool's visibility.",
|
|
93
|
-
...(env === undefined ? [] : [environmentSection(env)]),
|
|
94
|
-
taskGuidance(),
|
|
95
|
-
codingWorkingProtocol(toolNames),
|
|
96
|
-
"Be concise. Do not narrate what you are about to do; do it, then say what happened.",
|
|
97
|
-
].join("\n\n");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** I/O half of the environment snapshot; the formatting is pure (core/environment.ts). */
|
|
101
|
-
export function sessionEnvironment(workspace: string): SessionEnvironment {
|
|
102
|
-
const now = new Date();
|
|
103
|
-
const pad = (n: number) => String(n).padStart(2, "0");
|
|
104
|
-
let git = false;
|
|
105
|
-
try {
|
|
106
|
-
git = Bun.spawnSync(["git", "rev-parse", "--is-inside-work-tree"], { cwd: workspace, stdout: "pipe", stderr: "ignore" })
|
|
107
|
-
.stdout.toString().trim() === "true";
|
|
108
|
-
} catch { /* no git on PATH is "not a repository" for the model's purposes */ }
|
|
109
|
-
return {
|
|
110
|
-
platform: `${process.platform} ${release()} (${process.arch})`,
|
|
111
|
-
shell: "bash (tool commands run with bash -lc)",
|
|
112
|
-
cwd: workspace,
|
|
113
|
-
date: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`,
|
|
114
|
-
git,
|
|
115
|
-
ripgrep: findRipgrep(configDir()) !== null,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Fills the rg gap in the background for interactive sessions only: scripts,
|
|
121
|
-
* protocol servers and test subprocesses must not reach the network on their
|
|
122
|
-
* own. Failure is silent because grep keeps its TypeScript engine; the next
|
|
123
|
-
* session's environment line reports what is actually there. The returned
|
|
124
|
-
* stop aborts the download, so quitting never waits for it.
|
|
125
|
-
*/
|
|
126
|
-
export function provisionToolsInBackground(args: Args): () => void {
|
|
127
|
-
const location = packageLocation();
|
|
128
|
-
if (!isInteractiveSession(args) || !toolInstallAllowed({ env: process.env, offline: args.offline === true, install: installKind(location.root, location.git) })) return () => {};
|
|
129
|
-
const controller = new AbortController();
|
|
130
|
-
void provisionRipgrep({ configDir: configDir(), signal: controller.signal, fetchBytes: async (url, signal) => {
|
|
131
|
-
const response = await fetch(url, { ...(signal ? { signal } : {}), redirect: "follow" });
|
|
132
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
133
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
134
|
-
} }).catch(() => { /* grep falls back to its own engine */ });
|
|
135
|
-
return () => controller.abort();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Session policy a CLI coding session differs from a benchmark run in.
|
|
140
|
-
* Benchmarks construct their own SessionConfig and keep the core defaults.
|
|
141
|
-
*/
|
|
142
|
-
export function cliSessionConfig(args: Args): { continuationAfter: number; workingSet: true; strictVerification: true; timeBudgetMs?: number } {
|
|
143
|
-
// Never within a request. A real Kimi K3 run with the reminder at reply six
|
|
144
|
-
// spent half its requests on notes after the work was already verified
|
|
145
|
-
// (fix-cli-continuation-noise). The reminder still fires where it earns its
|
|
146
|
-
// place: a resumed handoff that cannot continue, and boundary preparation.
|
|
147
|
-
return { continuationAfter: Infinity, workingSet: true, strictVerification: true, ...(args.timeBudgetMs === undefined ? {} : { timeBudgetMs: args.timeBudgetMs }) };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Pure: where each kind of state lives for this workspace. Tool observations
|
|
152
|
-
* are not listed: Session keeps them in `<logDir>/artifacts`, inside the
|
|
153
|
-
* session, so fork and copy carry them -- moving sessions moves them too.
|
|
154
|
-
*/
|
|
155
|
-
export function statePaths(workspace: string, stateDir?: string): { blobs: string; sessions: string } {
|
|
156
|
-
const root = stateDir ?? join(workspace, ".xeno");
|
|
157
|
-
return { blobs: join(root, "blobs"), sessions: join(root, "sessions") };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export type AskSlot = {
|
|
161
|
-
current?: (request: PermissionRequest) => Promise<ApprovalChoice>;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
export interface Args {
|
|
165
|
-
agentBase?: { model: string; provider: string; thinking: string; maxTurns: number; excludeTools: string[]; noTools: boolean; noBuiltinTools: boolean;
|
|
166
|
-
tools?: string[]; toolProfile?: ToolProfile; timeBudgetMs?: number; workflow?: WorkflowMode };
|
|
167
|
-
clientMcp?: import("../core/mcp-config.ts").McpServers;
|
|
168
|
-
workflow?: WorkflowMode;
|
|
169
|
-
agent?: string;
|
|
170
|
-
outputSchema?: string;
|
|
171
|
-
outputLastMessage?: string;
|
|
172
|
-
cliFlags?: readonly string[];
|
|
173
|
-
role?: ResolvedAgentProfile;
|
|
174
|
-
shellCheckpoints?: boolean;
|
|
175
|
-
workspace: string;
|
|
176
|
-
/**
|
|
177
|
-
* `--state-dir <dir>`: where every xeno write goes -- snapshots, tool
|
|
178
|
-
* observations and, unless --session-dir says otherwise, sessions. Absent
|
|
179
|
-
* means `<workspace>/.xeno`. Evaluations need it outside the workspace:
|
|
180
|
-
* patches and verifiers see whatever is written there.
|
|
181
|
-
*/
|
|
182
|
-
stateDir?: string;
|
|
183
|
-
/** `--time-budget <seconds>`, in ms: when to warn the model its run is nearly out of time. */
|
|
184
|
-
timeBudgetMs?: number;
|
|
185
|
-
/**
|
|
186
|
-
* `--path-root`: how far file tools may reach, absolute. Must contain the
|
|
187
|
-
* workspace. Fingerprints, snapshots and rewind stay workspace-scoped.
|
|
188
|
-
*/
|
|
189
|
-
pathRoot?: string;
|
|
190
|
-
logDir: string;
|
|
191
|
-
maxTurns: number;
|
|
192
|
-
prompt: string;
|
|
193
|
-
trigger: number;
|
|
194
|
-
hard: number;
|
|
195
|
-
contextPolicy?: ContextPolicy;
|
|
196
|
-
model: string;
|
|
197
|
-
provider: string;
|
|
198
|
-
apiKey: string;
|
|
199
|
-
listModels: boolean;
|
|
200
|
-
modelSearch?: string;
|
|
201
|
-
help?: boolean;
|
|
202
|
-
version?: boolean;
|
|
203
|
-
manualUpdateCheck?: boolean;
|
|
204
|
-
exportFile?: string;
|
|
205
|
-
offline?: boolean;
|
|
206
|
-
tuiMode?: "regular" | "fullscreen";
|
|
207
|
-
noThemes?: boolean;
|
|
208
|
-
themePaths?: string[];
|
|
209
|
-
sessionId?: string;
|
|
210
|
-
modelsCommand: boolean;
|
|
211
|
-
modelsRefresh: boolean;
|
|
212
|
-
fresh: boolean;
|
|
213
|
-
noContextFiles: boolean;
|
|
214
|
-
appendSystemPrompt: string[];
|
|
215
|
-
systemPrompt: string | null;
|
|
216
|
-
/** `--tools` allowlist. Omitted when the flag was not passed. */
|
|
217
|
-
tools?: string[];
|
|
218
|
-
/** New sessions default to coding; restored context keeps its frozen schemas. */
|
|
219
|
-
toolProfile?: ToolProfile;
|
|
220
|
-
acceptanceFile?: string;
|
|
221
|
-
/** feat-blind-checks: a directory outside the workspace holding blind.json and its fixtures. */
|
|
222
|
-
blindChecks?: string;
|
|
223
|
-
excludeTools: string[];
|
|
224
|
-
noTools: boolean;
|
|
225
|
-
/** `--no-builtin-tools`. False when the flag was not passed. */
|
|
226
|
-
noBuiltinTools: boolean;
|
|
227
|
-
allowTools: string[];
|
|
228
|
-
/** `--yolo` / `-y`: skip tool approval for this process only. */
|
|
229
|
-
yolo: boolean;
|
|
230
|
-
/** `--approve` true, `--no-approve` false. Omitted when neither. */
|
|
231
|
-
approve?: boolean;
|
|
232
|
-
configCommand?: boolean;
|
|
233
|
-
local?: boolean;
|
|
234
|
-
packageCommand?: PackageCommand;
|
|
235
|
-
packagePaths?: {source:string;path:string}[];
|
|
236
|
-
/**
|
|
237
|
-
* `-p` / `--print`. Distinct from a leftover positional prompt: only this
|
|
238
|
-
* flag reads piped stdin, so a one-shot that did not ask for a pipe cannot
|
|
239
|
-
* hang on an open stdin the caller never meant to send.
|
|
240
|
-
*/
|
|
241
|
-
print: boolean;
|
|
242
|
-
/** `--theme <name|path>`. Empty when the flag was not passed. */
|
|
243
|
-
theme: string;
|
|
244
|
-
/** `--thinking <level>`. Empty when the flag was not passed. */
|
|
245
|
-
thinking?: string;
|
|
246
|
-
/** `--models <patterns>`. Comma-separated; empty when the flag was not passed. */
|
|
247
|
-
models?: string;
|
|
248
|
-
modelsExplicit?: boolean;
|
|
249
|
-
noSkills: boolean;
|
|
250
|
-
noPromptTemplates: boolean;
|
|
251
|
-
noExtensions?: boolean;
|
|
252
|
-
extensionPaths?: string[];
|
|
253
|
-
extensionFlags?: Record<string, string | boolean>;
|
|
254
|
-
extensionArgv?: string[];
|
|
255
|
-
extensionInputs?: Record<string, FlagInput>;
|
|
256
|
-
skillPaths: string[];
|
|
257
|
-
promptTemplatePaths: string[];
|
|
258
|
-
/** `-c` / `--continue`. */
|
|
259
|
-
continueLatest?: boolean;
|
|
260
|
-
/**
|
|
261
|
-
* `-r` / `--resume`. A string is an id prefix; `true` is the selector
|
|
262
|
-
* with no id (list, do not guess).
|
|
263
|
-
*/
|
|
264
|
-
resume?: string | true;
|
|
265
|
-
/** `--session <path|id prefix>`. */
|
|
266
|
-
session?: string;
|
|
267
|
-
/** `--fork <path|id prefix>`. */
|
|
268
|
-
forkFrom?: string;
|
|
269
|
-
/** `--no-session`. */
|
|
270
|
-
noSession?: boolean;
|
|
271
|
-
/** `--session-dir <dir>`. */
|
|
272
|
-
sessionDir?: string;
|
|
273
|
-
/** `-n` / `--name`. */
|
|
274
|
-
sessionName?: string;
|
|
275
|
-
/** `--mode json`. Empty / omitted when the flag was not passed. */
|
|
276
|
-
mode?: string;
|
|
277
|
-
/** Explicit local adapter endpoint. An owned mode-0700 directory is required. */
|
|
278
|
-
controlSocket?: string;
|
|
279
|
-
/** `--verbose`. Expands the startup header's counts into lists. */
|
|
280
|
-
verbose?: boolean;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Names `parseArgs` actually accepts. Subcommand-only `--refresh` and
|
|
285
|
-
* `--check` are not here; listing them would make a top-level
|
|
286
|
-
* `--refresh` suggest itself while still being rejected.
|
|
287
|
-
*
|
|
288
|
-
* Keep in sync with the branches in `parseArgs`. A missing entry still
|
|
289
|
-
* parses; it just cannot be offered as a suggestion.
|
|
290
|
-
*/
|
|
291
|
-
const KNOWN_FLAGS: readonly string[] = [
|
|
292
|
-
"--workflow", "--agent", "--output-schema", "--output-last-message",
|
|
293
|
-
"--checkpoint-shell",
|
|
294
|
-
"--workspace", "-w",
|
|
295
|
-
"--path-root",
|
|
296
|
-
"--state-dir",
|
|
297
|
-
"--time-budget",
|
|
298
|
-
"--log",
|
|
299
|
-
"--max-turns",
|
|
300
|
-
"--trigger",
|
|
301
|
-
"--hard",
|
|
302
|
-
"--context-policy",
|
|
303
|
-
"--model", "-m",
|
|
304
|
-
"--provider",
|
|
305
|
-
"--api-key",
|
|
306
|
-
"--list-models",
|
|
307
|
-
"--fresh",
|
|
308
|
-
"--continue", "-c",
|
|
309
|
-
"--resume", "-r",
|
|
310
|
-
"--session",
|
|
311
|
-
"--fork",
|
|
312
|
-
"--no-session",
|
|
313
|
-
"--session-dir",
|
|
314
|
-
"--name", "-n",
|
|
315
|
-
"--no-context-files", "-nc",
|
|
316
|
-
"--system-prompt",
|
|
317
|
-
"--append-system-prompt",
|
|
318
|
-
"--tools", "-t",
|
|
319
|
-
"--tool-profile",
|
|
320
|
-
"--acceptance-file",
|
|
321
|
-
"--blind-checks",
|
|
322
|
-
"--exclude-tools", "-xt",
|
|
323
|
-
"--no-tools", "-nt",
|
|
324
|
-
"--no-builtin-tools",
|
|
325
|
-
"--allow-tools",
|
|
326
|
-
"--yolo", "-y",
|
|
327
|
-
"--approve", "-a",
|
|
328
|
-
"--no-approve", "-na",
|
|
329
|
-
"--theme",
|
|
330
|
-
"--thinking",
|
|
331
|
-
"--models",
|
|
332
|
-
"--skill",
|
|
333
|
-
"--prompt-template",
|
|
334
|
-
"--no-skills",
|
|
335
|
-
"--no-prompt-templates",
|
|
336
|
-
"--extension", "-e", "--no-extensions", "-ne", "--extension-flag",
|
|
337
|
-
"--mode",
|
|
338
|
-
"--control-socket",
|
|
339
|
-
"--verbose",
|
|
340
|
-
"--print", "-p",
|
|
341
|
-
"--help", "-h",
|
|
342
|
-
"--version", "-v", "--export", "--session-id", "--offline", "--tui-mode", "--use-theme", "--no-themes", "-ns", "-np", "-nbt",
|
|
343
|
-
];
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Levenshtein. Ten lines instead of a dependency. `--no-session` and
|
|
347
|
-
* `--no-sessions` are distance 1: the caller must suggest, never apply.
|
|
348
|
-
*/
|
|
349
|
-
function editDistance(a: string, b: string): number {
|
|
350
|
-
const n = b.length;
|
|
351
|
-
const prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
352
|
-
const cur = new Array<number>(n + 1);
|
|
353
|
-
for (let i = 1; i <= a.length; i++) {
|
|
354
|
-
cur[0] = i;
|
|
355
|
-
for (let j = 1; j <= n; j++) {
|
|
356
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
357
|
-
cur[j] = Math.min(prev[j]! + 1, cur[j - 1]! + 1, prev[j - 1]! + cost);
|
|
358
|
-
}
|
|
359
|
-
for (let j = 0; j <= n; j++) prev[j] = cur[j]!;
|
|
360
|
-
}
|
|
361
|
-
return prev[n]!;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function closestKnownFlag(flag: string): string {
|
|
365
|
-
let best = KNOWN_FLAGS[0]!;
|
|
366
|
-
let bestD = Infinity;
|
|
367
|
-
for (const known of KNOWN_FLAGS) {
|
|
368
|
-
const d = editDistance(flag, known);
|
|
369
|
-
if (d < bestD || (d === bestD && known < best)) {
|
|
370
|
-
bestD = d;
|
|
371
|
-
best = known;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return best;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* A warning that still exits 0 is how this bug hid: scripts treated a typo
|
|
379
|
-
* as success. Suggest the nearest name and stop. Never rewrite the argv.
|
|
380
|
-
*/
|
|
381
|
-
function rejectUnknownFlag(flag: string): never {
|
|
382
|
-
const suggestion = closestKnownFlag(flag);
|
|
383
|
-
console.error(`unknown flag: ${flag}`);
|
|
384
|
-
console.error(` did you mean ${suggestion}?`);
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
|
|
389
|
-
let selectedTheme: string | undefined;
|
|
390
|
-
let updateExtensions = false;
|
|
391
|
-
const args: Args = {
|
|
392
|
-
cliFlags: argv.filter(value => value.startsWith("-")),
|
|
393
|
-
// No default request cap (S.6 item 1): the stall watchdog catches a run
|
|
394
|
-
// repeating a cycle without evidence, and asks the user when one can answer
|
|
395
|
-
// (S.8); --max-turns or an agent budget sets a hard cap.
|
|
396
|
-
workspace: process.cwd(), logDir: "", maxTurns: Infinity, prompt: "",
|
|
397
|
-
// Overridable so a node boundary can be forced in a short session --
|
|
398
|
-
// otherwise cutting is only reachable after tens of thousands of tokens,
|
|
399
|
-
// which makes the most interesting path the least exercised one.
|
|
400
|
-
trigger: 0, hard: 0,
|
|
401
|
-
model: "", provider: "", apiKey: "", listModels: false, modelsCommand: false, modelsRefresh: false, fresh: false,
|
|
402
|
-
noContextFiles: false, appendSystemPrompt: [],
|
|
403
|
-
systemPrompt: null,
|
|
404
|
-
excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [], yolo: false,
|
|
405
|
-
print: false,
|
|
406
|
-
theme: "",
|
|
407
|
-
thinking: "",
|
|
408
|
-
models: "",
|
|
409
|
-
noSkills: false, noPromptTemplates: false, skillPaths: [], promptTemplatePaths: [],
|
|
410
|
-
continueLatest: false, session: "", forkFrom: "", noSession: false, sessionDir: "", sessionName: "",
|
|
411
|
-
mode: "", verbose: false,
|
|
412
|
-
};
|
|
413
|
-
for (let i = 0; i < argv.length; i++) {
|
|
414
|
-
const a = argv[i]!;
|
|
415
|
-
if (["--log", "--session", "--fork", "--session-dir", "--state-dir"].includes(a) &&
|
|
416
|
-
(!argv[i + 1] || argv[i + 1]!.startsWith("-"))) {
|
|
417
|
-
console.error(`${a} requires a value`);
|
|
418
|
-
process.exit(1);
|
|
419
|
-
}
|
|
420
|
-
if (a === "--workflow") { const mode = argv[++i]; if (!mode || !isWorkflowMode(mode)) throw Error("--workflow requires build or plan"); args.workflow = mode; }
|
|
421
|
-
else if (a === "--agent") { const name = argv[++i]; if (!name || name.startsWith("-")) throw Error("--agent requires a profile name"); args.agent = name; }
|
|
422
|
-
else if (a === "--output-schema" || a === "--output-last-message") { const path = argv[++i]; if (!path || path.startsWith("-")) throw Error(`${a} requires a file path`); if (a === "--output-schema") args.outputSchema = resolve(path); else args.outputLastMessage = resolve(path); }
|
|
423
|
-
else if (a === "--checkpoint-shell") args.shellCheckpoints = true;
|
|
424
|
-
else if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
|
|
425
|
-
else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
|
|
426
|
-
else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
|
|
427
|
-
else if (a === "--time-budget") {
|
|
428
|
-
const seconds = Number(argv[++i]);
|
|
429
|
-
if (!Number.isFinite(seconds) || seconds <= 0) throw new Error("--time-budget requires a positive number of seconds");
|
|
430
|
-
args.timeBudgetMs = seconds * 1000;
|
|
431
|
-
}
|
|
432
|
-
else if (a === "--log") args.logDir = argv[++i] ?? "";
|
|
433
|
-
else if (a === "--max-turns") args.maxTurns = Number(argv[++i] ?? args.maxTurns);
|
|
434
|
-
else if (a === "--trigger") args.trigger = Number(argv[++i] ?? 0);
|
|
435
|
-
else if (a === "--hard") args.hard = Number(argv[++i] ?? 0);
|
|
436
|
-
else if (a === "--context-policy") {
|
|
437
|
-
const policy = argv[++i];
|
|
438
|
-
if (!isContextPolicy(policy)) throw new Error("--context-policy requires windowed, segmented, continuous, or adaptive");
|
|
439
|
-
args.contextPolicy = policy;
|
|
440
|
-
}
|
|
441
|
-
else if (a === "--model" || a === "-m") args.model = argv[++i] ?? "";
|
|
442
|
-
else if (a === "--provider") args.provider = argv[++i] ?? "";
|
|
443
|
-
else if (a === "--api-key") args.apiKey = argv[++i] ?? "";
|
|
444
|
-
else if (a === "--list-models") { args.listModels = true; const next = argv[i + 1]; if (next && !next.startsWith("-") && !next.startsWith("@")) args.modelSearch = argv[++i]!; }
|
|
445
|
-
else if (a === "--version" || a === "-v") args.version = true;
|
|
446
|
-
else if (a === "--export") { const file = argv[++i]; if (!file || file.startsWith("-")) throw new Error("--export requires a session path"); args.exportFile = file; }
|
|
447
|
-
else if (a === "--session-id") { const id = argv[++i]; if (!id || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(id)) throw new Error("--session-id requires a plain session identifier"); args.sessionId = id; }
|
|
448
|
-
else if (a === "--offline") args.offline = true;
|
|
449
|
-
else if (a === "--tui-mode") { const mode = argv[++i]; if (mode !== "regular" && mode !== "fullscreen") throw new Error("--tui-mode requires regular or fullscreen"); args.tuiMode = mode; }
|
|
450
|
-
else if (a === "models" && i === 0) {
|
|
451
|
-
// Subcommand, not a prompt. Do not consume the rest: a later
|
|
452
|
-
// `--models-url` must still be an unknown-flag error, and `-h`
|
|
453
|
-
// must still print help.
|
|
454
|
-
args.modelsCommand = true;
|
|
455
|
-
}
|
|
456
|
-
else if (a === "config" && i === 0) args.configCommand = true;
|
|
457
|
-
else if ((a === "install" || a === "remove" || a === "update" || a === "list") && i === 0) args.packageCommand = {operation:a,sources:[]};
|
|
458
|
-
else if ((a === "--local" || a === "-l") && (args.packageCommand || args.configCommand)) { args.local=true; if(args.packageCommand)args.packageCommand.local=true; }
|
|
459
|
-
else if (a === "--extensions" && args.packageCommand?.operation === "update") updateExtensions = true;
|
|
460
|
-
else if (a === "--check" && args.packageCommand?.operation === "update") args.manualUpdateCheck = true;
|
|
461
|
-
else if (a === "--refresh" && args.modelsCommand) {
|
|
462
|
-
args.modelsRefresh = true;
|
|
463
|
-
}
|
|
464
|
-
else if (a === "--fresh") args.fresh = true;
|
|
465
|
-
else if (a === "-c" || a === "--continue") args.continueLatest = true;
|
|
466
|
-
else if (a === "-r" || a === "--resume") {
|
|
467
|
-
const next = argv[i + 1];
|
|
468
|
-
if (next !== undefined && !next.startsWith("-")) args.resume = argv[++i]!;
|
|
469
|
-
else args.resume = true;
|
|
470
|
-
}
|
|
471
|
-
else if (a === "--session") args.session = argv[++i] ?? "";
|
|
472
|
-
else if (a === "--fork") args.forkFrom = argv[++i] ?? "";
|
|
473
|
-
else if (a === "--no-session") args.noSession = true;
|
|
474
|
-
else if (a === "--session-dir") args.sessionDir = argv[++i] ?? "";
|
|
475
|
-
else if (a === "-n" || a === "--name") args.sessionName = argv[++i] ?? "";
|
|
476
|
-
else if (a === "--no-context-files" || a === "-nc") args.noContextFiles = true;
|
|
477
|
-
else if (a === "--system-prompt") args.systemPrompt = argv[++i] ?? "";
|
|
478
|
-
else if (a === "--append-system-prompt") args.appendSystemPrompt.push(argv[++i] ?? "");
|
|
479
|
-
else if (a === "--tools" || a === "-t") args.tools = parseToolList(argv[++i] ?? "");
|
|
480
|
-
else if (a === "--blind-checks") {
|
|
481
|
-
const path = argv[++i]; if (!path || path.startsWith("--")) throw new Error("--blind-checks requires a directory outside the workspace");
|
|
482
|
-
args.blindChecks = path;
|
|
483
|
-
}
|
|
484
|
-
else if (a === "--acceptance-file") {
|
|
485
|
-
const path = argv[++i]; if (!path || path.startsWith("--")) throw new Error("--acceptance-file requires a JSON path");
|
|
486
|
-
args.acceptanceFile = path;
|
|
487
|
-
}
|
|
488
|
-
else if (a === "--tool-profile") {
|
|
489
|
-
const value = argv[++i] ?? "";
|
|
490
|
-
if (!isToolProfile(value)) throw new Error("unknown tool profile; choose coding, readonly, research, data, foundation or full");
|
|
491
|
-
args.toolProfile = value;
|
|
492
|
-
}
|
|
493
|
-
else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
|
|
494
|
-
else if (a === "--no-tools" || a === "-nt") args.noTools = true;
|
|
495
|
-
else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
|
|
496
|
-
else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
|
|
497
|
-
else if (a === "--yolo" || a === "-y") args.yolo = true;
|
|
498
|
-
else if (a === "--approve" || a === "-a") args.approve = true;
|
|
499
|
-
else if (a === "--no-approve" || a === "-na") args.approve = false;
|
|
500
|
-
else if (a === "--theme") {
|
|
501
|
-
const theme = argv[++i]; if (!theme || theme.startsWith("-")) throw new Error("--theme requires a name or path");
|
|
502
|
-
if (theme.includes("/") || theme.includes("\\") || theme.endsWith(".json")) { (args.themePaths ??= []).push(theme); if (theme.endsWith(".json")) args.theme = theme; }
|
|
503
|
-
else args.theme = theme;
|
|
504
|
-
}
|
|
505
|
-
else if (a === "--use-theme") { const theme = argv[++i]; if (!theme || theme.startsWith("-")) throw new Error("--use-theme requires a name"); selectedTheme = theme; }
|
|
506
|
-
else if (a === "--no-themes") args.noThemes = true;
|
|
507
|
-
else if (a === "--thinking") args.thinking = argv[++i] ?? "";
|
|
508
|
-
else if (a === "--models") { const value = argv[++i]; if (value === undefined || value.startsWith("--")) throw new Error("--models requires a value"); args.models = value; args.modelsExplicit = true; }
|
|
509
|
-
else if (a === "--skill") args.skillPaths.push(argv[++i] ?? "");
|
|
510
|
-
else if (a === "--prompt-template") args.promptTemplatePaths.push(argv[++i] ?? "");
|
|
511
|
-
else if (a === "--no-skills" || a === "-ns") args.noSkills = true;
|
|
512
|
-
else if (a === "--no-prompt-templates" || a === "-np") args.noPromptTemplates = true;
|
|
513
|
-
else if (a === "--no-extensions" || a === "-ne") args.noExtensions = true;
|
|
514
|
-
else if (a === "--extension" || a === "-e") {
|
|
515
|
-
const path = argv[++i]; if (!path || path.startsWith("-")) throw new Error("--extension requires a path");
|
|
516
|
-
(args.extensionPaths ??= []).push(path);
|
|
517
|
-
}
|
|
518
|
-
else if (a === "--extension-flag") {
|
|
519
|
-
const value = argv[++i] ?? "", at = value.indexOf("=");
|
|
520
|
-
if (at <= 0) throw new Error("--extension-flag requires name=value");
|
|
521
|
-
(args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[value.slice(0, at)] = value.slice(at + 1) === "true" ? true : value.slice(at + 1) === "false" ? false : value.slice(at + 1);
|
|
522
|
-
}
|
|
523
|
-
else if (a === "--mode") args.mode = argv[++i] ?? "";
|
|
524
|
-
else if (a === "--control-socket") {
|
|
525
|
-
const value = argv[++i];
|
|
526
|
-
if (!value || value.startsWith("-")) throw Error("--control-socket requires a path");
|
|
527
|
-
args.controlSocket = resolve(value);
|
|
528
|
-
}
|
|
529
|
-
else if (a === "--verbose") args.verbose = true;
|
|
530
|
-
else if (a === "-p" || a === "--print") {
|
|
531
|
-
args.print = true;
|
|
532
|
-
}
|
|
533
|
-
else if (a === "--help" || a === "-h") {
|
|
534
|
-
if (options.deferExtensions) { args.help = true; continue; }
|
|
535
|
-
printCliHelp();
|
|
536
|
-
process.exit(0);
|
|
537
|
-
} else if (a === "--") {
|
|
538
|
-
// Everything after `--` is content, never a flag. The delimiter
|
|
539
|
-
// is consumed so a dashed token cannot be mistaken for an option.
|
|
540
|
-
for (let j = i + 1; j < argv.length; j++) {
|
|
541
|
-
args.prompt += (args.prompt ? " " : "") + argv[j]!;
|
|
542
|
-
}
|
|
543
|
-
break;
|
|
544
|
-
} else if (a.startsWith("-") && a !== "-") {
|
|
545
|
-
const equal = a.indexOf("="), name = a.slice(2, equal === -1 ? undefined : equal), definition = options.flags?.get(name);
|
|
546
|
-
if (a.startsWith("--") && (definition || options.deferExtensions)) {
|
|
547
|
-
const next = argv[i + 1], input: FlagInput = { inline: equal !== -1,
|
|
548
|
-
...(equal !== -1 ? { value: a.slice(equal + 1) } : next !== undefined && !next.startsWith("-") && !next.startsWith("@") ? { value: next } : {}) };
|
|
549
|
-
if (definition) {
|
|
550
|
-
const value = extensionFlagValue(name, definition, input);
|
|
551
|
-
if (value !== undefined) (args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[name] = value;
|
|
552
|
-
if (!input.inline && input.value !== undefined && (definition.type === "string" || input.value === "true" || input.value === "false")) i++;
|
|
553
|
-
} else {
|
|
554
|
-
args.extensionArgv = [...argv]; (args.extensionInputs ??= Object.create(null) as Record<string, FlagInput>)[name] = input;
|
|
555
|
-
if (!input.inline && input.value !== undefined) i++;
|
|
556
|
-
}
|
|
557
|
-
} else rejectUnknownFlag(a);
|
|
558
|
-
} else if(args.packageCommand)args.packageCommand.sources.push(a);
|
|
559
|
-
else args.prompt += (args.prompt ? " " : "") + a;
|
|
560
|
-
}
|
|
561
|
-
if (selectedTheme !== undefined) args.theme = selectedTheme;
|
|
562
|
-
if (args.manualUpdateCheck && (args.packageCommand!.sources.length || args.local || updateExtensions || args.prompt)) {
|
|
563
|
-
throw new Error("update --check cannot be combined with package sources, --local, or --extensions");
|
|
564
|
-
}
|
|
565
|
-
return args;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Command-line prompt first, piped stdin after. Both belong in the first
|
|
570
|
-
* user message; reversing that would make `cat README | xeno -p "总结"`
|
|
571
|
-
* look like the file was the instruction.
|
|
572
|
-
*/
|
|
573
|
-
export function mergePrintPrompt(prompt: string, stdinText: string): string {
|
|
574
|
-
if (stdinText.length === 0) return prompt;
|
|
575
|
-
if (prompt.length === 0) return stdinText;
|
|
576
|
-
return (prompt.endsWith("\n") ? prompt : `${prompt}\n`) + stdinText;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Drain stdin only when it is not a tty. Reading a tty would wait forever
|
|
581
|
-
* for a pipe that an interactive `-p` never has.
|
|
582
|
-
*
|
|
583
|
-
* `firstByteMs` bounds the wait for the FIRST byte, and is set when the
|
|
584
|
-
* command line already carries a prompt. An eval runner, an IDE or a parent
|
|
585
|
-
* script often hands a child a pipe it never writes and never closes; reading
|
|
586
|
-
* that to EOF hung the whole run before the banner (fix-print-stdin-hang).
|
|
587
|
-
* Claude Code applies the same 3s guard. Once data starts, it is read to EOF
|
|
588
|
-
* however slowly it arrives, so a slow producer is not truncated.
|
|
589
|
-
*/
|
|
590
|
-
export async function readPipedStdin(
|
|
591
|
-
input: { readonly isTTY?: boolean } & AsyncIterable<unknown> = process.stdin,
|
|
592
|
-
opts: { firstByteMs?: number; warn?: (line: string) => void } = {},
|
|
593
|
-
): Promise<string> {
|
|
594
|
-
if (input.isTTY === true) return "";
|
|
595
|
-
const chunks: Uint8Array[] = [];
|
|
596
|
-
const encoder = new TextEncoder();
|
|
597
|
-
const iterator = input[Symbol.asyncIterator]();
|
|
598
|
-
const push = (chunk: unknown) => {
|
|
599
|
-
if (typeof chunk === "string") chunks.push(encoder.encode(chunk));
|
|
600
|
-
else if (chunk instanceof Uint8Array) chunks.push(chunk);
|
|
601
|
-
};
|
|
602
|
-
let first: IteratorResult<unknown>;
|
|
603
|
-
if (opts.firstByteMs === undefined) first = await iterator.next();
|
|
604
|
-
else {
|
|
605
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
606
|
-
const timeout = new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), opts.firstByteMs); });
|
|
607
|
-
const raced = await Promise.race([iterator.next(), timeout]);
|
|
608
|
-
clearTimeout(timer);
|
|
609
|
-
if (raced === null) {
|
|
610
|
-
(opts.warn ?? ((line) => process.stderr.write(line + "\n")))(
|
|
611
|
-
`warning: no stdin data received in ${(opts.firstByteMs / 1000).toFixed(0)}s, proceeding without it; ` +
|
|
612
|
-
"redirect stdin from /dev/null to skip the wait");
|
|
613
|
-
// Stop holding the pipe open for the rest of the run.
|
|
614
|
-
if (input === process.stdin) process.stdin.pause();
|
|
615
|
-
return "";
|
|
616
|
-
}
|
|
617
|
-
first = raced;
|
|
618
|
-
}
|
|
619
|
-
for (let result = first; !result.done; result = await iterator.next()) push(result.value);
|
|
620
|
-
if (chunks.length === 0) return "";
|
|
621
|
-
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
export function isPrintMode(args: Args): boolean {
|
|
625
|
-
return args.print || args.prompt.length > 0;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
export function isJsonMode(args: Args): boolean {
|
|
629
|
-
return args.mode === "json";
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Most-used actions, in glance order. Keys are read off the map so an
|
|
634
|
-
* unbound action cannot appear as a hint (feat-status-line rule 6, reused
|
|
635
|
-
* here). Labels are not chords; a chord that is not in `byKey` is a bug.
|
|
636
|
-
*/
|
|
637
|
-
const HEADER_SHORTCUTS: readonly { action: ActionId; label: string }[] = [
|
|
638
|
-
{ action: "app.interrupt", label: "interrupt" },
|
|
639
|
-
{ action: "app.toggleTools", label: "tools" },
|
|
640
|
-
{ action: "app.toggleThinking", label: "think" },
|
|
641
|
-
{ action: "app.externalEditor", label: "editor" },
|
|
642
|
-
{ action: "app.exit", label: "quit" },
|
|
643
|
-
];
|
|
644
|
-
|
|
645
|
-
export function formatShortcutLine(keymap: Keymap): string {
|
|
646
|
-
const parts: string[] = [];
|
|
647
|
-
for (const { action, label } of HEADER_SHORTCUTS) {
|
|
648
|
-
const key = keymap.byAction[action]?.[0];
|
|
649
|
-
if (key === undefined) continue;
|
|
650
|
-
parts.push(`${key} ${label}`);
|
|
651
|
-
}
|
|
652
|
-
return parts.join(" · ");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
export function formatStartupHeader(input: {
|
|
656
|
-
readonly model: string;
|
|
657
|
-
readonly workspace: string;
|
|
658
|
-
readonly session: string;
|
|
659
|
-
readonly agents: readonly string[];
|
|
660
|
-
readonly skills: readonly string[];
|
|
661
|
-
readonly prompts: readonly string[];
|
|
662
|
-
readonly tools?: readonly string[];
|
|
663
|
-
readonly keymap: Keymap;
|
|
664
|
-
readonly verbose: boolean;
|
|
665
|
-
}): string[] {
|
|
666
|
-
const lines: string[] = [
|
|
667
|
-
input.model,
|
|
668
|
-
`workspace ${input.workspace}`,
|
|
669
|
-
`session ${input.session}`,
|
|
670
|
-
];
|
|
671
|
-
const groups: [string, readonly string[]][] = [["agents", input.agents], ["skills", input.skills], ["prompts", input.prompts],
|
|
672
|
-
...(input.tools !== undefined ? [["tools", input.tools] as [string, readonly string[]]] : [])];
|
|
673
|
-
if (input.verbose) {
|
|
674
|
-
for (const [label, items] of groups) {
|
|
675
|
-
lines.push(`${label.padEnd(10)}${items.length}`);
|
|
676
|
-
for (const item of items) lines.push(` ${item}`);
|
|
677
|
-
}
|
|
678
|
-
} else {
|
|
679
|
-
// Four count rows were a third of the banner; one row says the same.
|
|
680
|
-
lines.push(`loaded ${groups.map(([label, items]) => `${items.length} ${label}`).join(" · ")}`);
|
|
681
|
-
}
|
|
682
|
-
const keys = formatShortcutLine(input.keymap);
|
|
683
|
-
if (keys.length > 0) lines.push(`keys ${keys}`);
|
|
684
|
-
return lines;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function countCatalogDelta(
|
|
688
|
-
beforeText: string | null,
|
|
689
|
-
afterText: string | null,
|
|
690
|
-
): { added: number; changed: number } {
|
|
691
|
-
const ids = (text: string | null): Map<string, string> => {
|
|
692
|
-
const map = new Map<string, string>();
|
|
693
|
-
if (text === null || text.trim().length === 0) return map;
|
|
694
|
-
let json: unknown;
|
|
695
|
-
try {
|
|
696
|
-
json = JSON.parse(text);
|
|
697
|
-
} catch {
|
|
698
|
-
return map;
|
|
699
|
-
}
|
|
700
|
-
for (const spec of parseUserCatalog(json).models) {
|
|
701
|
-
map.set(modelKey(spec), JSON.stringify(spec));
|
|
702
|
-
}
|
|
703
|
-
return map;
|
|
704
|
-
};
|
|
705
|
-
const before = ids(beforeText);
|
|
706
|
-
const after = ids(afterText);
|
|
707
|
-
let added = 0;
|
|
708
|
-
let changed = 0;
|
|
709
|
-
for (const [id, body] of after) {
|
|
710
|
-
const prev = before.get(id);
|
|
711
|
-
if (prev === undefined) added += 1;
|
|
712
|
-
else if (prev !== body) changed += 1;
|
|
713
|
-
}
|
|
714
|
-
return { added, changed };
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Explicit catalog refresh. Download and validate before the write; a
|
|
719
|
-
* failure returns a reason and must not have touched the existing file.
|
|
720
|
-
* Exit status is the caller's: the CLI process exits non-zero, `/models
|
|
721
|
-
* refresh` only prints.
|
|
722
|
-
*/
|
|
723
|
-
export async function runModelsRefresh(opts: {
|
|
724
|
-
fetch: CatalogFetch;
|
|
725
|
-
write: (value: unknown) => Promise<void>;
|
|
726
|
-
}): Promise<
|
|
727
|
-
| { ok: true; count: number; added: number; changed: number }
|
|
728
|
-
| { ok: false; reason: string }
|
|
729
|
-
> {
|
|
730
|
-
const before = await readCatalogCacheText();
|
|
731
|
-
const result = await refreshRemoteCatalog({
|
|
732
|
-
fetch: opts.fetch,
|
|
733
|
-
write: opts.write,
|
|
734
|
-
});
|
|
735
|
-
if (!result.ok) return result;
|
|
736
|
-
const after = await readCatalogCacheText();
|
|
737
|
-
const delta = countCatalogDelta(before.text, after.text);
|
|
738
|
-
return { ok: true, count: result.count, added: delta.added, changed: delta.changed };
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* The two effects credential resolution needs. Real ones here; the tests
|
|
743
|
-
* inject fakes, which is why the priority rules can be pinned offline.
|
|
744
|
-
*
|
|
745
|
-
* A key command runs once per process. Re-running `security find-generic-
|
|
746
|
-
* password` on every request would spend a keychain round trip per turn to
|
|
747
|
-
* re-learn something that has not changed.
|
|
748
|
-
*/
|
|
749
|
-
const commandCache = new Map<string, string>();
|
|
750
|
-
|
|
751
|
-
export function processEffects(): ResolveEffects {
|
|
752
|
-
return {
|
|
753
|
-
env: (name) => process.env[name],
|
|
754
|
-
run: (command) => {
|
|
755
|
-
const cached = commandCache.get(command);
|
|
756
|
-
if (cached !== undefined) return cached;
|
|
757
|
-
const p = Bun.spawnSync(["bash", "-lc", command]);
|
|
758
|
-
if (p.exitCode !== 0) {
|
|
759
|
-
throw new Error(`exit ${p.exitCode}: ${new TextDecoder().decode(p.stderr).trim()}`);
|
|
760
|
-
}
|
|
761
|
-
const out = new TextDecoder().decode(p.stdout);
|
|
762
|
-
commandCache.set(command, out);
|
|
763
|
-
return out;
|
|
764
|
-
},
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Mutable I/O for a running app. The TUI replaces `say` so retry lines
|
|
770
|
-
* land in the transcript rather than punching through the live region.
|
|
771
|
-
* The object identity is stable: providers close over this reference.
|
|
772
|
-
*/
|
|
773
|
-
export interface AppIO {
|
|
774
|
-
palette: Palette;
|
|
775
|
-
say: (line: string) => void;
|
|
776
|
-
err: (line: string) => void;
|
|
777
|
-
/**
|
|
778
|
-
* Print mode only. The assistant's final text belongs on stdout; `say` is
|
|
779
|
-
* stderr there so a script cannot mistake a draft or a banner for the
|
|
780
|
-
* answer. Absent in interactive and piped-REPL runs, which still print
|
|
781
|
-
* the whole transcript through `say`.
|
|
782
|
-
*/
|
|
783
|
-
answer?: (text: string) => void;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
export interface App {
|
|
787
|
-
foundation?: FoundationRuntime;
|
|
788
|
-
args: Args;
|
|
789
|
-
session: Session;
|
|
790
|
-
workspace: string;
|
|
791
|
-
logDir: string;
|
|
792
|
-
blobDir: string;
|
|
793
|
-
system: string;
|
|
794
|
-
providerConfig: ProviderConfig;
|
|
795
|
-
/** May be `{ ok: false }` in an interactive session waiting on /provider. */
|
|
796
|
-
credential: Resolved;
|
|
797
|
-
auth: AuthFile;
|
|
798
|
-
effects: ResolveEffects;
|
|
799
|
-
loadedContext: LoadedContext;
|
|
800
|
-
credentialOf: (spec: ModelSpec) => Resolved;
|
|
801
|
-
catalog: readonly ModelSpec[];
|
|
802
|
-
io: AppIO;
|
|
803
|
-
/**
|
|
804
|
-
* The TUI writes `current` while it can ask. One-shot and piped runs
|
|
805
|
-
* leave it empty, so an ask becomes a named refusal.
|
|
806
|
-
*/
|
|
807
|
-
askSlot: AskSlot;
|
|
808
|
-
ui?: UiState;
|
|
809
|
-
slash?: SlashCatalog;
|
|
810
|
-
resources?: ResourceIndex;
|
|
811
|
-
projectTrusted?: boolean;
|
|
812
|
-
extensions?: ExtensionRegistry;
|
|
813
|
-
shutdownRequested?: boolean;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
export interface UiState {
|
|
817
|
-
settings: Settings;
|
|
818
|
-
theme: UiResources["theme"];
|
|
819
|
-
keymap: UiResources["keymap"];
|
|
820
|
-
userOverrides: Partial<Settings>;
|
|
821
|
-
style: Style;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function toUiState(loaded: UiResources, color: boolean): UiState {
|
|
825
|
-
return {
|
|
826
|
-
settings: loaded.settings,
|
|
827
|
-
theme: loaded.theme,
|
|
828
|
-
keymap: loaded.keymap,
|
|
829
|
-
userOverrides: loaded.userOverrides,
|
|
830
|
-
style: color ? createStyle(loaded.theme.colors) : IDENTITY,
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/** `--models a,b` and settings.scopedModels share this split so a stray space cannot invent a pattern. */
|
|
835
|
-
export function splitModelPatterns(raw: string | undefined): string[] {
|
|
836
|
-
if (raw === undefined || raw.length === 0) return [];
|
|
837
|
-
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* One line saying where the key came from, or why there isn't one.
|
|
842
|
-
*
|
|
843
|
-
* When a stored entry is what failed, the reason has to be the stored entry
|
|
844
|
-
* -- naming the environment variable would offer a fix that this resolver
|
|
845
|
-
* will then deliberately ignore, because a stored credential owns its
|
|
846
|
-
* provider.
|
|
847
|
-
*/
|
|
848
|
-
export function whyNot(spec: ModelSpec, got: Resolved, auth: AuthFile, C: Palette): string {
|
|
849
|
-
if (got.ok) return C.dim(` via ${got.source}`);
|
|
850
|
-
if (auth[spec.provider] !== undefined) return C.yellow(` ${got.reason}`);
|
|
851
|
-
return C.dim(` ${envLabel(spec.provider)}`);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* /provider only exists inside a live session. A one-shot prompt and a pipe
|
|
856
|
-
* cannot answer it, so those runs must still refuse -- naming /provider there
|
|
857
|
-
* is how this file used to deadlock a first run.
|
|
858
|
-
*/
|
|
859
|
-
/**
|
|
860
|
-
* What a non-interactive run says when it has no key. A model named with
|
|
861
|
-
* --model is the only thing the caller asked about; listing every provider
|
|
862
|
-
* hides the one variable that would fix it.
|
|
863
|
-
*/
|
|
864
|
-
export function missingCredentialMessage(spec: ModelSpec, explicit: boolean, catalog: readonly ModelSpec[] = CATALOG): string {
|
|
865
|
-
return explicit ? `${spec.id}: ${describeMissingCredential(spec.provider)}` : describeUnauthenticatedScript(catalog);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
export function isInteractiveSession(args: Args): boolean {
|
|
869
|
-
return !args.print && !args.help && args.mode !== "json" && args.prompt.length === 0 && stdinIsTty() && stdoutIsTty();
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Script-facing missing-key text. Environment variables and --api-key are
|
|
874
|
-
* the routes that actually work without a tty; /provider is not one of them.
|
|
875
|
-
*/
|
|
876
|
-
export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CATALOG): string {
|
|
877
|
-
const providers = [...new Set(catalog.map((m) => m.provider))];
|
|
878
|
-
const vars = providers.map((p) => envLabel(p));
|
|
879
|
-
return (
|
|
880
|
-
`no model has a credential. Set an API key for one of: ${providers.join(", ")}` +
|
|
881
|
-
` -- export ${vars.join(" or ")}, or pass --api-key`
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Banner / hint for an interactive session that started without a key.
|
|
887
|
-
* Listing every provider's variable was a 20-line wall that buried the one
|
|
888
|
-
* action that works from here; the interactive routes are named instead.
|
|
889
|
-
*/
|
|
890
|
-
export function describeUnauthenticatedSession(_catalog: readonly ModelSpec[] = CATALOG): string {
|
|
891
|
-
return "no provider connected yet. Type /provider to connect one — a Claude, ChatGPT, Copilot or Grok account, an API key, a gateway or a local server — " +
|
|
892
|
-
`or export a key such as ${["anthropic", "openai", "google", "deepseek"].map(provider => envLabel(provider).split(" or ")[0]).join(", ")}.`;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
export function retryHandler(io: AppIO): (attempt: number, delayMs: number, error: unknown) => void {
|
|
896
|
-
return (attempt, delayMs, error) => {
|
|
897
|
-
const C = io.palette;
|
|
898
|
-
// A retry that prints nothing is indistinguishable from a hang, which is
|
|
899
|
-
// the problem retrying was supposed to remove rather than one to add.
|
|
900
|
-
io.say(
|
|
901
|
-
C.yellow(` retry ${attempt} in ${(delayMs / 1000).toFixed(1)}s`) +
|
|
902
|
-
C.dim(` ${describeError(error)}`),
|
|
903
|
-
);
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
export function toolSelectionOf(args: Args) {
|
|
908
|
-
return {
|
|
909
|
-
profile: args.toolProfile ?? "coding",
|
|
910
|
-
...(args.noTools ? { none: true as const } : {}),
|
|
911
|
-
...(args.tools !== undefined ? { only: args.tools } : {}),
|
|
912
|
-
...(args.excludeTools.length > 0 ? { exclude: args.excludeTools } : {}),
|
|
913
|
-
...(args.noBuiltinTools === true ? { noBuiltin: true as const } : {}),
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
export function processPermissionRules(args: Args, foundation?: FoundationRuntime, hostRules?: readonly PermissionRule[]): PermissionRule[] {
|
|
918
|
-
const configured = [...args.role?.profile.permissions ?? [], ...foundation?.permissions ?? []];
|
|
919
|
-
return [...(args.yolo ? configured.filter(rule => rule.action === "deny") : configured), ...(hostRules ?? rulesWithAllowTools(args.allowTools, { yolo: args.yolo }))];
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Schema assembly: every tool is tagged with a source here so
|
|
924
|
-
* `--no-builtin-tools` can drop a category without a later cross-layer
|
|
925
|
-
* recut. Optional extension descriptors remain local until selected; their
|
|
926
|
-
* harness-owned discovery entry belongs to that extension-enabled capability set.
|
|
927
|
-
*
|
|
928
|
-
* `source` is stripped before the frozen prefix: the wire schema is what
|
|
929
|
-
* the provider bills, and an extra field there would look like a tool
|
|
930
|
-
* argument.
|
|
931
|
-
*/
|
|
932
|
-
export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = [], builtinDefaults?: readonly string[]) {
|
|
933
|
-
const custom = [...extensions?.definitions().values() ?? []].map(tool => ({ ...tool, source: extensions?.builtinTools.has(tool.name) ? "builtin" as const : "extension" as const }));
|
|
934
|
-
// O.7/D6: a cold app open uses saved capabilities as defaults, never today's profile.
|
|
935
|
-
// Explicit selection still wins; extension loading and current exclusions remain live.
|
|
936
|
-
const retained = builtinDefaults !== undefined && args.tools === undefined && args.toolProfile === undefined ? new Set(builtinDefaults) : undefined;
|
|
937
|
-
const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) &&
|
|
938
|
-
(retained === undefined ? !tool.optional || args.tools?.includes(tool.name) : retained.has(tool.name))).map((t) => ({ ...t, source: "builtin" as const })),
|
|
939
|
-
...custom.filter(tool => tool.source !== "builtin" || retained === undefined || retained.has(tool.name))];
|
|
940
|
-
const admitted = new Set(workflowToolNames(available, args.workflow ?? "build"));
|
|
941
|
-
const selected = selectTools(available, { ...toolSelectionOf(args), ...(retained ? { profile: "full" as const } : {}) }).filter(tool => admitted.has(tool.name));
|
|
942
|
-
const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
|
|
943
|
-
.map(({ name, description, input_schema }) => ({ name, description, input_schema }));
|
|
944
|
-
const deferredNames = new Set(deferred.map(tool => tool.name));
|
|
945
|
-
const active = new Set([...declaredToolNames(previous), ...args.tools ?? []]);
|
|
946
|
-
const visible = selected.filter(tool => (!deferredNames.has(tool.name) || active.has(tool.name)) &&
|
|
947
|
-
(tool.name !== DISCOVERY_TOOL_NAME || deferred.length > 0 || args.tools?.includes(DISCOVERY_TOOL_NAME)));
|
|
948
|
-
// A cold open must not reorder an already accepted declaration and invent a checkpoint.
|
|
949
|
-
const order = new Map(declaredToolNames(previous).map((name, index) => [name, index]));
|
|
950
|
-
visible.sort((a, b) => (order.get(a.name) ?? Infinity) - (order.get(b.name) ?? Infinity));
|
|
951
|
-
const builtins = visible.filter(tool => tool.source === "builtin").map(tool => tool.name);
|
|
952
|
-
const schemas = visible.map(tool => tool.source === "builtin" ? modelToolSchema(tool, builtins)
|
|
953
|
-
: { name: tool.name, description: tool.description, input_schema: tool.input_schema });
|
|
954
|
-
// D6: extension authors retain their registration objects and SDK catalog views.
|
|
955
|
-
// Neither may mutate the already frozen provider declaration through an alias.
|
|
956
|
-
return { schemas: JSON.parse(JSON.stringify(schemas)) as typeof schemas,
|
|
957
|
-
deferred: JSON.parse(JSON.stringify(deferred)) as typeof deferred };
|
|
958
|
-
}
|
|
959
|
-
export function visibleSchemas(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
|
|
960
|
-
return sessionToolCatalog(args, extensions, previous).schemas;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
export function unknownToolNames(names: readonly string[]): string[] {
|
|
964
|
-
const known = new Set(TOOL_DEFINITIONS.map((t) => t.name));
|
|
965
|
-
return names.filter((n) => !known.has(n));
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
export function createSessionFor(opts: {
|
|
969
|
-
args: Args;
|
|
970
|
-
workspace: string;
|
|
971
|
-
logDir: string;
|
|
972
|
-
blobDir: string;
|
|
973
|
-
system: string;
|
|
974
|
-
providerConfig: ProviderConfig;
|
|
975
|
-
credential: Resolved;
|
|
976
|
-
io: AppIO;
|
|
977
|
-
askSlot: AskSlot;
|
|
978
|
-
oauth?: boolean;
|
|
979
|
-
effects?: ResolveEffects;
|
|
980
|
-
extensions?: ExtensionRegistry;
|
|
981
|
-
foundation?: FoundationRuntime;
|
|
982
|
-
acceptanceContract?: AcceptanceContract;
|
|
983
|
-
blindChecks?: BlindChecks;
|
|
984
|
-
skills?: readonly Skill[];
|
|
985
|
-
}): Session {
|
|
986
|
-
const { args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot } = opts;
|
|
987
|
-
const { schemas, deferred } = sessionToolCatalog(args, opts.extensions);
|
|
988
|
-
const names = schemas.map((t) => t.name);
|
|
989
|
-
const sessionRules: PermissionRule[] = [];
|
|
990
|
-
const tools: ToolContext = {
|
|
991
|
-
root: workspace,
|
|
992
|
-
...DEFAULT_TOOL_CONTEXT,
|
|
993
|
-
workflowMode: args.workflow ?? "build",
|
|
994
|
-
appOwnsPermissions: true,
|
|
995
|
-
...(opts.foundation ? { questionSlot: opts.foundation.questionSlot,
|
|
996
|
-
instructionCheck: (ctx: ToolContext, name: string, input: unknown) => opts.foundation!.instructions(ctx, name, input, args.noContextFiles) } : {}),
|
|
997
|
-
...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
|
|
998
|
-
allow: names,
|
|
999
|
-
deferredTools: deferred,
|
|
1000
|
-
rules: processPermissionRules(args, opts.foundation),
|
|
1001
|
-
sessionRules,
|
|
1002
|
-
askSlot,
|
|
1003
|
-
...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
|
|
1004
|
-
};
|
|
1005
|
-
return new Session(
|
|
1006
|
-
{
|
|
1007
|
-
logDir,
|
|
1008
|
-
frozen: { system, tools: schemas },
|
|
1009
|
-
blobDir,
|
|
1010
|
-
shellCheckpoints: args.shellCheckpoints === true,
|
|
1011
|
-
tools,
|
|
1012
|
-
maxTurns: args.maxTurns,
|
|
1013
|
-
...(args.agent ? { agentProfile: { name: args.agent, workflowExplicit: args.cliFlags?.includes("--workflow") === true } } : {}),
|
|
1014
|
-
...cliSessionConfig(args),
|
|
1015
|
-
...(opts.skills ? { skills: opts.skills } : {}),
|
|
1016
|
-
...(opts.acceptanceContract ? { acceptanceContract: opts.acceptanceContract } : {}),
|
|
1017
|
-
...(opts.blindChecks ? { blindChecks: opts.blindChecks } : {}),
|
|
1018
|
-
// Explicit legacy thresholds still select that experiment unless overridden.
|
|
1019
|
-
contextPolicy: args.contextPolicy ?? (args.trigger > 0 ? "segmented" : DEFAULT_CONTEXT_POLICY),
|
|
1020
|
-
...(args.trigger > 0
|
|
1021
|
-
? {
|
|
1022
|
-
boundary: {
|
|
1023
|
-
triggerTokens: args.trigger,
|
|
1024
|
-
hardTokens: args.hard || args.trigger * 2,
|
|
1025
|
-
// The work floor has to scale with the trigger: the two are
|
|
1026
|
-
// validated against each other, and a default floor of 5000 under
|
|
1027
|
-
// an experimental trigger of 1200 is a contradiction.
|
|
1028
|
-
minNodeWorkTokens: Math.max(1, Math.floor(args.trigger / 6)),
|
|
1029
|
-
},
|
|
1030
|
-
}
|
|
1031
|
-
: {}),
|
|
1032
|
-
},
|
|
1033
|
-
// Empty key is a placeholder the shell must not send through. Interactive
|
|
1034
|
-
// startup reaches this path so /provider exists; run() refuses until a real
|
|
1035
|
-
// credential is installed on the session.
|
|
1036
|
-
createProvider(
|
|
1037
|
-
providerConfig,
|
|
1038
|
-
credential.ok ? credential.key : "",
|
|
1039
|
-
retryHandler(io),
|
|
1040
|
-
{ sessionId: providerSessionId(logDir), ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
1041
|
-
),
|
|
1042
|
-
systemClock(),
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/** One location policy for startup, /new, /resume and /fork. */
|
|
1047
|
-
export function sessionDirFor(args: Args, workspace: string): string | undefined {
|
|
1048
|
-
if (args.sessionDir) return args.sessionDir;
|
|
1049
|
-
return args.stateDir === undefined ? undefined : statePaths(workspace, args.stateDir).sessions;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
type OpenSessionOptions = Omit<Parameters<typeof createSessionFor>[0], "providerConfig" | "credential" | "oauth" | "system"> & {
|
|
1053
|
-
startupPhase?: (name: string) => void;
|
|
1054
|
-
catalog: readonly ModelSpec[];
|
|
1055
|
-
auth: AuthFile;
|
|
1056
|
-
credentialOf: (spec: ModelSpec) => Resolved;
|
|
1057
|
-
interactive: boolean;
|
|
1058
|
-
settings?: Settings;
|
|
1059
|
-
/** CLI overrides belong to this open only; /resume must not inherit them. */
|
|
1060
|
-
modelFlags?: { model: string; provider: string };
|
|
1061
|
-
preparedResources?: { workspace: string; value: Awaited<ReturnType<typeof loadSessionResources>> };
|
|
1062
|
-
fallback?: ProviderConfig;
|
|
1063
|
-
projectTrusted?: boolean;
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
/** Pi replaces cwd-scoped resources with the session runtime; publish only after all reads succeed. */
|
|
1067
|
-
async function prepareExtensionPackages(args: Args, workspace: string): Promise<void> {
|
|
1068
|
-
const remote=(args.extensionPaths??[]).filter(path=>/^(npm:|git:|git\+|https?:\/\/|ssh:\/\/|git@)/.test(path));
|
|
1069
|
-
if(!remote.length)return;
|
|
1070
|
-
if(args.offline || process.env["XENO_OFFLINE"] === "1") throw new Error("--offline prevents remote extension package preparation; install it explicitly beforehand");
|
|
1071
|
-
const manager=new PackageManager({workspace});
|
|
1072
|
-
for(const source of remote)(args.packagePaths??=[]).push({source,path:await manager.temporary(source)});
|
|
1073
|
-
args.extensionPaths=(args.extensionPaths??[]).filter(path=>!remote.includes(path));
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
export async function loadSessionResources(args: Args, workspace: string, io: AppIO, trustOverride?: boolean, questionSlot?: import("../runtime/question-tool.ts").QuestionSlot): Promise<{
|
|
1077
|
-
loadedContext: LoadedContext; slash: SlashCatalog; ui: UiState; index: ResourceIndex; trusted: boolean; system: string; extensions: ExtensionRegistry; foundation: FoundationRuntime;
|
|
1078
|
-
}> {
|
|
1079
|
-
const { trusted } = await projectTrust(workspace, trustOverride ?? args.approve);
|
|
1080
|
-
const profiles = await loadAgentProfileLayers({ workspace, trustProject: trusted });
|
|
1081
|
-
for (const diagnostic of profiles.diagnostics) io.say(diagnostic);
|
|
1082
|
-
if (args.agent) {
|
|
1083
|
-
if (!args.agentBase) args.agentBase = { model: args.model, provider: args.provider, thinking: args.thinking ?? "", maxTurns: args.maxTurns, excludeTools: [...args.excludeTools], noTools: args.noTools, noBuiltinTools: args.noBuiltinTools,
|
|
1084
|
-
...(args.tools ? { tools: [...args.tools] } : {}), ...(args.toolProfile ? { toolProfile: args.toolProfile } : {}), ...(args.timeBudgetMs ? { timeBudgetMs: args.timeBudgetMs } : {}), ...(args.workflow ? { workflow: args.workflow } : {}) };
|
|
1085
|
-
const explicitWorkflow = args.cliFlags?.includes("--workflow") ? args.workflow : undefined;
|
|
1086
|
-
delete args.tools; delete args.toolProfile; delete args.timeBudgetMs; delete args.workflow;
|
|
1087
|
-
Object.assign(args, args.agentBase);
|
|
1088
|
-
if (explicitWorkflow) args.workflow = explicitWorkflow;
|
|
1089
|
-
if (profiles.diagnostics.length) throw Error("Agent profile configuration is invalid; fix the reported file before selecting a role");
|
|
1090
|
-
const role = resolveAgentProfile(args.agent, [builtinAgentProfileLayer(), ...profiles.layers]);
|
|
1091
|
-
if (!role) throw Error(`Unknown agent profile: ${args.agent}`);
|
|
1092
|
-
args.role = role;
|
|
1093
|
-
const profile = role.profile, explicit = (name: string) => args.cliFlags?.includes(name) === true;
|
|
1094
|
-
if (profile.mode && !explicit("--workflow")) args.workflow = profile.mode;
|
|
1095
|
-
if (profile.model) { if (!explicit("--model") && !explicit("-m")) args.model = profile.model.id; if (!explicit("--provider")) args.provider = profile.model.provider; if (profile.model.thinking && !explicit("--thinking")) args.thinking = profile.model.thinking; }
|
|
1096
|
-
if (profile.tools?.profile && !explicit("--tool-profile")) args.toolProfile = profile.tools.profile;
|
|
1097
|
-
if (profile.tools?.only && !explicit("--tools") && !explicit("-t")) args.tools = [...profile.tools.only];
|
|
1098
|
-
if (profile.tools?.exclude && !explicit("--exclude-tools") && !explicit("-xt")) args.excludeTools = [...profile.tools.exclude];
|
|
1099
|
-
if (profile.tools?.none !== undefined && !explicit("--no-tools")) args.noTools = profile.tools.none;
|
|
1100
|
-
if (profile.tools?.noBuiltin !== undefined && !explicit("--no-builtin-tools")) args.noBuiltinTools = profile.tools.noBuiltin;
|
|
1101
|
-
if (profile.budget?.maxTurns && !explicit("--max-turns")) args.maxTurns = profile.budget.maxTurns;
|
|
1102
|
-
if (profile.budget?.timeBudgetMs && !explicit("--time-budget")) args.timeBudgetMs = profile.budget.timeBudgetMs;
|
|
1103
|
-
}
|
|
1104
|
-
const foundationConfig = await readFoundationConfig(configDir(), workspace, trusted);
|
|
1105
|
-
if (args.clientMcp) { const { parseMcpServers } = await import("../core/mcp-config.ts"); foundationConfig["mcp"] = { ...parseMcpServers(foundationConfig["mcp"]), ...args.clientMcp }; }
|
|
1106
|
-
const foundation = new FoundationRuntime(workspace, foundationConfig, args.offline === true || process.env["XENO_OFFLINE"] === "1", configDir(), questionSlot);
|
|
1107
|
-
await prepareExtensionPackages(args,workspace);
|
|
1108
|
-
const index = await discoverResources({ ...args, workspace, trusted });
|
|
1109
|
-
const extensions = new ExtensionRegistry({ reserved: COMMANDS.map(command => command.name), reservedFlags: KNOWN_FLAGS.map(flag => flag.replace(/^-+/, "")),
|
|
1110
|
-
flagValue: (name, definition) => args.extensionFlags && Object.hasOwn(args.extensionFlags, name) ? args.extensionFlags[name] : extensionFlagValue(name, definition, args.extensionInputs && Object.hasOwn(args.extensionInputs, name) ? args.extensionInputs[name] : undefined), report: io.say });
|
|
1111
|
-
extensions.own(() => foundation.dispose());
|
|
1112
|
-
try {
|
|
1113
|
-
await foundation.register(extensions);
|
|
1114
|
-
await extensions.load(index);
|
|
1115
|
-
if(args.extensionArgv) {
|
|
1116
|
-
const validated = parseArgs(args.extensionArgv, { flags: extensions.flags, ...(args.help ? { deferExtensions: true } : {}) });
|
|
1117
|
-
if (validated.extensionArgv) rejectUnknownFlag("--" + Object.keys(validated.extensionInputs ?? {})[0]);
|
|
1118
|
-
args.prompt = validated.prompt; args.extensionFlags = validated.extensionFlags ?? {};
|
|
1119
|
-
}
|
|
1120
|
-
const loaded = await loadUiResources({ ...args, workspace, trusted, ...(args.theme ? { theme: args.theme } : {}) }, index);
|
|
1121
|
-
const instructionRoot = await resolveContextWorkspace(workspace);
|
|
1122
|
-
const loadedContext = await loadContext({ workspace: instructionRoot, cwd: workspace, configDir: configDir(),
|
|
1123
|
-
noContextFiles: args.noContextFiles, appendSystemPrompt: args.appendSystemPrompt,
|
|
1124
|
-
...(args.systemPrompt === null ? {} : { systemPrompt: args.systemPrompt }) });
|
|
1125
|
-
for (const notice of loadedContext.notices) if (notice.kind === "skipped" || notice.truncated) io.say(io.palette.yellow(formatNotice(notice)));
|
|
1126
|
-
const text = await loadTextResources({ workspace, configDir: configDir(), trusted, noSkills: args.noSkills,
|
|
1127
|
-
noPromptTemplates: args.noPromptTemplates, skillPaths: args.skillPaths, promptTemplatePaths: args.promptTemplatePaths }, index);
|
|
1128
|
-
const slash = resolveSlashCatalog(text.prompts, text.skills);
|
|
1129
|
-
for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
|
|
1130
|
-
const system = appendSkillsToSystem(assembleSystemPrompt([systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)), workflowInstructions(args.workflow ?? "build"), args.role?.profile.instructions ?? "",
|
|
1131
|
-
...(args.outputSchema ? ["Return the final answer as JSON matching this schema. Do not use Markdown fences:\n" + JSON.stringify(await readOutputSchema(args.outputSchema))] : [])].filter(Boolean).join("\n\n"), loadedContext), text.skills);
|
|
1132
|
-
const ui = toUiState(loaded, colorEnabled());
|
|
1133
|
-
// Also reached on /resume and /reload: `auto` is resolved once per process by the Tui (apps/shell.ts), not here.
|
|
1134
|
-
if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
|
|
1135
|
-
return { loadedContext, slash: { ...slash.catalog, skillExecution: true, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions, foundation };
|
|
1136
|
-
} catch (error) { await extensions.dispose().catch(() => {}); throw error; }
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/**
|
|
1140
|
-
* Pi's session-before-model order, with xeno's N.8 checkpoint on an explicit
|
|
1141
|
-
* model change. Opening never sends a request to the previous provider.
|
|
1142
|
-
* Callers publish the returned session/config/credential together on success.
|
|
1143
|
-
*/
|
|
1144
|
-
export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
1145
|
-
session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
|
|
1146
|
-
workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
|
|
1147
|
-
}> {
|
|
1148
|
-
const resourceSelector = () => JSON.stringify([opts.args.agent, opts.args.workflow, opts.args.toolProfile, opts.args.cliFlags]);
|
|
1149
|
-
const preparedSelector = resourceSelector();
|
|
1150
|
-
opts.startupPhase?.("Reading session history");
|
|
1151
|
-
const recorded = await readCursor(opts.logDir);
|
|
1152
|
-
const priorCursor = recorded === null ? null : await recoverCut(opts.logDir, recorded);
|
|
1153
|
-
// A durable selector restores intent, while current trusted files supply code
|
|
1154
|
-
// and permission authority. Missing/revoked project roles fail by name.
|
|
1155
|
-
if (priorCursor?.context_ref) {
|
|
1156
|
-
const savedContext = await getBlob(join(opts.logDir, "artifacts"), priorCursor.context_ref);
|
|
1157
|
-
if (!savedContext) throw Error("missing frozen session context");
|
|
1158
|
-
// S.3: the same parser Session.start uses; there is one reader of this format.
|
|
1159
|
-
const saved = parseSessionContext(JSON.parse(savedContext));
|
|
1160
|
-
const explicit = (...flags: string[]) => opts.args.cliFlags?.some(flag => flags.includes(flag)) === true;
|
|
1161
|
-
if (saved.agentProfile) {
|
|
1162
|
-
if (!explicit("--agent")) opts.args.agent = saved.agentProfile.name;
|
|
1163
|
-
if (saved.agentProfile.workflowExplicit && saved.workflowMode && !explicit("--workflow")) {
|
|
1164
|
-
opts.args.workflow = saved.workflowMode; opts.args.cliFlags = [...opts.args.cliFlags ?? [], "--workflow"];
|
|
1165
|
-
}
|
|
1166
|
-
} else if (saved.workflowMode && !explicit("--workflow")) opts.args.workflow = saved.workflowMode;
|
|
1167
|
-
if (saved.workflowToolProfile && !explicit("--tool-profile", "--tools", "-t", "--no-tools", "--no-builtin-tools")) opts.args.toolProfile = "foundation";
|
|
1168
|
-
}
|
|
1169
|
-
const workspace = priorCursor?.workspace ?? opts.workspace;
|
|
1170
|
-
if (opts.args.pathRoot) {
|
|
1171
|
-
const rel = relative(opts.args.pathRoot, workspace);
|
|
1172
|
-
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
|
|
1173
|
-
}
|
|
1174
|
-
const reusePrepared = opts.preparedResources?.workspace === workspace && preparedSelector === resourceSelector();
|
|
1175
|
-
if (opts.preparedResources && !reusePrepared) await opts.preparedResources.value.extensions.dispose();
|
|
1176
|
-
opts.startupPhase?.("Loading session resources");
|
|
1177
|
-
const resources = opts.preparedResources && reusePrepared ? opts.preparedResources.value
|
|
1178
|
-
: await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
|
|
1179
|
-
let openedSession: Session | undefined;
|
|
1180
|
-
try {
|
|
1181
|
-
const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
|
|
1182
|
-
const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
|
|
1183
|
-
const unknown = unknownToolNames(named).filter(name => !resources.extensions.definitions().has(name));
|
|
1184
|
-
if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
|
|
1185
|
-
const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
|
|
1186
|
-
const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
|
|
1187
|
-
if (opts.args.agent) { flags.model = opts.args.model; flags.provider = opts.args.provider; }
|
|
1188
|
-
const parsedModel = flags.model ? scopedModels([flags.model], flags.provider ? opts.catalog.filter(model => model.provider === flags.provider) : opts.catalog, true).entries[0] : undefined;
|
|
1189
|
-
if (parsedModel) flags.model = modelKey(parsedModel.model);
|
|
1190
|
-
const explicit = flags.model.length > 0 || flags.provider.length > 0;
|
|
1191
|
-
const settings = opts.settings ?? {};
|
|
1192
|
-
const preferred = saved ?? opts.fallback?.spec;
|
|
1193
|
-
opts.startupPhase?.("Resolving model credentials");
|
|
1194
|
-
const selected = !explicit && preferred !== undefined
|
|
1195
|
-
? { ok: true as const, spec: preferred }
|
|
1196
|
-
: selectModel(settings, flags, (spec) => opts.credentialOf(spec).ok, opts.catalog);
|
|
1197
|
-
let spec: ModelSpec;
|
|
1198
|
-
if (selected.ok) spec = selected.spec;
|
|
1199
|
-
else {
|
|
1200
|
-
const missingAll = selected.reason.startsWith("no model has a credential");
|
|
1201
|
-
if (!missingAll || !interactive) {
|
|
1202
|
-
throw new Error(missingAll ? describeUnauthenticatedScript(opts.catalog) : selected.reason);
|
|
1203
|
-
}
|
|
1204
|
-
const fallback = selectModel(settings, flags, () => true, opts.catalog);
|
|
1205
|
-
if (!fallback.ok) throw new Error(fallback.reason);
|
|
1206
|
-
spec = fallback.spec;
|
|
1207
|
-
}
|
|
1208
|
-
const credential = opts.credentialOf(spec);
|
|
1209
|
-
if (!credential.ok && !interactive) {
|
|
1210
|
-
throw new Error(credential.reason.startsWith("no credential for ")
|
|
1211
|
-
? missingCredentialMessage(spec, explicit || saved !== undefined, opts.catalog) : credential.reason);
|
|
1212
|
-
}
|
|
1213
|
-
const explicitEffort = asThinkingLevel(opts.args.thinking), requestedEffort = explicitEffort ?? parsedModel?.thinking;
|
|
1214
|
-
if (opts.args.thinking && (!explicitEffort || !supportedThinkingLevels(spec).includes(explicitEffort))) {
|
|
1215
|
-
throw new Error(`thinking ${opts.args.thinking} is not supported by ${modelKey(spec)}; supported: ${supportedThinkingLevels(spec).join(", ")}`);
|
|
1216
|
-
}
|
|
1217
|
-
const providerConfig: ProviderConfig = {
|
|
1218
|
-
spec, maxTokens: outputTokenBudget(spec),
|
|
1219
|
-
effort: clampThinkingLevel(spec, requestedEffort ?? asThinkingLevel(priorCursor?.thinking) ??
|
|
1220
|
-
settings.modelThinkingLevels?.[modelKey(spec)] ?? opts.fallback?.effort ?? asThinkingLevel(settings.defaultThinking) ??
|
|
1221
|
-
asThinkingLevel(settings.thinking) ?? DEFAULT_PROVIDER.effort),
|
|
1222
|
-
thinkingDisplay: opts.fallback?.thinkingDisplay ?? DEFAULT_PROVIDER.thinkingDisplay,
|
|
1223
|
-
};
|
|
1224
|
-
const restoring = saved ?? spec;
|
|
1225
|
-
const restoringCredential = saved === undefined ? credential : opts.credentialOf(restoring);
|
|
1226
|
-
const oauth = (model: ModelSpec, got: Resolved) => got.ok && storedIsOAuth(opts.auth, model.provider) && opts.args.apiKey.length === 0;
|
|
1227
|
-
const acceptanceContract = priorCursor === null
|
|
1228
|
-
? await loadAcceptanceContract(workspace, resources.trusted, opts.args.acceptanceFile) : undefined;
|
|
1229
|
-
// Frozen at session start like the contract; a resumed session reads its own copy from the saved context.
|
|
1230
|
-
const blindChecks = priorCursor === null && opts.args.blindChecks ? await loadBlindChecks(opts.args.blindChecks, workspace) : undefined;
|
|
1231
|
-
const session = createSessionFor({
|
|
1232
|
-
...opts,
|
|
1233
|
-
system: resources.system,
|
|
1234
|
-
extensions: resources.extensions,
|
|
1235
|
-
foundation: resources.foundation,
|
|
1236
|
-
skills: [...resources.slash.skills.values()],
|
|
1237
|
-
...(acceptanceContract ? { acceptanceContract } : {}),
|
|
1238
|
-
...(blindChecks ? { blindChecks } : {}),
|
|
1239
|
-
workspace: priorCursor?.workspace ?? opts.workspace,
|
|
1240
|
-
blobDir: priorCursor?.blob_dir ?? (priorCursor?.workspace && priorCursor.workspace !== opts.workspace
|
|
1241
|
-
? statePaths(priorCursor.workspace, opts.args.stateDir).blobs : opts.blobDir),
|
|
1242
|
-
providerConfig: { ...providerConfig, spec: restoring, maxTokens: outputTokenBudget(restoring) },
|
|
1243
|
-
credential: restoringCredential,
|
|
1244
|
-
...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
|
|
1245
|
-
});
|
|
1246
|
-
openedSession = session;
|
|
1247
|
-
opts.startupPhase?.("Opening session history");
|
|
1248
|
-
await session.start();
|
|
1249
|
-
const requestedWorkflow = opts.args.workflow;
|
|
1250
|
-
if (requestedWorkflow === undefined && session.config.tools.workflowMode) opts.args.workflow = session.config.tools.workflowMode;
|
|
1251
|
-
if (opts.args.outputSchema) session.config.validateOutput = outputValidator(await readOutputSchema(opts.args.outputSchema));
|
|
1252
|
-
resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
|
|
1253
|
-
const restoredTools = sessionToolCatalog(opts.args, resources.extensions, session.config.frozen.tools,
|
|
1254
|
-
priorCursor === null ? undefined : declaredToolNames(session.config.frozen.tools));
|
|
1255
|
-
const refreshResources = opts.args.agent !== undefined || opts.args.outputSchema !== undefined || opts.args.cliFlags?.includes("--workflow") === true || resources.extensions.loaded.length > 0 || session.config.frozen.tools.some(tool =>
|
|
1256
|
-
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length);
|
|
1257
|
-
const explicitTools = requestedWorkflow !== undefined || opts.args.agent !== undefined || opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
|
|
1258
|
-
opts.args.noBuiltinTools || opts.args.excludeTools.length > 0;
|
|
1259
|
-
const previousMode = session.config.tools.workflowMode, previousProfile = session.config.agentProfile, previousScope = session.config.workflowToolProfile;
|
|
1260
|
-
session.config.tools.workflowMode = opts.args.workflow ?? "build";
|
|
1261
|
-
if (opts.args.agent) session.config.agentProfile = { name: opts.args.agent, workflowExplicit: opts.args.cliFlags?.includes("--workflow") === true };
|
|
1262
|
-
if (opts.args.toolProfile === "foundation") session.config.workflowToolProfile = "foundation"; else delete session.config.workflowToolProfile;
|
|
1263
|
-
const metadataChanged = previousMode !== session.config.tools.workflowMode || JSON.stringify(previousProfile) !== JSON.stringify(session.config.agentProfile) || previousScope !== session.config.workflowToolProfile;
|
|
1264
|
-
if (refreshResources || (priorCursor !== null && (explicitTools || metadataChanged))) {
|
|
1265
|
-
await session.reloadResources({ system: refreshResources ? resources.system : session.config.frozen.system, tools: restoredTools.schemas }, undefined, undefined, metadataChanged);
|
|
1266
|
-
}
|
|
1267
|
-
// Admission follows this app's selected declaration; current process approval rules stay intact.
|
|
1268
|
-
// Keep this out of Session.start(): an SDK caller can intentionally supply a narrower allowlist.
|
|
1269
|
-
session.config.tools.allow = declaredToolNames(session.config.frozen.tools);
|
|
1270
|
-
session.config.tools.workflowMode = opts.args.workflow ?? "build";
|
|
1271
|
-
session.config.tools.deferredTools = restoredTools.deferred;
|
|
1272
|
-
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1273
|
-
const cut = await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
1274
|
-
{ sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1275
|
-
opts.io.say(opts.io.palette.yellow(` switched ${restoring.id} → ${spec.id}${cut ? " at a model checkpoint" : ""}`));
|
|
1276
|
-
}
|
|
1277
|
-
resources.ui.settings = { ...resources.ui.settings, thinking: providerConfig.effort,
|
|
1278
|
-
...(opts.args.modelsExplicit || opts.args.models ? { scopedModels: splitModelPatterns(opts.args.models) } : {}) };
|
|
1279
|
-
if (resources.ui.settings.scopedModels) for (const warning of scopedModels(resources.ui.settings.scopedModels, opts.catalog).diagnostics) opts.io.say(` ${warning}`);
|
|
1280
|
-
return { session, priorCursor, providerConfig, credential, workspace: session.config.tools.root,
|
|
1281
|
-
blobDir: session.config.blobDir ?? opts.blobDir, system: session.config.frozen.system, resources };
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
await openedSession?.dispose().catch(() => {});
|
|
1284
|
-
await resources.extensions.dispose().catch(() => {});
|
|
1285
|
-
throw error;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
1290
|
-
const withTerminal = <T>(run: (handoff: () => void) => Promise<T>): Promise<T> => input ? input.runWithTerminal(run) : run(() => {});
|
|
1291
|
-
if (args.extensionArgv && (args.listModels || args.modelsCommand || args.configCommand)) rejectUnknownFlag("--" + Object.keys(args.extensionInputs ?? {})[0]);
|
|
1292
|
-
const C = createPalette(colorEnabled());
|
|
1293
|
-
let printMode = isPrintMode(args);
|
|
1294
|
-
const jsonMode = isJsonMode(args);
|
|
1295
|
-
if ((args.mode ?? "").length > 0 && args.mode !== "json" && args.mode !== "text") {
|
|
1296
|
-
console.error(`unknown --mode ${args.mode} (supported: text, json)`);
|
|
1297
|
-
process.exit(1);
|
|
1298
|
-
}
|
|
1299
|
-
let divert = printMode || jsonMode;
|
|
1300
|
-
const io: AppIO = {
|
|
1301
|
-
palette: C,
|
|
1302
|
-
say: (line) => (divert ? console.error(line) : console.log(line)),
|
|
1303
|
-
err: (line) => console.error(line),
|
|
1304
|
-
...(printMode && !jsonMode
|
|
1305
|
-
? {
|
|
1306
|
-
answer: (text: string) => {
|
|
1307
|
-
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
1308
|
-
},
|
|
1309
|
-
}
|
|
1310
|
-
: {}),
|
|
1311
|
-
};
|
|
1312
|
-
const refreshOutputMode = () => {
|
|
1313
|
-
printMode = isPrintMode(args); divert = printMode || jsonMode;
|
|
1314
|
-
if (printMode && !jsonMode) io.answer = text => { process.stdout.write(text.endsWith("\n") ? text : `${text}\n`); };
|
|
1315
|
-
else delete io.answer;
|
|
1316
|
-
};
|
|
1317
|
-
const effects = processEffects();
|
|
1318
|
-
|
|
1319
|
-
const authRead = await readJsonFile<AuthFile>(authPath());
|
|
1320
|
-
if (authRead.error) io.say(C.yellow(` ${authRead.error}`));
|
|
1321
|
-
const auth: AuthFile = authRead.value ?? {};
|
|
1322
|
-
|
|
1323
|
-
if (args.modelsCommand) {
|
|
1324
|
-
if (!args.modelsRefresh) {
|
|
1325
|
-
io.say("usage: xeno models --refresh");
|
|
1326
|
-
io.say(C.dim(" fetches models.dev into ~/.xeno/models.json; startup never does this."));
|
|
1327
|
-
process.exit(0);
|
|
1328
|
-
}
|
|
1329
|
-
if (args.offline || process.env["XENO_OFFLINE"] === "1") throw new Error("--offline prevents catalog refresh");
|
|
1330
|
-
const result = await runModelsRefresh({
|
|
1331
|
-
fetch: globalThis.fetch,
|
|
1332
|
-
write: writeCatalogCache,
|
|
1333
|
-
});
|
|
1334
|
-
if (result.ok) {
|
|
1335
|
-
io.say(` added ${result.added}, changed ${result.changed} (${result.count} total) → ${catalogCachePath()}`);
|
|
1336
|
-
process.exit(0);
|
|
1337
|
-
} else {
|
|
1338
|
-
io.say(C.yellow(` ${result.reason}`));
|
|
1339
|
-
io.say(C.dim(" existing models.json left unchanged"));
|
|
1340
|
-
process.exit(1);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
// File only. Passing fetch here would make the test of D16 un-runnable.
|
|
1345
|
-
input?.phase("Loading model catalog");
|
|
1346
|
-
const loaded = loadCatalog(await readModelSources());
|
|
1347
|
-
for (const warning of loaded.warnings) io.say(C.yellow(` ${warning}`));
|
|
1348
|
-
const catalog = loaded.models;
|
|
1349
|
-
|
|
1350
|
-
const credentialOf = (spec: ModelSpec) => resolveCredential(spec, auth, args.apiKey, effects);
|
|
1351
|
-
|
|
1352
|
-
if (args.listModels) {
|
|
1353
|
-
io.say(C.bold("models"));
|
|
1354
|
-
const query = args.modelSearch?.toLowerCase();
|
|
1355
|
-
for (const group of groupedModels(query ? catalog.filter(model => [modelKey(model), model.name].some(text => { let at=0; for(const char of text.toLowerCase()) if(char===query[at])at++; return at===query.length; })) : catalog)) {
|
|
1356
|
-
io.say(C.bold(group.provider));
|
|
1357
|
-
for (const spec of group.models) {
|
|
1358
|
-
const got = credentialOf(spec);
|
|
1359
|
-
const mark = got.ok ? C.green("ready") : C.dim(" -- ");
|
|
1360
|
-
io.say(` ${mark} ${spec.id.padEnd(28)}${C.dim(spec.name.padEnd(28))}${whyNot(spec, got, auth, C)}`);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
io.say(C.dim("\n run one with --model <id>"));
|
|
1364
|
-
process.exit(0);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
/**
|
|
1368
|
-
* Resolving the workspace is the first thing that can fail, and it fails for
|
|
1369
|
-
* ordinary reasons -- a typo in `-w`, a directory that was moved. Reported as
|
|
1370
|
-
* a sentence rather than as a stat() stack trace.
|
|
1371
|
-
*
|
|
1372
|
-
* It has to happen before model selection so a trusted `.xeno/settings.json`
|
|
1373
|
-
* can override the default model. Trust does not affect tools.
|
|
1374
|
-
*/
|
|
1375
|
-
input?.phase("Opening workspace");
|
|
1376
|
-
let workspace: string;
|
|
1377
|
-
try {
|
|
1378
|
-
workspace = await workspaceRoot(args.workspace);
|
|
1379
|
-
} catch (err) {
|
|
1380
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1381
|
-
io.err(C.red(`\n cannot use ${args.workspace} as a workspace`));
|
|
1382
|
-
io.err(C.dim(` ${detail}\n`));
|
|
1383
|
-
process.exit(1);
|
|
1384
|
-
}
|
|
1385
|
-
// A path root that does not contain the workspace would confine relative
|
|
1386
|
-
// paths out of the very directory they resolve in.
|
|
1387
|
-
if (args.pathRoot !== undefined) {
|
|
1388
|
-
const rel = relative(args.pathRoot, workspace);
|
|
1389
|
-
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
1390
|
-
io.err(C.red(`\n --path-root ${args.pathRoot} does not contain the workspace ${workspace}\n`));
|
|
1391
|
-
process.exit(1);
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
input?.phase("Preparing extensions and project settings");
|
|
1396
|
-
await prepareExtensionPackages(args,workspace);
|
|
1397
|
-
const trust = await projectTrust(workspace, args.approve);
|
|
1398
|
-
let loadProjectSettings = trust.trusted;
|
|
1399
|
-
if (args.configCommand) {
|
|
1400
|
-
if (!stdinIsTty() || !stdoutIsTty()) {
|
|
1401
|
-
const index = await discoverResources({ ...args, workspace, trusted: loadProjectSettings });
|
|
1402
|
-
for (const entry of index.entries) io.say(`${entry.enabled ? "[x]" : "[ ]"} ${entry.kind} ${entry.scope}${entry.package?" · "+entry.package.source:""} ${entry.path}`);
|
|
1403
|
-
} else await withTerminal(handoff => runResourceConfig({ ...args, workspace, trusted: loadProjectSettings }, handoff));
|
|
1404
|
-
process.exit(0);
|
|
1405
|
-
}
|
|
1406
|
-
const hasProjectResources = (await Promise.all(["settings.json", "keybindings.json", "skills", "prompts", "themes", "extensions"].map(name => stat(join(workspace, ".xeno", name)).then(() => true, () => false)))).some(Boolean);
|
|
1407
|
-
if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await withTerminal(handoff => pickProjectTrust(workspace, handoff));
|
|
1408
|
-
if (hasProjectResources && !loadProjectSettings) io.say(C.dim(" Project resources not loaded (project is not trusted). /trust changes resource trust; tool permissions are separate."));
|
|
1409
|
-
|
|
1410
|
-
// Resolve the resource owner before factories run. Parsing flags must not load a different cwd first.
|
|
1411
|
-
let resourceWorkspace = workspace;
|
|
1412
|
-
const prepareFlags = (args.extensionArgv && args.resume !== true) || args.help;
|
|
1413
|
-
if (prepareFlags) {
|
|
1414
|
-
const target = args.logDir || args.session || args.forkFrom || (typeof args.resume === "string" ? args.resume : undefined);
|
|
1415
|
-
let resourceLog: string | undefined;
|
|
1416
|
-
if (target) { const found = await findSessionByPrefix(workspace, target, sessionDirFor(args, workspace)); if (found.ok) resourceLog = found.session.path; }
|
|
1417
|
-
else if (args.sessionId) resourceLog = join(sessionsDir(workspace, sessionDirFor(args, workspace)), args.sessionId);
|
|
1418
|
-
else if (args.continueLatest) resourceLog = (await newestConversation(workspace, sessionDirFor(args, workspace)))?.path;
|
|
1419
|
-
if (resourceLog) resourceWorkspace = (await readCursor(resourceLog))?.workspace ?? workspace;
|
|
1420
|
-
}
|
|
1421
|
-
input?.phase("Loading session resources");
|
|
1422
|
-
const preparedResources = prepareFlags
|
|
1423
|
-
? { workspace: resourceWorkspace, value: await loadSessionResources(args, resourceWorkspace, io, resourceWorkspace === workspace ? loadProjectSettings : undefined) } : undefined;
|
|
1424
|
-
if (args.help) { printCliHelp(preparedResources?.value.extensions.flags); await preparedResources?.value.extensions.dispose(); process.exit(0); }
|
|
1425
|
-
refreshOutputMode();
|
|
1426
|
-
const uiLoaded = preparedResources ? undefined : await loadUiResources({
|
|
1427
|
-
...args,
|
|
1428
|
-
workspace,
|
|
1429
|
-
trusted: loadProjectSettings,
|
|
1430
|
-
...(args.theme.length > 0 ? { theme: args.theme } : {}),
|
|
1431
|
-
});
|
|
1432
|
-
for (const line of uiLoaded?.diagnostics ?? []) io.say(C.yellow(` ${line}`));
|
|
1433
|
-
const ui = preparedResources?.value.ui ?? toUiState(uiLoaded!, colorEnabled());
|
|
1434
|
-
const mergedSettings = ui.settings;
|
|
1435
|
-
const scopedFromFlag = splitModelPatterns(args.models);
|
|
1436
|
-
if (args.modelsExplicit || scopedFromFlag.length > 0) {
|
|
1437
|
-
ui.settings = { ...ui.settings, scopedModels: scopedFromFlag };
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const paths = statePaths(workspace, args.stateDir);
|
|
1441
|
-
const sessionDir = sessionDirFor(args, workspace);
|
|
1442
|
-
let sessionOpts: ResolveLogOpts = {
|
|
1443
|
-
...(args.logDir ? { explicit: resolve(args.logDir) } : {}),
|
|
1444
|
-
fresh: args.fresh,
|
|
1445
|
-
...(args.continueLatest === undefined ? {} : { continueLatest: args.continueLatest }),
|
|
1446
|
-
...(args.noSession === undefined ? {} : { noSession: args.noSession }),
|
|
1447
|
-
...(args.session === undefined ? {} : { session: args.session }),
|
|
1448
|
-
...(args.sessionId === undefined ? {} : { sessionId: args.sessionId }),
|
|
1449
|
-
...(args.resume === undefined ? {} : { resume: args.resume }),
|
|
1450
|
-
...(args.forkFrom === undefined ? {} : { forkFrom: args.forkFrom }),
|
|
1451
|
-
...(sessionDir === undefined ? {} : { sessionDir }),
|
|
1452
|
-
};
|
|
1453
|
-
let logDir: string;
|
|
1454
|
-
try {
|
|
1455
|
-
if (planSessionEntry(sessionOpts).kind === "select") {
|
|
1456
|
-
if (printMode || jsonMode || !stdinIsTty() || !stdoutIsTty()) {
|
|
1457
|
-
throw new Error("--resume needs an interactive terminal. Pass -r <id> or --session <id> instead");
|
|
1458
|
-
}
|
|
1459
|
-
const picked = await withTerminal(handoff => pickStartupSession({ workspace, theme: ui.theme, say: io.say, keymap: ui.keymap,
|
|
1460
|
-
...(sessionDir === undefined ? {} : { sessionDir }) }, handoff));
|
|
1461
|
-
if (picked === null) { io.say(C.dim("No session selected")); process.exit(0); }
|
|
1462
|
-
sessionOpts = { explicit: picked };
|
|
1463
|
-
}
|
|
1464
|
-
const resolved = await resolveLogDir(workspace, sessionOpts);
|
|
1465
|
-
logDir = resolved.logDir;
|
|
1466
|
-
} catch (err) {
|
|
1467
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1468
|
-
io.err(C.red(`\n ${detail}\n`));
|
|
1469
|
-
process.exit(1);
|
|
1470
|
-
}
|
|
1471
|
-
// Snapshots are on by default here. They are optional in the loop because a
|
|
1472
|
-
// read-only branch has nothing to snapshot, but a session a person drives is
|
|
1473
|
-
// exactly the one that will want to go back, and the content has to have been
|
|
1474
|
-
// stored before the turn a person wants back -- it cannot be opted into after.
|
|
1475
|
-
const blobDir = paths.blobs;
|
|
1476
|
-
await mkdir(logDir, { recursive: true });
|
|
1477
|
-
await mkdir(blobDir, { recursive: true });
|
|
1478
|
-
|
|
1479
|
-
// Resource ownership follows the resolved session workspace. openSessionFor
|
|
1480
|
-
// loads and freezes it once; explicit reloads checkpoint a changed prefix (D6).
|
|
1481
|
-
const visible = visibleSchemas(args);
|
|
1482
|
-
|
|
1483
|
-
const askSlot: AskSlot = {};
|
|
1484
|
-
let opened: Awaited<ReturnType<typeof openSessionFor>>;
|
|
1485
|
-
try {
|
|
1486
|
-
opened = await openSessionFor({
|
|
1487
|
-
args, workspace, logDir, blobDir, io, askSlot, catalog, auth, credentialOf, effects,
|
|
1488
|
-
settings: mergedSettings, modelFlags: { model: args.model, provider: args.provider }, projectTrusted: loadProjectSettings,
|
|
1489
|
-
...(preparedResources ? { preparedResources } : {}),
|
|
1490
|
-
interactive: isInteractiveSession(args),
|
|
1491
|
-
...(input ? { startupPhase: input.phase } : {}),
|
|
1492
|
-
});
|
|
1493
|
-
} catch (err) {
|
|
1494
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1495
|
-
io.err(C.red(`\n cannot start a session in ${workspace}`));
|
|
1496
|
-
io.err(C.dim(` ${detail}\n`));
|
|
1497
|
-
process.exit(1);
|
|
1498
|
-
}
|
|
1499
|
-
const { session, priorCursor, providerConfig, credential } = opened;
|
|
1500
|
-
refreshOutputMode();
|
|
1501
|
-
|
|
1502
|
-
const sessionName = args.sessionName;
|
|
1503
|
-
if (sessionName !== undefined && sessionName.length > 0) {
|
|
1504
|
-
const cursor = await readCursor(logDir);
|
|
1505
|
-
if (cursor !== null) {
|
|
1506
|
-
await writeCursor(logDir, { ...cursor, name: sessionName });
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
if (opened.resources.ui.settings.quietStartup !== true || args.verbose) printBanner(args, io, {
|
|
1511
|
-
workspace: opened.workspace, logDir, providerConfig, credential, loadedContext: opened.resources.loadedContext, session, priorCursor, catalog,
|
|
1512
|
-
keymap: opened.resources.ui.keymap,
|
|
1513
|
-
skills: [...opened.resources.slash.skills.keys()],
|
|
1514
|
-
prompts: [...opened.resources.slash.prompts.keys()],
|
|
1515
|
-
tools: visible.map((t) => t.name),
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
return { args, logDir, auth, effects, credentialOf, catalog, io, askSlot, ...appSessionFields(opened) };
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
/** The banner's words for the shared limits; the loop acts at exactly these numbers. */
|
|
1522
|
-
export function boundaryDescription(policy: ContextPolicy, limits: ContextLimits): string {
|
|
1523
|
-
switch (limits.kind) {
|
|
1524
|
-
case "stop":
|
|
1525
|
-
case "open":
|
|
1526
|
-
return "continuous replay; stops at model window";
|
|
1527
|
-
case "window":
|
|
1528
|
-
return `windowed replay; prepare ${limits.prepare}, cut ${limits.trigger}, output reserved`;
|
|
1529
|
-
case "segment":
|
|
1530
|
-
return `${policy}${policy === "windowed" ? " (unknown window; fixed fallback)" : ""}; cut ${limits.trigger}, hard ${limits.hard}`;
|
|
1531
|
-
default:
|
|
1532
|
-
return assertNever(limits, "boundaryDescription");
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function printBanner(
|
|
1537
|
-
args: Args,
|
|
1538
|
-
io: AppIO,
|
|
1539
|
-
opts: {
|
|
1540
|
-
workspace: string;
|
|
1541
|
-
logDir: string;
|
|
1542
|
-
providerConfig: ProviderConfig;
|
|
1543
|
-
credential: Resolved;
|
|
1544
|
-
loadedContext: LoadedContext;
|
|
1545
|
-
session: Session;
|
|
1546
|
-
priorCursor: Cursor | null;
|
|
1547
|
-
catalog: readonly ModelSpec[];
|
|
1548
|
-
keymap: Keymap;
|
|
1549
|
-
skills: readonly string[];
|
|
1550
|
-
prompts: readonly string[];
|
|
1551
|
-
tools: readonly string[];
|
|
1552
|
-
},
|
|
1553
|
-
): void {
|
|
1554
|
-
const C = io.palette;
|
|
1555
|
-
const {
|
|
1556
|
-
workspace, logDir, providerConfig, credential, loadedContext, session, priorCursor, catalog,
|
|
1557
|
-
keymap, skills, prompts, tools,
|
|
1558
|
-
} = opts;
|
|
1559
|
-
io.say(
|
|
1560
|
-
C.bold("xeno") +
|
|
1561
|
-
C.dim(` ${providerConfig.spec.name} effort=${providerConfig.effort}`) +
|
|
1562
|
-
(credential.ok ? C.dim(` key from ${credential.source}`) : C.yellow(" no credential")),
|
|
1563
|
-
);
|
|
1564
|
-
const header = formatStartupHeader({
|
|
1565
|
-
model: providerConfig.spec.id,
|
|
1566
|
-
workspace,
|
|
1567
|
-
session: basename(logDir),
|
|
1568
|
-
agents: loadedContext.agents.map((f) => f.path),
|
|
1569
|
-
skills,
|
|
1570
|
-
prompts,
|
|
1571
|
-
tools,
|
|
1572
|
-
keymap,
|
|
1573
|
-
verbose: args.verbose === true,
|
|
1574
|
-
});
|
|
1575
|
-
for (const line of header) {
|
|
1576
|
-
// The first header line is the model; the banner already named it.
|
|
1577
|
-
if (line === providerConfig.spec.id) continue;
|
|
1578
|
-
io.say(C.dim(line));
|
|
1579
|
-
}
|
|
1580
|
-
io.say(C.dim(`log ${logDir}`));
|
|
1581
|
-
for (const notice of loadedContext.notices) {
|
|
1582
|
-
const line = formatNotice(notice);
|
|
1583
|
-
io.say(notice.kind === "skipped" ? C.yellow(` ${line}`) : C.dim(` ${line}`));
|
|
1584
|
-
}
|
|
1585
|
-
const policy = session.config.contextPolicy ?? DEFAULT_CONTEXT_POLICY;
|
|
1586
|
-
const limits = contextLimits({
|
|
1587
|
-
policy,
|
|
1588
|
-
contextWindow: session.provider.config.spec.contextWindow,
|
|
1589
|
-
outputReserve: session.provider.config.maxTokens,
|
|
1590
|
-
...(session.config.boundary ? { boundary: session.config.boundary } : {}),
|
|
1591
|
-
});
|
|
1592
|
-
io.say(C.dim(`boundary ${boundaryDescription(policy, limits)} (estimated tokens)`));
|
|
1593
|
-
if (priorCursor !== null) {
|
|
1594
|
-
const events = session.state.events.length;
|
|
1595
|
-
io.say(
|
|
1596
|
-
C.green(`resumed node ${session.state.node}`) +
|
|
1597
|
-
C.dim(` ${events} event${events === 1 ? "" : "s"} in context, last ${priorCursor.updated_at}`),
|
|
1598
|
-
);
|
|
1599
|
-
}
|
|
1600
|
-
if (!credential.ok) {
|
|
1601
|
-
io.say(C.yellow(` ${describeUnauthenticatedSession(catalog)}`));
|
|
1602
|
-
}
|
|
1603
|
-
io.say("");
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
1607
|
-
console.log(
|
|
1608
|
-
"usage: xeno [-m <model>] [--provider <name>] [--api-key <key>]\n" +
|
|
1609
|
-
" [-w <workspace>] [--log <dir>] [--max-turns N]\n" +
|
|
1610
|
-
" [--context-policy windowed|segmented|continuous|adaptive]\n" +
|
|
1611
|
-
" [--trigger N --hard N] [--list-models] [--fresh]\n" +
|
|
1612
|
-
" [-c|--continue] [-r|--resume [id]] [--session path|id]\n" +
|
|
1613
|
-
" [--fork path|id] [--no-session] [--session-dir dir]\n" +
|
|
1614
|
-
" [-n|--name name] [--session-id id] [--version|-v]\n" +
|
|
1615
|
-
" [models --refresh]\n" +
|
|
1616
|
-
" [install|remove <source>... [--local]] [update [source] [--extensions]] [list]\n" +
|
|
1617
|
-
" [update --check] Check for a newer xeno without installing (/update installs)\n" +
|
|
1618
|
-
" [config [--local]] [--export session [output.html]] [--offline]\n" +
|
|
1619
|
-
" [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
|
|
1620
|
-
" [--no-context-files|-nc]\n" +
|
|
1621
|
-
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
1622
|
-
" [--tool-profile coding|readonly|research|data|foundation|full]\n" +
|
|
1623
|
-
" [--workflow build|plan] [--agent name]\n" +
|
|
1624
|
-
" [--output-schema schema.json] [--output-last-message file]\n" +
|
|
1625
|
-
" [--acceptance-file contract.json] [--blind-checks dir]\n" +
|
|
1626
|
-
" [--control-socket /private/directory/control.sock]\n" +
|
|
1627
|
-
" [--no-builtin-tools]\n" +
|
|
1628
|
-
" [--allow-tools names] [--yolo|-y] [--approve|--no-approve]\n" +
|
|
1629
|
-
" [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
|
|
1630
|
-
" [--skill <path>] [--no-skills]\n" +
|
|
1631
|
-
" [--prompt-template <path>] [--no-prompt-templates]\n" +
|
|
1632
|
-
" [--extension <path>] [--no-extensions] [--extension-flag name=value]\n" +
|
|
1633
|
-
" [--mode text|json] [--tui-mode regular|fullscreen] [--verbose]\n" +
|
|
1634
|
-
" [-p <prompt>] [prompt...]\n\n" +
|
|
1635
|
-
" xeno app-server [session/model flags] runs the experimental JSONL control interface.\n" +
|
|
1636
|
-
" xeno app-server schema prints its versioned request schema without startup.\n" +
|
|
1637
|
-
" xeno acp [session/model flags] serves the standard ACP protocol over stdio.\n" +
|
|
1638
|
-
" xeno review [working|last-turn|branch <base>|commit <ref>] runs an independent read-only review.\n" +
|
|
1639
|
-
" --control-socket attaches local adapters to the same live session as the TUI.\n" +
|
|
1640
|
-
" /control opens goal, progress and context panels; XENO_PROGRAM_STATUS=auto|1|0 controls terminal status.\n\n" +
|
|
1641
|
-
" Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
|
|
1642
|
-
" -c continues the newest (or starts one); -r opens the session picker.\n" +
|
|
1643
|
-
" --session / -r <id> pick one (unique prefix only). Resume restores its model;\n" +
|
|
1644
|
-
" an explicit --model switches at a checkpoint before the next turn.\n" +
|
|
1645
|
-
" --no-session runs without writing .xeno/sessions/. --fork copies into a new one.\n" +
|
|
1646
|
-
" A leftover .xeno/log is not moved; continue it with --log .xeno/log.\n\n" +
|
|
1647
|
-
" With a prompt, runs it and exits. Without one, reads prompts from stdin.\n" +
|
|
1648
|
-
" Commands: " + COMMANDS.map(command => "/" + command.name).join(" ") + "\n\n" +
|
|
1649
|
-
" Credentials resolve in this order: --api-key, then ~/.xeno/auth.json,\n" +
|
|
1650
|
-
" then the provider's environment variable.\n" +
|
|
1651
|
-
" --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
|
|
1652
|
-
" --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
|
|
1653
|
-
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
1654
|
-
" New sessions keep coding (10 tools); foundation adds the new basic workflows.\n" +
|
|
1655
|
-
" --agent build|plan selects foundation; full exposes the complete builtin catalog.\n" +
|
|
1656
|
-
" Plan admits read tools plus note/question; Build admits the selected capability set.\n" +
|
|
1657
|
-
" /agent selects named profiles from ~/.xeno/agents.json and trusted .xeno/agents.json.\n" +
|
|
1658
|
-
" foundation.json configures MCP, web search, formatters, LSP and resource permissions.\n" +
|
|
1659
|
-
" --output-schema validates final JSON without coercion; invalid output exits 1.\n" +
|
|
1660
|
-
" --output-last-message writes the accepted final answer after the owner completes.\n" +
|
|
1661
|
-
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
1662
|
-
" Shell entry points (exec_command, environment, bash, target) ask before they run.\n" +
|
|
1663
|
-
" --allow-tools exec_command skips asks for that tool for this process.\n" +
|
|
1664
|
-
" --yolo (-y) skips all tool approvals for this process, including resumed sessions.\n" +
|
|
1665
|
-
" Tool exclusions and path limits still apply; YOLO does not change project trust.\n" +
|
|
1666
|
-
" --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
|
|
1667
|
-
" --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
|
|
1668
|
-
" --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +
|
|
1669
|
-
" --path-root <dir> lets file tools reach absolute paths under <dir> (e.g. / in a container);\n" +
|
|
1670
|
-
" relative paths, fingerprints and /back stay in the workspace, so edits outside it are not rewound.\n" +
|
|
1671
|
-
" --approve loads .xeno/settings.json this run; --no-approve skips it.\n" +
|
|
1672
|
-
" Project trust does not affect tool execution.\n" +
|
|
1673
|
-
" --mode json writes one JSON object per event to stdout; diagnostics stay on stderr.\n" +
|
|
1674
|
-
" --verbose expands the startup header (AGENTS.md, skills, prompt templates).",
|
|
1675
|
-
);
|
|
1676
|
-
if (flags?.size) console.log("\nExtension CLI flags:\n" + [...flags].map(([name, flag]) => ` --${name}${flag.type === "string" ? " <value>" : ""} ${flag.description ?? ""}`).join("\n"));
|
|
1677
|
-
}
|