@visual-z/xeno 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +8 -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,285 +0,0 @@
1
- /**
2
- * Which model to run, and which adapter to run it through.
3
- *
4
- * Selection is a pure function of the catalog, the settings file, the flags
5
- * and one predicate saying whether a model has a credential. Keeping it pure
6
- * matters because the interesting case is the one nobody configures on
7
- * purpose: no flag, no settings, two providers installed, one of them
8
- * authenticated. What happens then is a decision, and a decision should be
9
- * testable without a filesystem.
10
- *
11
- * The built-in list is a typed constant. The live list is that constant
12
- * overlaid with `~/.xeno/models.json`. Overlay is also pure: the file is
13
- * read elsewhere, this module only parses and merges. Fetch is accepted on
14
- * `loadCatalog` so a test can prove startup never calls it (D16).
15
- */
16
-
17
- import { assertNever } from "../core/exhaustive.ts";
18
- import { createHash } from "node:crypto";
19
- import { resolve } from "node:path";
20
- import { modelKey } from "../core/session-position.ts";
21
- import { mergeCatalogs } from "../core/catalog-merge.ts";
22
- import { parseModelConfiguration } from "./model-config.ts";
23
- import type { Settings } from "../core/settings.ts";
24
- import { CATALOG, CURATED_CATALOG, type ModelSpec } from "./model.ts";
25
- import { AnthropicProvider, type Provider, type ProviderConfig } from "./provider.ts";
26
- import { OpenAICompletionsProvider } from "./openai.ts";
27
- import { OpenAIResponsesProvider } from "./openai-responses.ts";
28
- import { PiProvider } from "./pi-provider.ts";
29
- import { copyProviderAccess, hasProviderAccess } from "./provider-access.ts";
30
- import type { ResolveEffects } from "./auth.ts";
31
-
32
- export type { Settings };
33
-
34
- export interface LoadedCatalog {
35
- models: ModelSpec[];
36
- warnings: string[];
37
- }
38
-
39
- /** Call signature only — `typeof fetch` also has `preconnect`, which a test spy does not. */
40
- export type CatalogFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
41
-
42
- /**
43
- * Built-in catalog plus a user file. `fetch` is part of the signature so the
44
- * startup-path test can inject one and assert it was never called — a default
45
- * `refresh: false` flag would not prove that.
46
- */
47
- export function loadCatalog(opts: { userText: string | null; cacheText?: string | null; readWarnings?: readonly string[]; fetch?: CatalogFetch }): LoadedCatalog {
48
- void opts.fetch;
49
- const warnings: string[] = [...(opts.readWarnings ?? [])];
50
- let models = [...CATALOG];
51
- if (opts.cacheText?.trim()) {
52
- try {
53
- const parsed = parseModelConfiguration(JSON.parse(opts.cacheText));
54
- const curated = new Map(CURATED_CATALOG.map(spec => [modelKey(spec), spec]));
55
- const cached = parsed.models.map(spec => {
56
- const known = curated.get(modelKey(spec));
57
- if (!known) return spec;
58
- // Refresh metadata without replacing already verified wire dialects for existing sessions.
59
- const next = { ...spec, api: known.api, reasoning: known.reasoning };
60
- delete next.driver; delete next.compat; delete next.nativeCompat; delete next.thinkingLevelMap;
61
- if (known.driver) next.driver = known.driver;
62
- if (known.nativeCompat) next.nativeCompat = known.nativeCompat;
63
- if (known.compat) next.compat = known.compat;
64
- if (known.thinkingLevelMap) next.thinkingLevelMap = known.thinkingLevelMap;
65
- if (known.baseUrl !== undefined) next.baseUrl = known.baseUrl;
66
- copyProviderAccess(spec, next);
67
- return next;
68
- });
69
- models = mergeCatalogs(models, cached);
70
- warnings.push(...parsed.errors.map(message => "catalog cache: " + message));
71
- } catch { warnings.push("catalog cache is not valid JSON; using bundled models"); }
72
- }
73
- if (opts.userText?.trim()) {
74
- try { const parsed = parseModelConfiguration(JSON.parse(opts.userText), models); models = parsed.models; warnings.push(...parsed.errors); }
75
- catch { warnings.push("user catalog is not valid JSON; using bundled/cache models"); }
76
- }
77
- return { models, warnings };
78
- }
79
-
80
- export function overlayCatalog(builtin: readonly ModelSpec[], user: readonly ModelSpec[]): ModelSpec[] { return mergeCatalogs(builtin, user); }
81
- export function parseUserCatalog(json: unknown): { models: ModelSpec[]; errors: string[] } { return parseModelConfiguration(json); }
82
-
83
- export function findModel(id: string, catalog: readonly ModelSpec[] = CATALOG): ModelSpec | undefined {
84
- return catalog.find((m) => m.id === id);
85
- }
86
-
87
- /**
88
- * Resolve a typed pattern to one catalog entry.
89
- *
90
- * Order is `provider/id`, then exact id, then substring, then subsequence
91
- * ("fuzzy"). Each tier is tried only if the one above it matched nothing,
92
- * so a unique exact id cannot be displaced by a looser match. Several
93
- * hits at the winning tier is an error that lists the candidates --
94
- * picking the first would make `--model sonnet` silently follow catalog
95
- * order after a refresh, and bill, cost, and behaviour would all change
96
- * with nothing in the log saying why.
97
- */
98
- export type ModelPatternMatch =
99
- | { ok: true; spec: ModelSpec }
100
- | { ok: false; reason: string; candidates: readonly ModelSpec[] };
101
-
102
- export function findModelByPattern(
103
- pattern: string,
104
- catalog: readonly ModelSpec[] = CATALOG,
105
- ): ModelPatternMatch {
106
- const needle = pattern.trim().toLowerCase();
107
- if (needle.length === 0) {
108
- return { ok: false, reason: "empty model pattern", candidates: [] };
109
- }
110
-
111
- const byRef = catalog.filter((m) => `${m.provider}/${m.id}`.toLowerCase() === needle);
112
- if (byRef.length > 0) return reportPattern(pattern, byRef);
113
-
114
- const exactId = catalog.filter((m) => m.id.toLowerCase() === needle);
115
- if (exactId.length > 0) return reportPattern(pattern, exactId);
116
-
117
- const substring = catalog.filter((m) => haystacks(m).some((h) => h.includes(needle)));
118
- if (substring.length > 0) return reportPattern(pattern, substring);
119
-
120
- const fuzzy = catalog.filter((m) => haystacks(m).some((h) => isSubsequence(needle, h)));
121
- return reportPattern(pattern, fuzzy);
122
- }
123
-
124
- function haystacks(m: ModelSpec): string[] {
125
- return [m.id.toLowerCase(), m.name.toLowerCase(), `${m.provider}/${m.id}`.toLowerCase()];
126
- }
127
-
128
- /**
129
- * Characters of `needle` appear in order in `haystack`, not necessarily
130
- * adjacent. A typed abbreviation ("dflash") and a dropped letter both
131
- * match; an edit-distance threshold would be a guessed constant, and
132
- * this function is the same rule a picker uses without one.
133
- */
134
- function isSubsequence(needle: string, haystack: string): boolean {
135
- let i = 0;
136
- for (const ch of haystack) {
137
- if (ch === needle[i]) i += 1;
138
- if (i === needle.length) return true;
139
- }
140
- return false;
141
- }
142
-
143
- function reportPattern(pattern: string, matches: readonly ModelSpec[]): ModelPatternMatch {
144
- if (matches.length === 1) return { ok: true, spec: matches[0]! };
145
- if (matches.length === 0) {
146
- return { ok: false, reason: `unknown model: ${pattern}. Try --list-models`, candidates: [] };
147
- }
148
- const ids = matches.map(modelKey);
149
- return {
150
- ok: false,
151
- reason:
152
- `ambiguous model pattern ${JSON.stringify(pattern)}: matches ${ids.join(", ")}. ` +
153
- "Be more specific rather than guessing",
154
- candidates: matches,
155
- };
156
- }
157
-
158
- export function modelsOfProvider(
159
- provider: string,
160
- catalog: readonly ModelSpec[] = CATALOG,
161
- ): readonly ModelSpec[] {
162
- return catalog.filter((m) => m.provider === provider);
163
- }
164
-
165
- /** First-seen provider order, which is catalog order, not alphabetical. */
166
- export function groupedModels(
167
- catalog: readonly ModelSpec[] = CATALOG,
168
- ): { provider: string; models: readonly ModelSpec[] }[] {
169
- const order: string[] = [];
170
- const map = new Map<string, ModelSpec[]>();
171
- for (const spec of catalog) {
172
- let list = map.get(spec.provider);
173
- if (list === undefined) {
174
- list = [];
175
- map.set(spec.provider, list);
176
- order.push(spec.provider);
177
- }
178
- list.push(spec);
179
- }
180
- return order.map((provider) => ({ provider, models: map.get(provider)! }));
181
- }
182
-
183
- export type Selection =
184
- | { ok: true; spec: ModelSpec }
185
- | { ok: false; reason: string };
186
-
187
- /**
188
- * `--model` wins, then `--provider`'s first model, then the settings file,
189
- * then a usable recent selection from older settings, then the first model
190
- * in the catalog that has a credential.
191
- *
192
- * The last step is what makes a fresh install work: someone who exported one
193
- * API key should get a working session without also having to learn the name
194
- * of a model. It is deliberately last, so it can never override something the
195
- * person actually asked for -- an explicit `--model` that has no credential
196
- * must fail loudly rather than quietly running a different model, which would
197
- * bill the wrong account and invalidate any comparison between runs.
198
- */
199
- export function selectModel(
200
- settings: Settings,
201
- flags: { model?: string | undefined; provider?: string | undefined },
202
- hasCredential: (spec: ModelSpec) => boolean,
203
- catalog: readonly ModelSpec[] = CATALOG,
204
- ): Selection {
205
- if (flags.model !== undefined && flags.model.length > 0) {
206
- const candidates = flags.provider ? modelsOfProvider(flags.provider, catalog) : catalog;
207
- const match = findModelByPattern(flags.model, candidates);
208
- return match.ok ? { ok: true, spec: match.spec } : { ok: false, reason: `${match.reason}. Try --list-models` };
209
- }
210
-
211
- if (flags.provider !== undefined && flags.provider.length > 0) {
212
- const models = modelsOfProvider(flags.provider, catalog);
213
- if (models.length === 0) {
214
- return { ok: false, reason: `unknown provider: ${flags.provider}. Try --list-models` };
215
- }
216
- return { ok: true, spec: models.find(hasCredential) ?? models[0]! };
217
- }
218
-
219
- if (settings.defaultModel !== undefined) {
220
- const candidates = settings.defaultProvider ? catalog.filter(model => model.provider === settings.defaultProvider) : catalog;
221
- const found = findModelByPattern(settings.defaultModel, candidates);
222
- const spec = found.ok ? found.spec : undefined;
223
- if (spec !== undefined) return { ok: true, spec };
224
- // Not fatal: a settings file naming a model this build dropped should
225
- // degrade to the normal search, not refuse to start.
226
- }
227
-
228
- if (settings.defaultProvider !== undefined) {
229
- const preferred = modelsOfProvider(settings.defaultProvider, catalog).find(hasCredential);
230
- if (preferred !== undefined) return { ok: true, spec: preferred };
231
- }
232
-
233
- // Old pickers only persisted recent identities. Reuse one exactly, without fuzzy guesses (Q.25).
234
- for (const key of settings.recentModels ?? []) {
235
- const recent = catalog.find(model => modelKey(model) === key);
236
- if (recent !== undefined && hasCredential(recent)) return { ok: true, spec: recent };
237
- }
238
-
239
- const authenticated = catalog.find(hasCredential);
240
- if (authenticated !== undefined) return { ok: true, spec: authenticated };
241
-
242
- return {
243
- ok: false,
244
- reason:
245
- "no model has a credential. Set an API key for one of: " +
246
- [...new Set(catalog.map((m) => m.provider))].join(", ") +
247
- " -- export its environment variable, or run /provider",
248
- };
249
- }
250
-
251
- /** One adapter per wire format. A new API means a new case here, never a flag. */
252
- export function createProvider(
253
- config: ProviderConfig,
254
- apiKey: string,
255
- onRetry?: (attempt: number, delayMs: number, error: unknown) => void,
256
- opts?: { oauth?: boolean; fetch?: typeof fetch; effects?: ResolveEffects; sessionId?: string },
257
- ): Provider {
258
- const oauth = opts?.oauth === true;
259
- if (config.spec.driver === "pi" || hasProviderAccess(config.spec)) return new PiProvider(config, apiKey, {
260
- oauth,
261
- ...(opts?.sessionId ? { sessionId: opts.sessionId } : {}),
262
- ...(onRetry ? { onRetry } : {}), ...(opts?.fetch ? { fetch: opts.fetch } : {}), ...(opts?.effects ? { effects: opts.effects } : {}),
263
- });
264
- switch (config.spec.api) {
265
- case "anthropic-messages":
266
- return new AnthropicProvider(config, apiKey.length > 0 ? apiKey : undefined, {
267
- ...(onRetry ? { onRetry } : {}),
268
- ...(oauth ? { oauth: true } : {}),
269
- });
270
- case "openai-completions":
271
- return new OpenAICompletionsProvider(config, apiKey, { ...(onRetry ? { onRetry } : {}) });
272
- case "openai-responses":
273
- return new OpenAIResponsesProvider(config, apiKey, { ...(onRetry ? { onRetry } : {}) });
274
- case "azure-openai-responses": case "openai-codex-responses": case "google-generative-ai": case "google-vertex":
275
- case "bedrock-converse-stream": case "mistral-conversations": case "pi-messages":
276
- return new PiProvider(config, apiKey, { ...(onRetry ? { onRetry } : {}), ...(opts?.fetch ? { fetch: opts.fetch } : {}), ...(opts?.effects ? { effects: opts.effects } : {}), ...(opts?.sessionId ? { sessionId: opts.sessionId } : {}) });
277
- default:
278
- return assertNever(config.spec.api, "createProvider");
279
- }
280
- }
281
-
282
- /** D6/D8: routing affinity survives reload and cold resume without server-owned history or path disclosure. */
283
- export function providerSessionId(logDir: string): string {
284
- return createHash("sha256").update(resolve(logDir)).digest("hex");
285
- }
@@ -1,125 +0,0 @@
1
- /**
2
- * Pi's files → image → text precedence (MIT, 02eed88) over the platform's own
3
- * clipboard commands. The optional native helper went with the vendored TUI
4
- * (D20); a caller may still inject one.
5
- */
6
- import { fileURLToPath } from "node:url";
7
-
8
- export interface NativeClipboard {
9
- setText?(text: string): Promise<void>;
10
- getText?(): Promise<string | null | undefined>;
11
- getImage?(): Promise<Uint8Array | null | undefined>;
12
- getFilePaths?(): Promise<string[] | null | undefined>;
13
- }
14
- function getNativeClipboard(): NativeClipboard | null { return null; }
15
- import { runClipboardCommand } from "../vendor/pi-rich/clipboard-command.ts";
16
-
17
- export type ClipboardContent = { files: string[] } | { image: Uint8Array } | { text: string } | null;
18
- type Run = (command: string, args: readonly string[], options?: { input?: string; timeoutMs?: number }) => Promise<Buffer | undefined>;
19
- export interface ClipboardOptions {
20
- platform?: NodeJS.Platform;
21
- env?: NodeJS.ProcessEnv;
22
- native?: NativeClipboard | null;
23
- run?: Run;
24
- }
25
-
26
- /** Text is transported as data. Clipboard contents must never become PowerShell or shell source. */
27
- export async function writeClipboard(text: string, options: ClipboardOptions = {}): Promise<boolean> {
28
- const platform = options.platform ?? process.platform, env = options.env ?? process.env;
29
- const native = options.native === undefined ? getNativeClipboard() : options.native, run = options.run ?? runClipboardCommand;
30
- if (native?.setText) {
31
- try { await native.setText(text); return true; } catch { /* Continue to the same platform's command transport. */ }
32
- }
33
- const windows = platform === "win32" || !!(env["WSL_DISTRO_NAME"] || env["WSL_INTEROP"]);
34
- const commands: readonly (readonly [string, readonly string[]])[] = windows
35
- ? [["powershell.exe", ["-NoProfile", "-STA", "-Command", "Add-Type -AssemblyName System.Windows.Forms; [Console]::InputEncoding = [System.Text.Encoding]::UTF8; $t = [Console]::In.ReadToEnd(); if ($t.Length) { [System.Windows.Forms.Clipboard]::SetText($t) } else { [System.Windows.Forms.Clipboard]::Clear() }"]]]
36
- : platform === "darwin" ? [["pbcopy", []]]
37
- : env["TERMUX_VERSION"] ? [["termux-clipboard-set", []]]
38
- : platform === "linux" ? [
39
- ...(env["WAYLAND_DISPLAY"] || env["XDG_SESSION_TYPE"] === "wayland" ? [["wl-copy", ["--type", "text/plain;charset=utf-8"]] as const] : []),
40
- ...(env["DISPLAY"] ? [["xclip", ["-selection", "clipboard"]] as const, ["xsel", ["--clipboard", "--input"]] as const] : []),
41
- ] : [];
42
- for (const [command, args] of commands) {
43
- try { if (await run(command, args, { input: text, timeoutMs: 5000 }) !== undefined) return true; } catch { /* Try a same-platform backend. */ }
44
- }
45
- return false;
46
- }
47
-
48
- // Fixed programs only. Clipboard content is data, never interpolated into a shell or script.
49
- const MAC_READ = `ObjC.import('AppKit');
50
- const p = $.NSPasteboard.generalPasteboard;
51
- const urls = p.readObjectsForClassesOptions([$.NSURL], $({NSPasteboardURLReadingFileURLsOnlyKey: true}));
52
- const files = [];
53
- if (urls) for (let i = 0; i < urls.count; i++) files.push(ObjC.unwrap(urls.objectAtIndex(i).path));
54
- if (files.length) JSON.stringify({files});
55
- else {
56
- const png = p.dataForType('public.png');
57
- const tiff = png ? null : p.dataForType('public.tiff');
58
- const data = png || (tiff ? $.NSBitmapImageRep.imageRepWithData(tiff).representationUsingTypeProperties($.NSPNGFileType, $({})) : null);
59
- if (data) JSON.stringify({image: ObjC.unwrap(data.base64EncodedStringWithOptions(0))});
60
- else JSON.stringify({text: ObjC.unwrap(p.stringForType($.NSPasteboardTypeString)) || ''});
61
- }`;
62
- const WINDOWS_READ = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing;
63
- $c = [System.Windows.Forms.Clipboard];
64
- if ($c::ContainsFileDropList()) { @{files = @($c::GetFileDropList())} | ConvertTo-Json -Compress }
65
- elseif ($c::ContainsImage()) { $i = $c::GetImage(); $s = New-Object System.IO.MemoryStream; try { $i.Save($s, [System.Drawing.Imaging.ImageFormat]::Png); @{image = [Convert]::ToBase64String($s.ToArray())} | ConvertTo-Json -Compress } finally { $i.Dispose(); $s.Dispose() } }
66
- else { @{text = $c::GetText()} | ConvertTo-Json -Compress }`;
67
-
68
- function decode(bytes: Buffer | undefined): ClipboardContent | undefined {
69
- if (bytes === undefined) return undefined;
70
- const value = JSON.parse(bytes.toString("utf8")) as { files?: unknown; image?: unknown; text?: unknown };
71
- if (Array.isArray(value.files) && value.files.every(p => typeof p === "string") && value.files.length) return { files: value.files };
72
- if (typeof value.image === "string" && value.image) return { image: Buffer.from(value.image, "base64") };
73
- return typeof value.text === "string" && value.text ? { text: value.text } : null;
74
- }
75
-
76
- export async function readClipboard(options: ClipboardOptions = {}): Promise<ClipboardContent> {
77
- const platform = options.platform ?? process.platform, env = options.env ?? process.env;
78
- const run = options.run ?? runClipboardCommand;
79
- const native = options.native === undefined ? getNativeClipboard() : options.native;
80
- const files = await native?.getFilePaths?.();
81
- if (files?.length) return { files };
82
- if (platform === "darwin" && !native) {
83
- const result = decode(await run("osascript", ["-l", "JavaScript", "-e", MAC_READ]));
84
- if (result !== undefined) return result;
85
- }
86
- const wsl = !!(env["WSL_DISTRO_NAME"] || env["WSL_INTEROP"]);
87
- if (platform === "win32" || wsl) {
88
- const result = decode(await run("powershell.exe", ["-NoProfile", "-STA", "-Command", WINDOWS_READ], { timeoutMs: 5000 }));
89
- if (result !== undefined) return result;
90
- }
91
- if (platform === "linux" && !env["TERMUX_VERSION"]) {
92
- const wayland = !!env["WAYLAND_DISPLAY"] || env["XDG_SESSION_TYPE"] === "wayland";
93
- const backends = [...(wayland ? ["wayland"] : []), ...(env["DISPLAY"] ? ["x11"] : [])];
94
- for (const backend of backends) {
95
- const read = (type: string) => backend === "wayland" ? run("wl-paste", ["--type", type, "--no-newline"]) : run("xclip", ["-selection", "clipboard", "-t", type, "-o"]);
96
- const available = backend === "wayland" ? await run("wl-paste", ["--list-types"], { timeoutMs: 1000 }) : await read("TARGETS");
97
- if (available === undefined) continue;
98
- const types = available.toString("utf8").trim().split(/\r?\n/);
99
- if (types.includes("text/uri-list")) {
100
- const bytes = await read("text/uri-list");
101
- if (bytes === undefined) throw new Error("Clipboard file transfer failed");
102
- const paths = bytes.toString("utf8").split(/\r?\n/).filter(p => p.startsWith("file://")).map(p => fileURLToPath(p));
103
- if (paths.length) return { files: paths };
104
- }
105
- const type = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/bmp"].find(t => types.includes(t));
106
- if (type) {
107
- const bytes = await read(type);
108
- if (bytes === undefined) throw new Error("Clipboard image transfer failed");
109
- return bytes.length ? { image: bytes } : null;
110
- }
111
- const textType = ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain", "text"].find(t => types.includes(t));
112
- if (!textType) return null; // An empty Wayland selection must not reveal stale X11 contents.
113
- const bytes = await read(textType);
114
- if (bytes === undefined) throw new Error("Clipboard text transfer failed");
115
- return bytes.length ? { text: bytes.toString("utf8") } : null;
116
- }
117
- }
118
- const image = await native?.getImage?.();
119
- if (image?.length) return { image };
120
- const text = await native?.getText?.();
121
- if (text !== undefined) return text ? { text } : null;
122
- const fallback = env["TERMUX_VERSION"] ? await run("termux-clipboard-get", []) : platform === "darwin" ? await run("pbpaste", []) : undefined;
123
- if (fallback === undefined) throw new Error("Clipboard unavailable for this terminal session");
124
- return fallback.length ? { text: fallback.toString("utf8") } : null;
125
- }
@@ -1,64 +0,0 @@
1
- /** R8: one owner observes an admitted command once, including after a managed call yields. */
2
- import { hostname } from "node:os";
3
- import { resolve, join } from "node:path";
4
- import { artifactChange, commandArtifactState, parseCommandArtifactPlan, COMMAND_ARTIFACT_SCOPE, type CommandArtifactReceipt } from "../core/command-artifacts.ts";
5
- import { observeArtifactInputs, observeArtifactFiles, artifactExclusions, archiveCommandArtifacts } from "../store/command-artifacts.ts";
6
- import type { ToolContext } from "./tools.ts";
7
-
8
- export type ArtifactCommandEnd = { exit: number; stopped?: "cancelled" | "timeout"; capture?: "complete" | "partial"; cleanup?: unknown };
9
- export type CommandArtifactObserver = {
10
- pending: CommandArtifactReceipt;
11
- finish(end: ArtifactCommandEnd): Promise<CommandArtifactReceipt>;
12
- };
13
-
14
- /** Syntactic validation precedes admission; no preflight disk probing is smuggled through a denied tool. */
15
- export function prepareArtifactArguments(input: unknown): unknown {
16
- if (input === null || typeof input !== "object" || Array.isArray(input) || !("artifacts" in input)) return input;
17
- return { ...input, artifacts: parseCommandArtifactPlan(input.artifacts) };
18
- }
19
-
20
- export async function beginCommandArtifacts(ctx: Pick<ToolContext, "root" | "hostId" | "ownedStatePaths" | "outputStore" | "signal">,
21
- command: string, cwd: string, requested: unknown): Promise<CommandArtifactObserver | undefined> {
22
- if (requested === undefined) return undefined;
23
- const plan = parseCommandArtifactPlan(requested), exclusions = artifactExclusions(ctx.root, plan,
24
- [...(ctx.ownedStatePaths ?? []), ctx.outputStore ?? join(ctx.root, ".xeno", "observations")]);
25
- const beforeInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
26
- const before = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
27
- const pending: CommandArtifactReceipt = {
28
- kind: "command_artifacts", version: 1, state: "pending", plan,
29
- source: { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd), runtime: `bun/${Bun.version}` },
30
- command, exit: null, started_at: new Date().toISOString(), observed_at: null,
31
- inputs: { scope: COMMAND_ARTIFACT_SCOPE, exclusions, before: beforeInputs.sha256, after: null },
32
- outputs: [], reasons: beforeInputs.reason ? [beforeInputs.reason] : [],
33
- };
34
- let completed: Promise<CommandArtifactReceipt> | undefined;
35
- return { pending, finish(end) {
36
- completed ??= (async () => {
37
- // Observation failures retain the actual exit. Never retry a post-exit read against later files.
38
- const reasons = [...pending.reasons];
39
- if (end.exit !== 0) reasons.push("command exited unsuccessfully");
40
- if (end.stopped) reasons.push(`command ${end.stopped}`);
41
- if (end.capture === "partial") reasons.push("command output capture is partial");
42
- if (end.cleanup) reasons.push("command cleanup is unconfirmed");
43
- const after = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
44
- const afterInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
45
- if (afterInputs.reason) reasons.push(afterInputs.reason);
46
- const receipt: CommandArtifactReceipt = { ...pending, exit: end.exit, observed_at: new Date().toISOString(),
47
- inputs: { ...pending.inputs, after: afterInputs.sha256 },
48
- outputs: plan.paths.map((path, i) => ({ path, before: before[i]!, after: after[i]!, change: artifactChange(before[i]!, after[i]!) })), reasons };
49
- receipt.state = commandArtifactState(receipt);
50
- try { receipt.manifest = await archiveCommandArtifacts(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), receipt); }
51
- catch { receipt.reasons.push("artifact observation archive unavailable"); receipt.state = commandArtifactState(receipt); }
52
- return receipt;
53
- })();
54
- return completed;
55
- } };
56
- }
57
-
58
- export function commandArtifactSummary(receipt: CommandArtifactReceipt | undefined): string {
59
- if (!receipt) return "";
60
- const changes = ["created", "modified", "unchanged", "unknown"].map(change => `${change}=${receipt.outputs.filter(output => output.change === change).length}`).join(", ");
61
- return `\n[artifacts ${receipt.state}; ${changes}; reasons=${receipt.reasons.length}; ` +
62
- (receipt.manifest ? `history artifact=${receipt.manifest.sha256}; ` : "") +
63
- "command association only, not build causality or runtime acceptance]";
64
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Whether a provider may keep reusing pooled connections.
3
- *
4
- * A socket that a proxy, NAT or server dropped while it sat in the pool fails
5
- * the next request the same way -- Horizyn 2026-10-12 lost the first request
6
- * after "continue" to it. Claude Code answers by turning keep-alive off for
7
- * the rest of the process once a connection dies; this does the same per
8
- * provider instance, so tests sharing a process cannot leak state into each
9
- * other and a fresh session starts from the cheap path again
10
- * (feat-fresh-connection-after-drop).
11
- *
12
- * Bun honours `keepalive: false` by opening a new socket per request
13
- * (measured on 1.3.10: three default fetches share one socket, three with
14
- * keepalive:false open three). The cost is a TLS handshake per request,
15
- * negligible next to a model call. Google, Vertex and Bedrock go through
16
- * pi-ai's native HTTP with no fetch hook, so this cannot reach them.
17
- */
18
- import { connectionFailure } from "./retry.ts";
19
-
20
- export class ConnectionReuse {
21
- private fresh = false;
22
-
23
- /**
24
- * Called with every failed attempt, retried or final. Only a connection
25
- * that existed and died counts: one that never opened left nothing stale
26
- * in the pool.
27
- */
28
- note(error: unknown): void {
29
- if (connectionFailure(error) === "stale") this.fresh = true;
30
- }
31
-
32
- init(init: RequestInit | undefined): RequestInit | undefined {
33
- return this.fresh ? { ...init, keepalive: false } : init;
34
- }
35
-
36
- /** A fetch that opens fresh connections once one has died. */
37
- wrap(fetchImpl: typeof fetch): typeof fetch {
38
- return Object.assign((input: RequestInfo | URL, init?: RequestInit) => fetchImpl(input, this.init(init)), { preconnect: fetchImpl.preconnect });
39
- }
40
- }