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.
- package/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- 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
|
+
}
|