@tsconfigdotjson/vibe-os 0.2.0-rc.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -847
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/web/assets/index-82MhMOB4.css +1 -0
- package/dist/web/assets/index-82MhMOB4.css.br +0 -0
- package/dist/web/assets/index-82MhMOB4.css.gz +0 -0
- package/dist/web/assets/index-DCB2nO0u.js +36 -0
- package/dist/web/assets/index-DCB2nO0u.js.br +0 -0
- package/dist/web/assets/index-DCB2nO0u.js.gz +0 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/scripts/notices.ts +3 -1
- package/server/api.ts +449 -20
- package/server/assets.generated.ts +13 -13
- package/server/attach.ts +65 -16
- package/server/browser.ts +1 -65
- package/server/cli.ts +48 -150
- package/server/client.ts +440 -0
- package/server/config.ts +64 -2
- package/server/db.ts +19 -0
- package/server/doctor.ts +176 -123
- package/server/harness.ts +378 -590
- package/server/harnesses/claude.ts +248 -0
- package/server/harnesses/codex.ts +241 -0
- package/server/harnesses/cursor.ts +202 -0
- package/server/harnesses/hermes.ts +259 -0
- package/server/harnesses/util.ts +181 -0
- package/server/headless.ts +358 -0
- package/server/index.ts +47 -14
- package/server/memory.ts +418 -0
- package/server/profiles.ts +27 -15
- package/server/projects.ts +52 -3
- package/server/reaper.ts +5 -3
- package/server/screens.ts +372 -0
- package/server/session.ts +84 -95
- package/server/setup.ts +73 -7
- package/server/windows.ts +51 -1
- package/shared/blanks.ts +113 -0
- package/shared/harness.ts +375 -0
- package/shared/wire.ts +105 -94
- package/dist/web/assets/index-B77Qk8B5.js +0 -35
- package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
- package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css +0 -1
- package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Hermes.
|
|
2
|
+
//
|
|
3
|
+
// It has no listing mode at all (`hermes model` is an interactive wizard), so
|
|
4
|
+
// what is discoverable is its own configuration: which providers it has been
|
|
5
|
+
// set up with and what it would use by default. The model and provider fields
|
|
6
|
+
// stay free text with those as suggestions.
|
|
7
|
+
//
|
|
8
|
+
// No floor of well-known provider names. A Hermes provider id is account
|
|
9
|
+
// configuration, so offering one that is not on this box would suggest a choice
|
|
10
|
+
// that can only fail at launch.
|
|
11
|
+
|
|
12
|
+
import { log } from "../log.ts";
|
|
13
|
+
import { type HarnessAdapter, looksLikeLadder, run } from "./util.ts";
|
|
14
|
+
|
|
15
|
+
export const HERMES_INSTALL =
|
|
16
|
+
"curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The reasoning ladder as of writing, used only when the help cannot be read.
|
|
20
|
+
* Not merged with what was discovered, because the list is the scale.
|
|
21
|
+
*/
|
|
22
|
+
const KNOWN_REASONING = [
|
|
23
|
+
"none",
|
|
24
|
+
"minimal",
|
|
25
|
+
"low",
|
|
26
|
+
"medium",
|
|
27
|
+
"high",
|
|
28
|
+
"xhigh",
|
|
29
|
+
"max",
|
|
30
|
+
"ultra",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One `hermes config get <key> --json`, parsed, or null.
|
|
35
|
+
*
|
|
36
|
+
* Null is "could not tell", never "not set": a key never written, a build that
|
|
37
|
+
* spells it differently and a Python traceback all arrive here the same way.
|
|
38
|
+
*/
|
|
39
|
+
async function hermesConfig(
|
|
40
|
+
binary: string,
|
|
41
|
+
key: string,
|
|
42
|
+
): Promise<Record<string, unknown> | null> {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await run(binary, ["config", "get", key, "--json"], {
|
|
45
|
+
timeout: 20_000,
|
|
46
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
47
|
+
});
|
|
48
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
49
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
50
|
+
? (parsed as Record<string, unknown>)
|
|
51
|
+
: null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The levels `--reasoning` lists in its own help output.
|
|
59
|
+
*
|
|
60
|
+
* Hermes writes "Reasoning effort for this session: none, minimal, low, medium,
|
|
61
|
+
* high, xhigh, max, or ultra." across two wrapped lines, so the anchor is the
|
|
62
|
+
* first colon after the flag and the terminator is the full stop.
|
|
63
|
+
*/
|
|
64
|
+
export function reasoningLevels(help: string): string[] {
|
|
65
|
+
const section = /--reasoning[\s\S]{0,200}?:\s*([\s\S]{0,200}?)\./.exec(help);
|
|
66
|
+
if (!section) return [];
|
|
67
|
+
const levels = section[1]
|
|
68
|
+
.split(/,|\bor\b/)
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
return looksLikeLadder(levels) ? levels : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A field of a config block, when it is a non-empty string. */
|
|
75
|
+
function str(
|
|
76
|
+
block: Record<string, unknown> | null,
|
|
77
|
+
key: string,
|
|
78
|
+
): string | null {
|
|
79
|
+
const value = block?.[key];
|
|
80
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const hermes: HarnessAdapter = {
|
|
84
|
+
spec: {
|
|
85
|
+
id: "hermes",
|
|
86
|
+
label: "Hermes",
|
|
87
|
+
command: "hermes",
|
|
88
|
+
// `/usr/local/bin` is its FHS default; `~/.local/bin` is what it uses when
|
|
89
|
+
// it cannot write there.
|
|
90
|
+
paths: ["~/.local/bin/hermes", "/usr/local/bin/hermes"],
|
|
91
|
+
// `--model` and `--provider` belong to the `chat` subcommand, so a profile
|
|
92
|
+
// that picks a model has to name it. Prepended rather than stored, because
|
|
93
|
+
// it is not a flag anyone should be able to delete in the editor.
|
|
94
|
+
leading: ["chat"],
|
|
95
|
+
resume: {
|
|
96
|
+
args: ["--continue"],
|
|
97
|
+
unless: ["--continue", "-c", "--resume", "-r"],
|
|
98
|
+
},
|
|
99
|
+
install: HERMES_INSTALL,
|
|
100
|
+
defaults: ["--yolo"],
|
|
101
|
+
fields: [
|
|
102
|
+
{
|
|
103
|
+
kind: "select",
|
|
104
|
+
key: "model",
|
|
105
|
+
label: "Model",
|
|
106
|
+
flag: "--model",
|
|
107
|
+
aliases: ["-m"],
|
|
108
|
+
free: true,
|
|
109
|
+
placeholder: "Default — whatever Hermes is set to",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
kind: "select",
|
|
113
|
+
key: "provider",
|
|
114
|
+
label: "Provider",
|
|
115
|
+
flag: "--provider",
|
|
116
|
+
free: true,
|
|
117
|
+
placeholder: "Default — whatever Hermes is set to",
|
|
118
|
+
hint: "Providers this box is set up with. Add one with `hermes model` on the box and it appears here.",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
kind: "select",
|
|
122
|
+
key: "reasoning",
|
|
123
|
+
label: "Thinking",
|
|
124
|
+
flag: "--reasoning",
|
|
125
|
+
options: KNOWN_REASONING.map((value) => ({ value })),
|
|
126
|
+
none: "Default — whatever Hermes picks",
|
|
127
|
+
hint: "Read off `hermes chat --help` on the box, so the ladder follows Hermes' releases.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
kind: "select",
|
|
131
|
+
key: "interface",
|
|
132
|
+
label: "Interface",
|
|
133
|
+
none: "Default — whatever Hermes picks",
|
|
134
|
+
specials: [
|
|
135
|
+
{ value: "cli", label: "Classic — a plain REPL", flag: "--cli" },
|
|
136
|
+
{
|
|
137
|
+
value: "tui",
|
|
138
|
+
label: "Full screen — overlays and mouse selection",
|
|
139
|
+
flag: "--tui",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
kind: "toggle",
|
|
145
|
+
flag: "--yolo",
|
|
146
|
+
label: "Skip approvals",
|
|
147
|
+
hint: "Run dangerous commands without asking. Same trade as Claude's skip-every-check.",
|
|
148
|
+
warn: "This session will not ask before running anything. Reasonable on a box that is already a sandbox; think twice anywhere else.",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
kind: "toggle",
|
|
152
|
+
flag: "--continue",
|
|
153
|
+
// `-c` is the short form and takes an optional session name. Only the
|
|
154
|
+
// bare form is this switch; `-c mine` is listed under `optional`.
|
|
155
|
+
aliases: ["-c"],
|
|
156
|
+
label: "Resume last conversation",
|
|
157
|
+
hint: "Pick up the most recent session rather than starting a new one.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
kind: "toggle",
|
|
161
|
+
flag: "--ignore-rules",
|
|
162
|
+
label: "Ignore rules and memory",
|
|
163
|
+
hint: "Skip AGENTS.md, SOUL.md and the memory directory for this session.",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
kind: "toggle",
|
|
167
|
+
flag: "--ignore-user-config",
|
|
168
|
+
label: "Ignore user config",
|
|
169
|
+
hint: "Use defaults instead of ~/.hermes/config.yaml. Credentials still load.",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
kind: "toggle",
|
|
173
|
+
flag: "--safe-mode",
|
|
174
|
+
label: "Safe mode",
|
|
175
|
+
hint: "Disable every customisation at once. The reproducible-run switch.",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
kind: "toggle",
|
|
179
|
+
flag: "--checkpoints",
|
|
180
|
+
label: "Checkpoints",
|
|
181
|
+
hint: "Snapshot files before destructive edits, so /rollback can undo them.",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
kind: "toggle",
|
|
185
|
+
flag: "--verbose",
|
|
186
|
+
label: "Verbose output",
|
|
187
|
+
hint: "Show full tool output rather than the collapsed form.",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
kind: "toggle",
|
|
191
|
+
flag: "--pass-session-id",
|
|
192
|
+
label: "Pass session id",
|
|
193
|
+
hint: "Put the session id in the system prompt, so the agent can name its own session.",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
// From `hermes chat --help` on a real install rather than the docs, which
|
|
197
|
+
// list a `-p/--profile` the CLI does not accept.
|
|
198
|
+
valued: [
|
|
199
|
+
"--in",
|
|
200
|
+
"--resume",
|
|
201
|
+
"-r",
|
|
202
|
+
"--toolsets",
|
|
203
|
+
"-t",
|
|
204
|
+
"--skills",
|
|
205
|
+
"-s",
|
|
206
|
+
"--max-turns",
|
|
207
|
+
"--source",
|
|
208
|
+
"--image",
|
|
209
|
+
"--query",
|
|
210
|
+
"-q",
|
|
211
|
+
],
|
|
212
|
+
optional: ["-c"],
|
|
213
|
+
extraPlaceholder: '--append-system-prompt "…"',
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async discover(binary) {
|
|
217
|
+
// Concurrently: `hermes` is Python, so each invocation costs seconds of
|
|
218
|
+
// interpreter and import time before it does anything.
|
|
219
|
+
const [help, model, providers] = await Promise.all([
|
|
220
|
+
// `chat`, not the bare command: `--reasoning` is a flag of that
|
|
221
|
+
// subcommand, which is also the one a profile launches.
|
|
222
|
+
run(binary, ["chat", "--help"], {
|
|
223
|
+
timeout: 30_000,
|
|
224
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
225
|
+
})
|
|
226
|
+
.then(({ stdout }) => stdout)
|
|
227
|
+
.catch(() => ""),
|
|
228
|
+
hermesConfig(binary, "model"),
|
|
229
|
+
hermesConfig(binary, "providers"),
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
const defaultProvider = str(model, "provider");
|
|
233
|
+
const defaultModel = str(model, "default");
|
|
234
|
+
const names = [
|
|
235
|
+
...Object.keys(providers ?? {}).filter((n) => n.trim()),
|
|
236
|
+
...(defaultProvider ? [defaultProvider] : []),
|
|
237
|
+
];
|
|
238
|
+
const reasoning = reasoningLevels(help);
|
|
239
|
+
|
|
240
|
+
log.debug(
|
|
241
|
+
`hermes: ${names.length} providers, ` +
|
|
242
|
+
`${reasoning.length > 0 ? reasoning.join("/") : "no"} reasoning levels`,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
options: {
|
|
247
|
+
model: defaultModel ? [{ value: defaultModel }] : [],
|
|
248
|
+
provider: [...new Set(names)].sort().map((value) => ({ value })),
|
|
249
|
+
...(reasoning.length > 0
|
|
250
|
+
? { reasoning: reasoning.map((value) => ({ value })) }
|
|
251
|
+
: {}),
|
|
252
|
+
},
|
|
253
|
+
defaults: {
|
|
254
|
+
...(defaultModel ? { model: defaultModel } : {}),
|
|
255
|
+
...(defaultProvider ? { provider: defaultProvider } : {}),
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// What every harness adapter needs to ask the box about a binary.
|
|
2
|
+
//
|
|
3
|
+
// Kept apart from the registry in `../harness.ts` so an adapter can import it
|
|
4
|
+
// without importing the list of adapters it is itself a member of.
|
|
5
|
+
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { stat } from "node:fs/promises";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import type { HarnessReport, HarnessSpec } from "../../shared/harness.ts";
|
|
10
|
+
|
|
11
|
+
export const home = process.env.HOME ?? "";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The PATH a window's harness gets, so discovery sees what a window will.
|
|
17
|
+
*
|
|
18
|
+
* A window exports `$HOME/.local/bin` ahead of everything (see `launch` in
|
|
19
|
+
* `session.ts`). Without the same here, a CLI that is a Node script, like
|
|
20
|
+
* Codex, resolves but cannot start when `node` itself lives in
|
|
21
|
+
* `~/.local/bin`: `vibe-os doctor` over a plain `ssh host 'command'` called
|
|
22
|
+
* it installed with no version and could not read its login.
|
|
23
|
+
*/
|
|
24
|
+
const env = {
|
|
25
|
+
...process.env,
|
|
26
|
+
PATH: `${home}/.local/bin:${process.env.PATH ?? ""}`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const run = (
|
|
30
|
+
file: string,
|
|
31
|
+
args: string[],
|
|
32
|
+
opts: { timeout?: number; maxBuffer?: number } = {},
|
|
33
|
+
) => execFileAsync(file, args, { ...opts, env, encoding: "utf8" });
|
|
34
|
+
|
|
35
|
+
/** `~/x` to `$HOME/x`, for the paths a spec lists. */
|
|
36
|
+
export const expandHome = (p: string): string =>
|
|
37
|
+
p === "~" ? home : p.startsWith("~/") ? `${home}${p.slice(1)}` : p;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a command through PATH, then the given locations, then symlinks.
|
|
41
|
+
*
|
|
42
|
+
* A login shell picks `~/.local/bin` up from `.profile`; a systemd service does
|
|
43
|
+
* not, and vibe-os is meant to run as one. Without the extra locations the
|
|
44
|
+
* server cannot find a binary the window will happily launch a moment later,
|
|
45
|
+
* because the window runs under a login-ish shell with that PATH restored and
|
|
46
|
+
* the server does not.
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveBinary(
|
|
49
|
+
command: string,
|
|
50
|
+
extraPaths: string[],
|
|
51
|
+
): Promise<string | null> {
|
|
52
|
+
const candidates: string[] = [];
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await run(
|
|
55
|
+
"/bin/sh",
|
|
56
|
+
["-c", 'command -v "$1"', "sh", command],
|
|
57
|
+
{ timeout: 5_000 },
|
|
58
|
+
);
|
|
59
|
+
if (stdout.trim()) candidates.push(stdout.trim());
|
|
60
|
+
} catch {
|
|
61
|
+
// not on PATH; the explicit locations may still have it
|
|
62
|
+
}
|
|
63
|
+
candidates.push(...extraPaths.map(expandHome).filter(Boolean));
|
|
64
|
+
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
try {
|
|
67
|
+
await stat(candidate);
|
|
68
|
+
const { stdout: real } = await run(
|
|
69
|
+
"/bin/sh",
|
|
70
|
+
["-c", 'readlink -f "$1"', "sh", candidate],
|
|
71
|
+
{ timeout: 5_000 },
|
|
72
|
+
);
|
|
73
|
+
return real.trim() || candidate;
|
|
74
|
+
} catch {
|
|
75
|
+
// next
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Runs a command and returns everything it printed, whatever it exited with.
|
|
83
|
+
*
|
|
84
|
+
* For the questions whose answer is the text either way: `codex login status`
|
|
85
|
+
* prints "Not logged in" to stderr and exits 1, and that is an answer, not a
|
|
86
|
+
* failure. Null only when nothing ran at all.
|
|
87
|
+
*/
|
|
88
|
+
export async function output(
|
|
89
|
+
binary: string,
|
|
90
|
+
args: string[],
|
|
91
|
+
timeout = 30_000,
|
|
92
|
+
): Promise<string | null> {
|
|
93
|
+
try {
|
|
94
|
+
const { stdout, stderr } = await run(binary, args, {
|
|
95
|
+
timeout,
|
|
96
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
97
|
+
});
|
|
98
|
+
return `${stdout}\n${stderr}`;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const e = err as { stdout?: unknown; stderr?: unknown; killed?: boolean };
|
|
101
|
+
if (e.killed) return null;
|
|
102
|
+
const text = `${typeof e.stdout === "string" ? e.stdout : ""}\n${typeof e.stderr === "string" ? e.stderr : ""}`;
|
|
103
|
+
return text.trim() ? text : null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The version-shaped token on the first line that has anything on it.
|
|
109
|
+
*
|
|
110
|
+
* Covers `2.1.3 (Claude Code)`, `codex-cli 0.160.1` and Cursor's date-shaped
|
|
111
|
+
* `2026.08.11-e8db854` alike. Only the first line is a claim about the binary
|
|
112
|
+
* itself; a wrapper or update notice printed after it must not become the
|
|
113
|
+
* answer.
|
|
114
|
+
*/
|
|
115
|
+
export function versionToken(stdout: string): string | null {
|
|
116
|
+
const first = stdout.split("\n").find((l) => l.trim()) ?? "";
|
|
117
|
+
const match = /\bv?(\d+(?:\.\d+)+[^\s()]*)/.exec(first);
|
|
118
|
+
return match ? match[1] : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What a status command says about being logged in, when it says anything.
|
|
123
|
+
*
|
|
124
|
+
* "Not logged in" contains "logged in", so the negative is checked first. Text
|
|
125
|
+
* this cannot read returns null rather than false: only a definite "no" should
|
|
126
|
+
* make the editor warn that a window will sit at a login prompt.
|
|
127
|
+
*/
|
|
128
|
+
export function loggedIn(text: string): boolean | null {
|
|
129
|
+
const lower = text.toLowerCase();
|
|
130
|
+
if (
|
|
131
|
+
/not\s+logged\s+in|logged\s+out|unauthenticated|no.*credentials/.test(lower)
|
|
132
|
+
)
|
|
133
|
+
return false;
|
|
134
|
+
if (/logged\s+in|signed\s+in|authenticated/.test(lower)) return true;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Checks that read a ladder have to keep its order.
|
|
140
|
+
*
|
|
141
|
+
* A thinking level is ordinal, so the list is the scale, and anything that does
|
|
142
|
+
* not look like a level means the parse caught a sentence. Half a list read out
|
|
143
|
+
* of a paragraph would be worse than none, so the caller falls back to its own.
|
|
144
|
+
*/
|
|
145
|
+
export const looksLikeLadder = (levels: string[]): boolean =>
|
|
146
|
+
levels.length >= 2 && levels.every((l) => /^[a-z][a-z0-9-]*$/.test(l));
|
|
147
|
+
|
|
148
|
+
/** A check doctor prints, without importing doctor. */
|
|
149
|
+
export interface HarnessCheck {
|
|
150
|
+
label: string;
|
|
151
|
+
ok: boolean;
|
|
152
|
+
detail: string;
|
|
153
|
+
fix?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** What an adapter's own discovery adds to the generic version and login. */
|
|
157
|
+
export interface Discovery<D = unknown> {
|
|
158
|
+
/** Overrides the generic `--version` read, for a harness that prints oddly. */
|
|
159
|
+
version?: string | null;
|
|
160
|
+
options?: HarnessReport["options"];
|
|
161
|
+
defaults?: HarnessReport["defaults"];
|
|
162
|
+
notes?: HarnessReport["notes"];
|
|
163
|
+
/** Anything the adapter's own doctor checks need that the report does not carry. */
|
|
164
|
+
detail?: D;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A harness: a spec, plus whatever code it needs that data cannot say.
|
|
169
|
+
*
|
|
170
|
+
* Every hook is optional. A spec alone is a working harness, which is what a
|
|
171
|
+
* `~/.vibe-os/harnesses/*.json` file is.
|
|
172
|
+
*/
|
|
173
|
+
export interface HarnessAdapter<D = unknown> {
|
|
174
|
+
spec: HarnessSpec;
|
|
175
|
+
/** Reads models and options off the installed binary. */
|
|
176
|
+
discover?: (binary: string) => Promise<Discovery<D>>;
|
|
177
|
+
/** Shell run in the window before the harness starts. */
|
|
178
|
+
setup?: (where: { cwd: string; stateDir: string }) => string;
|
|
179
|
+
/** Doctor checks beyond installed and logged in. */
|
|
180
|
+
checks?: (report: HarnessReport, detail: D | undefined) => HarnessCheck[];
|
|
181
|
+
}
|