super-dsh 0.1.0 → 0.1.2

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 (178) hide show
  1. package/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,299 @@
1
+ /**
2
+ * OMP model catalog for the Dash model selector.
3
+ *
4
+ * The browser model selector is a pure RPC round-trip through the apiproxy:
5
+ * `session.models` reads `ctx.llm.listProviders/listModels/resolveModelInfo`,
6
+ * so the OMP provider registers an `LlmAdapter` whose catalog is OMP's real
7
+ * AVAILABLE model set. This module owns the source of that catalog.
8
+ *
9
+ * Source: `omp models --json` (first-party CLI, `omp-cli.ts`) — OMP's own
10
+ * credential-resolved availability verdict. The documented selectability
11
+ * rule (provider not in `disabledProviders` and keyless or with a
12
+ * resolvable credential) runs through a 7-level credential precedence that
13
+ * includes OAuth/login keys stored in agent.db — a state NO config file
14
+ * reveals, which is why the catalog must come from OMP itself.
15
+ *
16
+ * The authoritative RPC `get_available_models` would serve the same set but
17
+ * exceeds the 1 MiB transport frame limit for this install; the CLI returns
18
+ * the compact form (~50 models / 5 providers, 15 KB, ~1.7s). The on-disk
19
+ * `models.db` ∪ `models.yml` pair remains ONLY as the subprocess-failure
20
+ * fallback (registry-cache noise included) — never the primary.
21
+ */
22
+ import { readFileSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { DatabaseSync } from "node:sqlite";
25
+ import { ompAvailableModels, ompAvailableModelsSync, ompModelRolesSync } from "./omp-cli.js";
26
+ import { OMP_NATIVE_HOME } from "./knobs.js";
27
+ import { readOmpModelRolesFromConfig } from "./omp-store.js";
28
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
29
+ const OMP_AGENT_DIR = join(OMP_NATIVE_HOME, "agent");
30
+ /**
31
+ * Providers named by config.yml `modelRoles` lead the selector: they are the
32
+ * roles the operator actually wired (the TUI default-model semantics).
33
+ */
34
+ let cached = null;
35
+ /**
36
+ * Read the AVAILABLE OMP model catalog, memoized. First call prefers the CLI
37
+ * (`omp models --json`, synchronous — the boot route list must be known
38
+ * before `registerAdapter` returns); a CLI failure falls back to the on-disk
39
+ * registry cache with a stderr note. Later refreshes go through
40
+ * `refreshOmpModelsCli` (async, per reconcile tick).
41
+ */
42
+ export function loadOmpModels() {
43
+ if (cached === null) {
44
+ const cli = ompAvailableModelsSync();
45
+ if (cli !== undefined) {
46
+ cached = cli.map((model) => normalizeModel(model)).filter((model) => model !== null);
47
+ }
48
+ else {
49
+ process.stderr.write("[omp-provider] omp models --json unavailable; falling back to the on-disk model cache\n");
50
+ cached = mergeModels(loadModelsDb(), loadModelsYml());
51
+ }
52
+ }
53
+ return cached;
54
+ }
55
+ /**
56
+ * Re-run the CLI catalog and swap the memo on success.
57
+ * @returns whether the refresh produced a fresh catalog.
58
+ */
59
+ export async function refreshOmpModelsCli() {
60
+ const cli = await ompAvailableModels();
61
+ if (cli === undefined)
62
+ return false;
63
+ cached = cli.map((model) => normalizeModel(model)).filter((model) => model !== null);
64
+ return true;
65
+ }
66
+ /** The distinct provider ids across the catalog, modelRoles roles first. */
67
+ export function ompProviderIds(models = loadOmpModels()) {
68
+ const seen = new Set();
69
+ for (const model of models)
70
+ seen.add(model.provider);
71
+ const ordered = [];
72
+ // Providers the operator wired into modelRoles lead the selector (TUI
73
+ // semantics: the roles actually in use); everything else follows
74
+ // alphabetically. Roles come from config.yml ON DISK — never a sidecar
75
+ // warm (sidecar-decoupling); the memo is only a fallback for a sidecar
76
+ // that is already live.
77
+ const roles = readOmpModelRolesFromConfig() ?? ompModelRolesSync();
78
+ for (const selector of Object.values(roles ?? {})) {
79
+ const slash = selector.indexOf("/");
80
+ const provider = slash > 0 ? selector.slice(0, slash) : "";
81
+ if (provider !== "" && seen.has(provider) && !ordered.includes(provider))
82
+ ordered.push(provider);
83
+ }
84
+ for (const provider of [...seen].sort((a, b) => a.localeCompare(b))) {
85
+ if (!ordered.includes(provider))
86
+ ordered.push(provider);
87
+ }
88
+ return ordered;
89
+ }
90
+ /** Human-readable provider name for `LlmProviderInfo.name`. */
91
+ export function providerDisplayName(provider) {
92
+ const titles = { zai: "Z.AI", xai: "xAI", openrouter: "OpenRouter" };
93
+ return titles[provider] ?? (provider.charAt(0).toUpperCase() + provider.slice(1));
94
+ }
95
+ /** Merge the registry cache under the user overlay (later models win per provider+id). */
96
+ function mergeModels(cache, overlay) {
97
+ if (overlay.length === 0)
98
+ return cache;
99
+ const byKey = new Map();
100
+ for (const model of cache)
101
+ byKey.set(model.provider + "/" + model.id, model);
102
+ for (const model of overlay)
103
+ byKey.set(model.provider + "/" + model.id, model);
104
+ return [...byKey.values()];
105
+ }
106
+ /**
107
+ * Read `~/.omp/agent/models.db` (`model_cache.models` JSON column). This is
108
+ * the on-disk form of the registry `get_available_models` serves. Read-only
109
+ * and tolerant of a missing/empty file (OMP may not have refreshed it yet).
110
+ */
111
+ function loadModelsDb() {
112
+ const out = [];
113
+ try {
114
+ const db = new DatabaseSync(join(OMP_AGENT_DIR, "models.db"), { readOnly: true });
115
+ try {
116
+ const rows = db.prepare("SELECT models FROM model_cache").all();
117
+ for (const row of rows) {
118
+ try {
119
+ const models = JSON.parse(row.models);
120
+ if (!Array.isArray(models))
121
+ continue;
122
+ for (const model of models) {
123
+ const parsed = normalizeModel(model);
124
+ if (parsed !== null)
125
+ out.push(parsed);
126
+ }
127
+ }
128
+ catch {
129
+ // one malformed provider row must not sink the whole catalog
130
+ }
131
+ }
132
+ }
133
+ finally {
134
+ db.close();
135
+ }
136
+ }
137
+ catch {
138
+ // absent db / schema drift → fall through to the overlay alone
139
+ }
140
+ return out;
141
+ }
142
+ /**
143
+ * Parse `~/.omp/agent/models.yml` — a `providers: <id>: { models: [ ... ] }`
144
+ * overlay OMP writes. A focused indentation scan is enough for this fixed
145
+ * shape and avoids pulling `js-yaml` into the provider's dependency surface.
146
+ * Any parse failure degrades to an empty overlay.
147
+ */
148
+ function loadModelsYml() {
149
+ const out = [];
150
+ let text;
151
+ try {
152
+ text = readFileSync(join(OMP_AGENT_DIR, "models.yml"), "utf8");
153
+ }
154
+ catch {
155
+ return out;
156
+ }
157
+ let provider = "";
158
+ let inModels = false;
159
+ let model = null;
160
+ const push = () => {
161
+ if (model !== null) {
162
+ const parsed = normalizeModel({ ...model, provider });
163
+ if (parsed !== null)
164
+ out.push(parsed);
165
+ model = null;
166
+ }
167
+ };
168
+ for (const raw of text.split("\n")) {
169
+ const line = raw.replace(/\r$/, "");
170
+ if (line.trim() === "" || line.trimStart().startsWith("#"))
171
+ continue;
172
+ const indent = line.length - line.trimStart().length;
173
+ const trimmed = line.trim();
174
+ if (indent === 0) {
175
+ if (trimmed === "providers:") {
176
+ provider = "";
177
+ inModels = false;
178
+ model = null;
179
+ }
180
+ else {
181
+ provider = "";
182
+ inModels = false;
183
+ model = null;
184
+ }
185
+ continue;
186
+ }
187
+ if (indent === 2 && trimmed.endsWith(":")) {
188
+ push();
189
+ provider = trimmed.slice(0, -1).trim();
190
+ inModels = false;
191
+ continue;
192
+ }
193
+ if (indent === 4 && trimmed === "models:") {
194
+ inModels = true;
195
+ continue;
196
+ }
197
+ if (inModels && indent >= 6 && trimmed.startsWith("- ")) {
198
+ push();
199
+ model = {};
200
+ const entry = trimmed.slice(2).trim();
201
+ const colon = entry.indexOf(":");
202
+ if (colon > 0)
203
+ model[entry.slice(0, colon).trim()] = entry.slice(colon + 1).trim();
204
+ continue;
205
+ }
206
+ if (inModels && model !== null && indent >= 8) {
207
+ const colon = trimmed.indexOf(":");
208
+ if (colon > 0) {
209
+ const key = trimmed.slice(0, colon).trim();
210
+ model[key] = trimmed.slice(colon + 1).trim();
211
+ }
212
+ continue;
213
+ }
214
+ // Any other shape is outside the models list we care about.
215
+ }
216
+ push();
217
+ return out;
218
+ }
219
+ /** Coerce one raw model record into an {@link OmpModel}, or null when unusable. */
220
+ function normalizeModel(raw) {
221
+ if (raw === null || typeof raw !== "object")
222
+ return null;
223
+ const record = raw;
224
+ const id = typeof record.id === "string" ? record.id : "";
225
+ const provider = typeof record.provider === "string" ? record.provider : "";
226
+ const name = typeof record.name === "string" && record.name !== "" ? record.name : id;
227
+ if (id === "" || provider === "" || name === "")
228
+ return null;
229
+ const thinking = normalizeThinking(record.thinking);
230
+ return {
231
+ id,
232
+ provider,
233
+ name,
234
+ ...(typeof record.api === "string" ? { api: record.api } : {}),
235
+ ...(typeof record.baseUrl === "string" ? { baseUrl: record.baseUrl } : {}),
236
+ ...(typeof record.reasoning === "boolean" ? { reasoning: record.reasoning } : {}),
237
+ ...(Array.isArray(record.input) ? { input: record.input.filter((m) => typeof m === "string") } : {}),
238
+ ...(typeof record.contextWindow === "number" ? { contextWindow: record.contextWindow } : {}),
239
+ ...(typeof record.maxTokens === "number" ? { maxTokens: record.maxTokens } : {}),
240
+ ...(thinking === undefined ? {} : { thinking }),
241
+ };
242
+ }
243
+ /** Map OMP's declared input modalities to the harness vocabulary. */
244
+ function inputModalities(model) {
245
+ if (model.input === undefined)
246
+ return undefined;
247
+ const modalities = model.input.filter((m) => m === "text" || m === "image");
248
+ return modalities.length === 0 ? undefined : modalities;
249
+ }
250
+ /** Extract OMP's `thinking` sub-object, checking each field at the boundary. */
251
+ function normalizeThinking(raw) {
252
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
253
+ return undefined;
254
+ const t = raw;
255
+ return {
256
+ ...(typeof t.mode === "string" ? { mode: t.mode } : {}),
257
+ ...(Array.isArray(t.efforts) ? { efforts: t.efforts.filter((e) => typeof e === "string") } : {}),
258
+ ...(typeof t.defaultLevel === "string" ? { defaultLevel: t.defaultLevel } : {}),
259
+ };
260
+ }
261
+ /** Map OMP `thinking.efforts` into the adapter reasoning-effort vocabulary. */
262
+ function reasoningInfo(model) {
263
+ const efforts = model.thinking?.efforts ?? [];
264
+ if (efforts.length === 0)
265
+ return undefined;
266
+ const ids = new Set();
267
+ const mapped = efforts
268
+ .filter((effort) => effort !== "" && !ids.has(effort) && (ids.add(effort), true))
269
+ .map((effort) => ({ id: ReasoningEffortId(effort), name: effort }));
270
+ if (mapped.length === 0)
271
+ return undefined;
272
+ const defaultEffort = model.thinking?.defaultLevel;
273
+ return {
274
+ efforts: mapped,
275
+ ...(defaultEffort !== undefined && ids.has(defaultEffort) ? { defaultEffort: ReasoningEffortId(defaultEffort) } : {}),
276
+ };
277
+ }
278
+ /** One catalog row: {@link LlmModelInfo} for `listModels`. */
279
+ export function toModelInfo(model) {
280
+ return {
281
+ provider: model.provider,
282
+ id: model.id,
283
+ name: model.name,
284
+ ...(inputModalities(model) === undefined ? {} : { inputModalities: inputModalities(model) }),
285
+ };
286
+ }
287
+ /** One exact-model resolution: {@link LlmResolvedModelInfo} for `resolveModel`. */
288
+ export function toResolvedModelInfo(model) {
289
+ const info = toModelInfo(model);
290
+ const contextWindow = typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined;
291
+ const defaultMaxTokens = typeof model.maxTokens === "number" && model.maxTokens > 0 ? model.maxTokens : undefined;
292
+ const reasoning = reasoningInfo(model);
293
+ return {
294
+ ...info,
295
+ ...(contextWindow === undefined ? {} : { context: { contextWindow } }),
296
+ ...(defaultMaxTokens === undefined ? {} : { defaultMaxTokens }),
297
+ ...(reasoning === undefined ? {} : { reasoning }),
298
+ };
299
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * OMP model catalog + modelRoles data plane, served by the shared bun sidecar
3
+ * (which embeds the OMP core via @oh-my-pi/pi-coding-agent) instead of
4
+ * spawning `omp` CLI subprocesses.
5
+ *
6
+ * Two data planes are bridged:
7
+ * - `models.list` → the credential-resolved AVAILABLE model
8
+ * catalog (the same set `omp models --json` returned: ~50 models / 5
9
+ * providers), keyed the way `models.ts` normalizes them (id/provider/name/
10
+ * api/baseUrl/reasoning/input/contextWindow/maxTokens/thinking).
11
+ * - `settings.modelRoles.get/set` → the role→"provider/model[:effort]" map,
12
+ * including the persisted `default` selector (TUI semantics: the default
13
+ * model IS config state, surviving sessions). `set` is a whole-object
14
+ * roundtrip write, matching `omp config set modelRoles` (dotted keys are
15
+ * rejected; the entire modelRoles object is the unit).
16
+ *
17
+ * The sidecar is a SHARED SINGLETON (src/sdk-client.ts acquire/release) — this
18
+ * module goes through the same instance via `callShared`, never spawning a
19
+ * second sidecar process.
20
+ *
21
+ * Sync variants (`*Sync`) cannot block on sidecar IPC, so they return the last
22
+ * cache and warm it asynchronously: the first boot-time call returns
23
+ * `undefined` (the caller's on-disk fallback engages) and a later background
24
+ * fetch repopulates the cache. The async variants always fetch fresh.
25
+ *
26
+ * Fail-soft: any sidecar failure (not started / crashed) yields `undefined`
27
+ * (reads) or `false` (write), so the provider never fails to boot on a down
28
+ * sidecar.
29
+ *
30
+ * @module omp-web/omp-cli
31
+ */
32
+ import { callShared, callSharedIfLive } from "./sdk-client.js";
33
+ // ---------------------------------------------------------------------------
34
+ // Sidecar plumbing (fail-soft: any failure → undefined/false)
35
+ // ---------------------------------------------------------------------------
36
+ /** `models.list` over the shared sidecar → validated OmpCliModel[], undefined on failure. */
37
+ async function sidecarModelsList() {
38
+ // LIVE-GATED: resolves only when the shared sidecar already exists (a live
39
+ // session or an on-demand op). Cold → undefined → the disk fallback engages;
40
+ // the reconcile tick can never spawn a sidecar (sidecar-decoupling ruling).
41
+ try {
42
+ const data = await callSharedIfLive("models.list", { refresh: true });
43
+ if (data === null || typeof data !== "object")
44
+ return undefined;
45
+ const models = data.models;
46
+ if (!Array.isArray(models))
47
+ return undefined;
48
+ const out = [];
49
+ for (const model of models) {
50
+ if (model !== null &&
51
+ typeof model === "object" &&
52
+ typeof model.provider === "string" &&
53
+ typeof model.id === "string") {
54
+ out.push(model);
55
+ }
56
+ }
57
+ return out.length > 0 ? out : undefined;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /** `settings.modelRoles.get` over the shared sidecar → validated OmpModelRoles, undefined on failure. */
64
+ async function sidecarModelRolesGet() {
65
+ try {
66
+ const data = await callShared("settings.modelRoles.get", {});
67
+ const value = data?.modelRoles;
68
+ if (value === null || typeof value !== "object" || Array.isArray(value))
69
+ return undefined;
70
+ const out = {};
71
+ for (const [role, selector] of Object.entries(value)) {
72
+ if (typeof selector === "string" && selector !== "")
73
+ out[role] = selector;
74
+ }
75
+ return Object.keys(out).length > 0 ? out : undefined;
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
81
+ /** `settings.modelRoles.set` over the shared sidecar → true only on a durable write. */
82
+ async function sidecarModelRolesSet(roles) {
83
+ try {
84
+ const data = await callShared("settings.modelRoles.set", { modelRoles: roles });
85
+ return data?.ok === true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Model catalog
93
+ // ---------------------------------------------------------------------------
94
+ let modelsCache;
95
+ /**
96
+ * Synchronous catalog read — MEMO-ONLY by design (sidecar-decoupling): no boot
97
+ * warm, no sidecar touch. `undefined` until something live swaps the cache in
98
+ * ({@link ompAvailableModels} on an already-live sidecar), so the boot path in
99
+ * `models.ts` falls back to the on-disk registry without spawning anything.
100
+ */
101
+ export function ompAvailableModelsSync() {
102
+ return modelsCache;
103
+ }
104
+ /**
105
+ * `models.list` — the credential-resolved catalog, LIVE-GATED: fetched only
106
+ * when the shared sidecar already exists (a live session); `undefined` and the
107
+ * cache untouched when cold. Callers fall back to the on-disk sources.
108
+ */
109
+ export async function ompAvailableModels() {
110
+ const models = await sidecarModelsList();
111
+ if (models !== undefined)
112
+ modelsCache = models;
113
+ return models;
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // modelRoles
117
+ // ---------------------------------------------------------------------------
118
+ let rolesCache;
119
+ /**
120
+ * Synchronous roles read — memo-only, same contract as
121
+ * {@link ompAvailableModelsSync}. The boot path reads modelRoles from
122
+ * `config.yml` on disk (omp-store); this cache only ever reflects what a live
123
+ * sidecar reported. The write path (`ompModelRoles`/`ompSetModelRoles`) may
124
+ * spawn on real need — a default-model change is a user action, not a tick.
125
+ */
126
+ export function ompModelRolesSync() {
127
+ return rolesCache;
128
+ }
129
+ /**
130
+ * `settings.modelRoles.get` — the current modelRoles object, fetched fresh
131
+ * from the sidecar on every call (the OMP TUI may have rewritten config.yml).
132
+ * `undefined` when unset or the sidecar fails.
133
+ */
134
+ export async function ompModelRoles() {
135
+ const roles = await sidecarModelRolesGet();
136
+ if (roles !== undefined)
137
+ rolesCache = roles;
138
+ return roles;
139
+ }
140
+ /**
141
+ * `settings.modelRoles.set` — the TUI-grade default-model write: the sidecar
142
+ * replaces the whole modelRoles object (matching `omp config set modelRoles`
143
+ * whole-object semantics) and confirms the write landed before returning true.
144
+ */
145
+ export async function ompSetModelRoles(roles) {
146
+ return sidecarModelRolesSet(roles);
147
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Host-side disk readers for OMP slash commands and skills.
3
+ *
4
+ * Replaces the sidecar round-trips (`slashCommands.list` / `skills.list`) that
5
+ * used to spawn the whole bun/OMP core just to walk a few directories. The
6
+ * upstream discovery (pi-coding-agent src/discovery/*) is a 10+ provider
7
+ * capability web we deliberately do NOT port; v1 scans the practical roots:
8
+ *
9
+ * commands: <OMP_NATIVE_HOME>/agent/commands/*.md (native user)
10
+ * <cwd>/.omp/commands/*.md (native project)
11
+ * ~/.claude/commands/*.md (claude user)
12
+ * <cwd>/.claude/commands/*.md (claude project)
13
+ * skills: the same four roots' `skills/` sibling, each entry either
14
+ * `<root>/<name>/SKILL.md` (Agent Skills standard) or a lenient
15
+ * flat `<root>/*.md`.
16
+ *
17
+ * Known divergences from the SDK path (accepted, see plan §3): no embedded
18
+ * bundled commands, no codex/opencode/cursor/gemini providers, no capability
19
+ * semantics (at-imports, managed skills, extensionRoots reload). Dedupe order
20
+ * is scan order with FIRST occurrence winning: omp user → claude user →
21
+ * omp project → claude project. All readers are fail-soft: missing roots or
22
+ * unreadable files are skipped, never thrown.
23
+ *
24
+ * @module omp-web/omp-disk-discovery
25
+ */
26
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { join } from "node:path";
29
+ import { resolveOmpNativeHome } from "./knobs.js";
30
+ function discoveryRoots(cwd, kind) {
31
+ const home = homedir();
32
+ const roots = [
33
+ { base: resolveOmpNativeHome(), subdir: join("agent", kind), label: `omp:user` },
34
+ { base: home, subdir: join(".claude", kind), label: `claude:user` },
35
+ ];
36
+ if (cwd !== undefined && cwd !== "") {
37
+ roots.push({ base: cwd, subdir: join(".omp", kind), label: `omp:project` }, { base: cwd, subdir: join(".claude", kind), label: `claude:project` });
38
+ }
39
+ return roots;
40
+ }
41
+ /**
42
+ * Split an optional YAML-ish frontmatter from a markdown body and derive the
43
+ * display description the way the SDK's parseCommandTemplate does: the
44
+ * frontmatter `description` when present, else the first non-empty body line
45
+ * trimmed to 60 chars. Unknown keys are ignored; a malformed block degrades to
46
+ * "whole file is body".
47
+ */
48
+ function parseMarkdownTemplate(raw) {
49
+ const text = raw.replace(/\r\n/g, "\n");
50
+ if (!text.startsWith("---\n")) {
51
+ return { description: fallbackDescription(text), content: text };
52
+ }
53
+ const end = text.indexOf("\n---", 4);
54
+ if (end === -1) {
55
+ return { description: fallbackDescription(text), content: text };
56
+ }
57
+ const frontmatter = text.slice(4, end);
58
+ const bodyStart = text.indexOf("\n", end + 1);
59
+ const body = bodyStart === -1 ? "" : text.slice(bodyStart + 1).replace(/^\n+/, "");
60
+ let description;
61
+ for (const line of frontmatter.split("\n")) {
62
+ const match = /^(description|argument-hint):\s*(.*)$/.exec(line);
63
+ if (match !== null && match[2] !== "") {
64
+ description = match[2].trim().replace(/^["']|["']$/g, "");
65
+ break;
66
+ }
67
+ }
68
+ return { description: description ?? fallbackDescription(body), content: body };
69
+ }
70
+ function fallbackDescription(body) {
71
+ const line = body.split("\n").find((candidate) => candidate.trim() !== "") ?? "";
72
+ return line.trim().slice(0, 60);
73
+ }
74
+ function listMarkdownFiles(dir) {
75
+ try {
76
+ return readdirSync(dir, { withFileTypes: true })
77
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
78
+ .map((entry) => entry.name)
79
+ .sort();
80
+ }
81
+ catch {
82
+ return [];
83
+ }
84
+ }
85
+ function readFileSoft(path) {
86
+ try {
87
+ return readFileSync(path, "utf8");
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ }
93
+ /**
94
+ * Read every file-based slash command from the discovery roots.
95
+ * Deterministic output: deduped by name (scan order, first wins), then sorted.
96
+ */
97
+ export function readSlashCommands(cwd) {
98
+ const byName = new Map();
99
+ for (const root of discoveryRoots(cwd, "commands")) {
100
+ const dir = join(root.base, root.subdir);
101
+ for (const file of listMarkdownFiles(dir)) {
102
+ const name = file.replace(/\.md$/i, "");
103
+ if (name === "" || byName.has(name))
104
+ continue;
105
+ const raw = readFileSoft(join(dir, file));
106
+ if (raw === undefined)
107
+ continue;
108
+ const parsed = parseMarkdownTemplate(raw);
109
+ byName.set(name, {
110
+ name,
111
+ description: parsed.description,
112
+ content: parsed.content,
113
+ source: root.label,
114
+ filePath: join(dir, file),
115
+ });
116
+ }
117
+ }
118
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
119
+ }
120
+ function readSkillFile(path, label) {
121
+ const raw = readFileSoft(path);
122
+ if (raw === undefined)
123
+ return undefined;
124
+ const parsed = parseMarkdownTemplate(raw);
125
+ // Frontmatter `name:` may rename the skill; default to the directory/file name.
126
+ let name = "";
127
+ const frontmatterName = /^---\n[\s\S]*?^name:\s*(\S+)/m.exec(raw);
128
+ if (frontmatterName !== null)
129
+ name = frontmatterName[1];
130
+ if (name === "") {
131
+ const base = path.split("/").pop() ?? "";
132
+ name = base.toLowerCase() === "skill.md" ? (path.split("/").at(-2) ?? base) : base.replace(/\.md$/i, "");
133
+ }
134
+ return {
135
+ name,
136
+ description: parsed.description,
137
+ filePath: path,
138
+ baseDir: path.slice(0, path.lastIndexOf("/")) || path,
139
+ source: label,
140
+ };
141
+ }
142
+ /**
143
+ * Read every discovered skill from the discovery roots: the Agent Skills
144
+ * standard `<root>/<name>/SKILL.md` layout, plus a lenient flat `<root>/*.md`.
145
+ * Deterministic output: deduped by name (scan order, first wins), then sorted.
146
+ */
147
+ export function readSkills(cwd) {
148
+ const byName = new Map();
149
+ for (const root of discoveryRoots(cwd, "skills")) {
150
+ const dir = join(root.base, root.subdir);
151
+ if (!existsSync(dir))
152
+ continue;
153
+ let entries = [];
154
+ try {
155
+ entries = readdirSync(dir, { withFileTypes: true }).map((entry) => ({
156
+ name: entry.name,
157
+ isDirectory: entry.isDirectory(),
158
+ }));
159
+ }
160
+ catch {
161
+ continue;
162
+ }
163
+ for (const entry of entries) {
164
+ const path = entry.isDirectory
165
+ ? join(dir, entry.name, "SKILL.md")
166
+ : join(dir, entry.name);
167
+ if (!entry.isDirectory && !entry.name.toLowerCase().endsWith(".md"))
168
+ continue;
169
+ if (entry.isDirectory && byName.has(entry.name))
170
+ continue;
171
+ const skill = readSkillFile(path, root.label);
172
+ if (skill === undefined || byName.has(skill.name))
173
+ continue;
174
+ byName.set(skill.name, skill);
175
+ }
176
+ }
177
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
178
+ }