@visual-z/xeno 0.8.4 → 0.9.1

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.
Files changed (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -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
- }