@oh-hai/cli 0.1.0-beta.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 +154 -0
- package/dist/auth/file-backend.d.ts +16 -0
- package/dist/auth/file-backend.js +98 -0
- package/dist/auth/file-backend.js.map +1 -0
- package/dist/auth/keychain.d.ts +54 -0
- package/dist/auth/keychain.js +232 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/resolve-token.d.ts +34 -0
- package/dist/auth/resolve-token.js +91 -0
- package/dist/auth/resolve-token.js.map +1 -0
- package/dist/auth/secure-write.d.ts +2 -0
- package/dist/auth/secure-write.js +30 -0
- package/dist/auth/secure-write.js.map +1 -0
- package/dist/auth/token-store.d.ts +104 -0
- package/dist/auth/token-store.js +208 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +238 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +370 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/ask.d.ts +2 -0
- package/dist/commands/ask.js +246 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/context.d.ts +72 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +237 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flags.d.ts +25 -0
- package/dist/commands/flags.js +100 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/handlers.d.ts +2 -0
- package/dist/commands/handlers.js +26 -0
- package/dist/commands/handlers.js.map +1 -0
- package/dist/commands/http.d.ts +8 -0
- package/dist/commands/http.js +19 -0
- package/dist/commands/http.js.map +1 -0
- package/dist/commands/inbox.d.ts +2 -0
- package/dist/commands/inbox.js +111 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +272 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/messaging/await.d.ts +43 -0
- package/dist/commands/messaging/await.js +125 -0
- package/dist/commands/messaging/await.js.map +1 -0
- package/dist/commands/messaging/build.d.ts +46 -0
- package/dist/commands/messaging/build.js +66 -0
- package/dist/commands/messaging/build.js.map +1 -0
- package/dist/commands/messaging/http.d.ts +22 -0
- package/dist/commands/messaging/http.js +270 -0
- package/dist/commands/messaging/http.js.map +1 -0
- package/dist/commands/messaging/identity.d.ts +29 -0
- package/dist/commands/messaging/identity.js +63 -0
- package/dist/commands/messaging/identity.js.map +1 -0
- package/dist/commands/messaging/shared.d.ts +53 -0
- package/dist/commands/messaging/shared.js +135 -0
- package/dist/commands/messaging/shared.js.map +1 -0
- package/dist/commands/messaging/state.d.ts +26 -0
- package/dist/commands/messaging/state.js +82 -0
- package/dist/commands/messaging/state.js.map +1 -0
- package/dist/commands/messaging/validate.d.ts +40 -0
- package/dist/commands/messaging/validate.js +193 -0
- package/dist/commands/messaging/validate.js.map +1 -0
- package/dist/commands/messaging/wire.d.ts +133 -0
- package/dist/commands/messaging/wire.js +16 -0
- package/dist/commands/messaging/wire.js.map +1 -0
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +68 -0
- package/dist/commands/notify.js.map +1 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/commands/registry.js +144 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/stub.d.ts +1 -0
- package/dist/commands/stub.js +9 -0
- package/dist/commands/stub.js.map +1 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +223 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +90 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config-file.d.ts +38 -0
- package/dist/config-file.js +233 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/envelope.d.ts +25 -0
- package/dist/envelope.js +41 -0
- package/dist/envelope.js.map +1 -0
- package/dist/exit-codes.d.ts +51 -0
- package/dist/exit-codes.js +57 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +17 -0
- package/dist/help.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +18 -0
- package/dist/terminal.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Config-file loading (docs/specs/cli.md §6). `config.ts` owns the pure resolution CONTRACT over
|
|
2
|
+
// already-parsed config objects; this module owns the missing half — reading and parsing the TOML
|
|
3
|
+
// files into those objects (#176). Kept separate so `config.ts` stays a pure, disk-free function.
|
|
4
|
+
//
|
|
5
|
+
// Two files feed §6:
|
|
6
|
+
// • user-global ~/.config/oh-hai/config.toml ($XDG_CONFIG_HOME honored) — the ONLY file that may
|
|
7
|
+
// set routing values (base_url / default_account), plus non-routing (output / timeout_ms / color).
|
|
8
|
+
// • per-project .oh-hai/config.toml (walked up from cwd) — non-routing values only; base_url /
|
|
9
|
+
// account are passed THROUGH so `resolveConfig` can DETECT + reject them (credential-safety §6),
|
|
10
|
+
// rather than the loader silently dropping the security signal.
|
|
11
|
+
//
|
|
12
|
+
// Zero runtime deps by design: a tolerant reader for the flat `key = value` + optional `[oh-hai]`
|
|
13
|
+
// section dialect (strings, ints, bools, comments). Malformed lines are skipped, never thrown — a
|
|
14
|
+
// broken config file degrades to "no config", it never crashes the CLI.
|
|
15
|
+
import { readFileSync, statSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
18
|
+
/**
|
|
19
|
+
* Parse the minimal config dialect: flat `key = value` lines plus an optional `[oh-hai]` section.
|
|
20
|
+
* Values are quoted strings (single/double), integers (optional sign + `_` separators), or booleans.
|
|
21
|
+
* `#` starts a comment. Keys are collected from the top level (before any section header) and from an
|
|
22
|
+
* `[oh-hai]` section; keys under any other section are ignored. Unparseable lines are skipped so a
|
|
23
|
+
* malformed file degrades gracefully — this function never throws.
|
|
24
|
+
*/
|
|
25
|
+
export function parseToml(text) {
|
|
26
|
+
const out = {};
|
|
27
|
+
// Top-level (before any section header) is collected; a section header flips this per-section.
|
|
28
|
+
let collecting = true;
|
|
29
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
30
|
+
const line = rawLine.trim();
|
|
31
|
+
if (line === "" || line.startsWith("#")) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// A section header, tolerating a trailing inline comment (`[other] # note`). Matching the raw
|
|
35
|
+
// `[...]` only would leave `collecting` unchanged on a commented header, leaking that section's
|
|
36
|
+
// keys into the collected namespace.
|
|
37
|
+
const section = /^\[([^\]]*)\]\s*(?:#.*)?$/.exec(line);
|
|
38
|
+
if (section !== null) {
|
|
39
|
+
collecting = section[1]?.trim() === "oh-hai";
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!collecting) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const eq = line.indexOf("=");
|
|
46
|
+
if (eq <= 0) {
|
|
47
|
+
continue; // no key ("= value") or no separator ("garbage") — skip tolerantly
|
|
48
|
+
}
|
|
49
|
+
const key = line.slice(0, eq).trim();
|
|
50
|
+
if (key === "") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const value = parseTomlValue(line.slice(eq + 1).trim());
|
|
54
|
+
if (value !== undefined) {
|
|
55
|
+
out[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** Classify a raw right-hand-side token into a scalar, or undefined when empty/unparseable. */
|
|
61
|
+
function parseTomlValue(raw) {
|
|
62
|
+
if (raw === "") {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const quote = raw[0];
|
|
66
|
+
if (quote === '"' || quote === "'") {
|
|
67
|
+
// Quoted string: content up to the matching close quote; trailing text (e.g. a comment) ignored.
|
|
68
|
+
// A `#` inside the quotes is preserved. An unterminated quote is malformed → skip.
|
|
69
|
+
const close = raw.indexOf(quote, 1);
|
|
70
|
+
return close === -1 ? undefined : raw.slice(1, close);
|
|
71
|
+
}
|
|
72
|
+
// Bare value: drop a trailing inline comment, then classify by shape.
|
|
73
|
+
const hash = raw.indexOf("#");
|
|
74
|
+
const bare = (hash === -1 ? raw : raw.slice(0, hash)).trim();
|
|
75
|
+
if (bare === "") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (bare === "true") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (bare === "false") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (/^[+-]?\d[\d_]*$/.test(bare)) {
|
|
85
|
+
const parsed = Number.parseInt(bare.replace(/_/g, ""), 10);
|
|
86
|
+
return Number.isFinite(parsed) ? parsed : bare;
|
|
87
|
+
}
|
|
88
|
+
return bare; // an unquoted string
|
|
89
|
+
}
|
|
90
|
+
/** Type-only coercers: pick a known key when it has the expected type, else drop it (tolerance). */
|
|
91
|
+
function asString(value) {
|
|
92
|
+
// A blank string counts as absent (matching resolveConfig's presence semantics) so an empty
|
|
93
|
+
// `account = ""` can't `??`-mask a `default_account` account-setting attempt in mapProjectConfig.
|
|
94
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
95
|
+
}
|
|
96
|
+
/** Node's timer ceiling (`TIMEOUT_MAX`, 2^31 - 1 ms ≈ 24.8 days). A delay above this is silently
|
|
97
|
+
* rescheduled as 1 ms (with a `TimeoutOverflowWarning`), so an out-of-range config timeout must be
|
|
98
|
+
* dropped rather than accepted — otherwise `whoami --check` would time out almost instantly. */
|
|
99
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
100
|
+
/**
|
|
101
|
+
* A timeout must be a non-negative integer no larger than Node's timer ceiling — the same bound the
|
|
102
|
+
* `--timeout` flag and `MA2H_TIMEOUT_MS` env already enforce. A config-file value outside that range
|
|
103
|
+
* is dropped here so a stray `timeout_ms = -1` (or an above-ceiling value) can't reach
|
|
104
|
+
* `AbortSignal.timeout` and either throw or collapse to a 1 ms timeout instead of the sane default.
|
|
105
|
+
*/
|
|
106
|
+
function asTimeout(value) {
|
|
107
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= MAX_TIMEOUT_MS
|
|
108
|
+
? value
|
|
109
|
+
: undefined;
|
|
110
|
+
}
|
|
111
|
+
function asBool(value) {
|
|
112
|
+
return typeof value === "boolean" ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
function asOutput(value) {
|
|
115
|
+
return value === "human" || value === "json" ? value : undefined;
|
|
116
|
+
}
|
|
117
|
+
/** Map a parsed record to the user-global config — the only file that may carry routing values. */
|
|
118
|
+
function mapUserConfig(record) {
|
|
119
|
+
return {
|
|
120
|
+
base_url: asString(record.base_url),
|
|
121
|
+
default_account: asString(record.default_account),
|
|
122
|
+
output: asOutput(record.output),
|
|
123
|
+
timeout_ms: asTimeout(record.timeout_ms),
|
|
124
|
+
color: asBool(record.color),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Map a parsed record to the per-project config. base_url / account are carried through on purpose:
|
|
129
|
+
* `resolveConfig` detects and rejects them with a warning (credential-safety §6) — dropping them here
|
|
130
|
+
* would hide the fact that a repo tried to redirect the authenticated Hub.
|
|
131
|
+
*/
|
|
132
|
+
function mapProjectConfig(record) {
|
|
133
|
+
return {
|
|
134
|
+
base_url: asString(record.base_url),
|
|
135
|
+
// Accept BOTH the project key (`account`) and the user-config spelling (`default_account`) as an
|
|
136
|
+
// account-setting attempt, so a repo trying to choose the identity via either name is surfaced to
|
|
137
|
+
// resolveConfig for a credential-safety rejection rather than silently ignored.
|
|
138
|
+
account: asString(record.account) ?? asString(record.default_account),
|
|
139
|
+
output: asOutput(record.output),
|
|
140
|
+
timeout_ms: asTimeout(record.timeout_ms),
|
|
141
|
+
color: asBool(record.color),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Load `~/.config/oh-hai/config.toml` ($XDG_CONFIG_HOME honored). Undefined when the file is absent. */
|
|
145
|
+
export function loadUserConfig(io) {
|
|
146
|
+
const xdg = io.xdgConfigHome?.trim();
|
|
147
|
+
const home = io.homedir.trim();
|
|
148
|
+
// The user-global config is TRUSTED (it alone may set base_url / default_account), so its directory
|
|
149
|
+
// must be ABSOLUTE — a relative path would resolve against cwd and let a repo-committed file
|
|
150
|
+
// masquerade as user-global config, bypassing per-project rejection. Prefer an absolute
|
|
151
|
+
// $XDG_CONFIG_HOME; a relative/blank XDG is ignored in favor of the ~/.config default; if neither
|
|
152
|
+
// yields an absolute dir (e.g. a minimal container with an empty homedir), skip the user config.
|
|
153
|
+
const configHome = xdg !== undefined && xdg !== "" && isAbsolute(xdg)
|
|
154
|
+
? xdg
|
|
155
|
+
: home !== "" && isAbsolute(home)
|
|
156
|
+
? join(home, ".config")
|
|
157
|
+
: undefined;
|
|
158
|
+
if (configHome === undefined) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const text = io.readFile(join(configHome, "oh-hai", "config.toml"));
|
|
162
|
+
return text === undefined ? undefined : mapUserConfig(parseToml(text));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Discover the per-project config by walking up from cwd for a `.oh-hai/config.toml`; the nearest
|
|
166
|
+
* ancestor wins. Undefined when none is found up to the filesystem root.
|
|
167
|
+
*/
|
|
168
|
+
export function loadProjectConfig(io) {
|
|
169
|
+
// Project discovery needs a real absolute cwd. A non-absolute cwd (e.g. "" when the working
|
|
170
|
+
// directory was removed before the process started) has nothing meaningful to walk up, so skip
|
|
171
|
+
// rather than resolving relative paths against a missing cwd.
|
|
172
|
+
if (!isAbsolute(io.cwd)) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
let dir = io.cwd;
|
|
176
|
+
for (;;) {
|
|
177
|
+
const text = io.readFile(join(dir, ".oh-hai", "config.toml"));
|
|
178
|
+
if (text !== undefined) {
|
|
179
|
+
return mapProjectConfig(parseToml(text));
|
|
180
|
+
}
|
|
181
|
+
const parent = dirname(dir);
|
|
182
|
+
if (parent === dir) {
|
|
183
|
+
return undefined; // reached the root without a hit
|
|
184
|
+
}
|
|
185
|
+
dir = parent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Load both config files (user-global + per-project) for `resolveConfig`. */
|
|
189
|
+
export function loadConfigFiles(io) {
|
|
190
|
+
return {
|
|
191
|
+
userConfig: loadUserConfig(io),
|
|
192
|
+
projectConfig: loadProjectConfig(io),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Generous ceiling on a config file's size — real configs are a few keys; anything larger is either
|
|
196
|
+
* a mistake or a hostile plant, so it's skipped rather than read into memory. */
|
|
197
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
198
|
+
/** Read an OS value that can throw (notably `process.cwd()` when the working dir was removed), degrading
|
|
199
|
+
* to "" so config loading never crashes a command that doesn't even need the value. */
|
|
200
|
+
function safeOsValue(read) {
|
|
201
|
+
try {
|
|
202
|
+
return read();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Production `ConfigFileIo` backed by the real filesystem, home dir, cwd, and environment. */
|
|
209
|
+
export function nodeConfigFileIo() {
|
|
210
|
+
return {
|
|
211
|
+
readFile: (path) => {
|
|
212
|
+
try {
|
|
213
|
+
// The per-project `.oh-hai/config.toml` path is repo-controlled, so a hostile checkout could
|
|
214
|
+
// plant a FIFO, a device, or a symlink to `/dev/zero` there — an unconditional readFileSync
|
|
215
|
+
// would hang or exhaust memory. Stat first (stat never blocks on a FIFO) and only read a
|
|
216
|
+
// regular file within the size cap. The stat→read TOCTOU is irrelevant here: the threat is a
|
|
217
|
+
// static malicious checkout, not a local attacker racing us between the two calls.
|
|
218
|
+
const stats = statSync(path);
|
|
219
|
+
if (!stats.isFile() || stats.size > MAX_CONFIG_BYTES) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
return readFileSync(path, "utf8");
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined; // missing / unreadable → treated as "no config file here"
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
homedir: safeOsValue(homedir),
|
|
229
|
+
cwd: safeOsValue(() => process.cwd()),
|
|
230
|
+
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=config-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-file.js","sourceRoot":"","sources":["../src/config-file.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const DEFAULT_BASE_URL = "http://localhost:8787";
|
|
2
|
+
export type OutputFormat = "human" | "json";
|
|
3
|
+
export type TokenSource = "env" | "keychain" | "file" | "none";
|
|
4
|
+
/** Global flags after parsing (the subset that feeds config resolution). */
|
|
5
|
+
export interface RawFlags {
|
|
6
|
+
baseUrl?: string | undefined;
|
|
7
|
+
account?: string | undefined;
|
|
8
|
+
json?: boolean | undefined;
|
|
9
|
+
noColor?: boolean | undefined;
|
|
10
|
+
timeoutMs?: number | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** The environment variables §6 consumes (the `MA2H_*` set kept for back-compat). */
|
|
13
|
+
export interface EnvVars {
|
|
14
|
+
MA2H_BASE_URL?: string | undefined;
|
|
15
|
+
MA2H_AGENT_ID?: string | undefined;
|
|
16
|
+
MA2H_AGENT_TOKEN?: string | undefined;
|
|
17
|
+
MA2H_TIMEOUT_MS?: string | undefined;
|
|
18
|
+
NO_COLOR?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** User-global config (`~/.config/oh-hai/config.toml`) — the ONLY file that may set routing values. */
|
|
21
|
+
export interface UserConfig {
|
|
22
|
+
base_url?: string | undefined;
|
|
23
|
+
default_account?: string | undefined;
|
|
24
|
+
output?: OutputFormat | undefined;
|
|
25
|
+
timeout_ms?: number | undefined;
|
|
26
|
+
color?: boolean | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Per-project config (`.oh-hai/config.toml` discovered by walking up from cwd).
|
|
30
|
+
* `base_url` / `account` are accepted in the type only so we can DETECT and reject them —
|
|
31
|
+
* they are never honored (credential-safety, §6).
|
|
32
|
+
*/
|
|
33
|
+
export interface ProjectConfig {
|
|
34
|
+
base_url?: string | undefined;
|
|
35
|
+
account?: string | undefined;
|
|
36
|
+
output?: OutputFormat | undefined;
|
|
37
|
+
timeout_ms?: number | undefined;
|
|
38
|
+
color?: boolean | undefined;
|
|
39
|
+
}
|
|
40
|
+
export interface ResolvedConfig {
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
account: string | undefined;
|
|
43
|
+
output: OutputFormat;
|
|
44
|
+
timeoutMs: number | undefined;
|
|
45
|
+
color: boolean;
|
|
46
|
+
token: string | undefined;
|
|
47
|
+
tokenSource: TokenSource;
|
|
48
|
+
}
|
|
49
|
+
export interface ResolveInput {
|
|
50
|
+
flags?: RawFlags | undefined;
|
|
51
|
+
env?: EnvVars | undefined;
|
|
52
|
+
userConfig?: UserConfig | undefined;
|
|
53
|
+
projectConfig?: ProjectConfig | undefined;
|
|
54
|
+
}
|
|
55
|
+
export interface ResolveResult {
|
|
56
|
+
config: ResolvedConfig;
|
|
57
|
+
/** Non-fatal warnings (e.g. a per-project config tried to set a routing value). */
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the effective config from flags, env, and (already-parsed) config objects,
|
|
62
|
+
* honoring §6 precedence, the token special-case, and per-project credential-safety.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveConfig(input?: ResolveInput): ResolveResult;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Config precedence + resolution (docs/specs/cli.md §6).
|
|
2
|
+
//
|
|
3
|
+
// value := first-present( CLI flag , environment , config file , built-in default )
|
|
4
|
+
//
|
|
5
|
+
// Two rules make this security-relevant, so they live here as pure, tested functions:
|
|
6
|
+
// 1. The TOKEN is special-cased — it comes from the `MA2H_AGENT_TOKEN` env (the CI path)
|
|
7
|
+
// here, and NEVER from a config-file value (§5.4). Keychain/file backends land in #106.
|
|
8
|
+
// 2. Per-project config is credential-safe — a repo-controlled `.oh-hai/config.toml` may
|
|
9
|
+
// set only non-routing values (output/timeout/color). It MUST NOT redirect `base_url`
|
|
10
|
+
// or `account`, or a hostile checkout could send the bearer to an endpoint it chose.
|
|
11
|
+
//
|
|
12
|
+
// #105 implements the resolution CONTRACT over an already-parsed, in-memory config object.
|
|
13
|
+
// The TOML file loader + per-project discovery + keychain read land in #106/#107.
|
|
14
|
+
export const DEFAULT_BASE_URL = "http://localhost:8787";
|
|
15
|
+
/** A string is "present" only when non-empty after trimming; the trimmed value is returned
|
|
16
|
+
* so stray whitespace never rides along into a base URL / account / token. */
|
|
17
|
+
function presentString(value) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
return trimmed === "" ? undefined : trimmed;
|
|
23
|
+
}
|
|
24
|
+
/** First present value in precedence order (undefined / empty-string entries are skipped). */
|
|
25
|
+
function firstPresent(...values) {
|
|
26
|
+
for (const value of values) {
|
|
27
|
+
const present = presentString(value);
|
|
28
|
+
if (present !== undefined) {
|
|
29
|
+
return present;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function parseTimeout(value) {
|
|
35
|
+
const present = presentString(value);
|
|
36
|
+
if (present === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const parsed = Number.parseInt(present, 10);
|
|
40
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the effective config from flags, env, and (already-parsed) config objects,
|
|
44
|
+
* honoring §6 precedence, the token special-case, and per-project credential-safety.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveConfig(input = {}) {
|
|
47
|
+
const flags = input.flags ?? {};
|
|
48
|
+
const env = input.env ?? {};
|
|
49
|
+
const userConfig = input.userConfig ?? {};
|
|
50
|
+
const projectConfig = input.projectConfig ?? {};
|
|
51
|
+
const warnings = [];
|
|
52
|
+
// Credential-safety: a per-project config may NOT set base_url / account (§6). Detect
|
|
53
|
+
// and drop them so a repo-controlled checkout can never redirect the authenticated Hub.
|
|
54
|
+
if (presentString(projectConfig.base_url) !== undefined) {
|
|
55
|
+
warnings.push("ignoring per-project `base_url` — the Hub URL must come from --base-url or MA2H_BASE_URL (credential-safety, cli spec §6)");
|
|
56
|
+
}
|
|
57
|
+
if (presentString(projectConfig.account) !== undefined) {
|
|
58
|
+
warnings.push("ignoring per-project `account` — the account must come from --account or MA2H_AGENT_ID (credential-safety, cli spec §6)");
|
|
59
|
+
}
|
|
60
|
+
// base_url: flag > env > USER config only > default. Project config is excluded on purpose.
|
|
61
|
+
// Strip trailing slashes so downstream URL construction (`${baseUrl}/v1/…`, #107) can't
|
|
62
|
+
// double up — matches the scripts/ layer's resolveBaseUrl. A slash-only value strips to
|
|
63
|
+
// empty, so fall back to the default rather than yield an unusable "".
|
|
64
|
+
const resolvedBase = (firstPresent(flags.baseUrl, env.MA2H_BASE_URL, userConfig.base_url) ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
65
|
+
const baseUrl = resolvedBase === "" ? DEFAULT_BASE_URL : resolvedBase;
|
|
66
|
+
// account: flag > env > USER config only. Project config excluded on purpose.
|
|
67
|
+
const account = firstPresent(flags.account, env.MA2H_AGENT_ID, userConfig.default_account);
|
|
68
|
+
// output: an explicit --json flag wins EITHER way (--json → json, --json=false → human, upholding
|
|
69
|
+
// flag > config precedence); only when the flag is absent does the config `output` decide; else human.
|
|
70
|
+
const output = flags.json === true
|
|
71
|
+
? "json"
|
|
72
|
+
: flags.json === false
|
|
73
|
+
? "human"
|
|
74
|
+
: projectConfig.output ?? userConfig.output ?? "human";
|
|
75
|
+
// timeout: flag > env > project > user config. (project MAY set this — non-routing.)
|
|
76
|
+
const timeoutMs = flags.timeoutMs ??
|
|
77
|
+
parseTimeout(env.MA2H_TIMEOUT_MS) ??
|
|
78
|
+
projectConfig.timeout_ms ??
|
|
79
|
+
userConfig.timeout_ms;
|
|
80
|
+
// color: --no-color / NO_COLOR force off; else project/user config; else auto (true here,
|
|
81
|
+
// the print layer narrows to TTY). Non-routing, so project config MAY set it.
|
|
82
|
+
let color;
|
|
83
|
+
if (flags.noColor === true || presentString(env.NO_COLOR) !== undefined) {
|
|
84
|
+
color = false;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
color = projectConfig.color ?? userConfig.color ?? true;
|
|
88
|
+
}
|
|
89
|
+
// token: env only (#105); keychain/file land in #106. NEVER from a config value (§5.4).
|
|
90
|
+
const envToken = presentString(env.MA2H_AGENT_TOKEN);
|
|
91
|
+
const tokenSource = envToken !== undefined ? "env" : "none";
|
|
92
|
+
return {
|
|
93
|
+
config: { baseUrl, account, output, timeoutMs, color, token: envToken, tokenSource },
|
|
94
|
+
warnings,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ErrorCode, type ExitCode } from "./exit-codes.js";
|
|
2
|
+
/** A command failure carrying a stable `error.code` (§8) → §7 exit code. */
|
|
3
|
+
export declare class CliError extends Error {
|
|
4
|
+
readonly code: ErrorCode;
|
|
5
|
+
constructor(code: ErrorCode, message: string);
|
|
6
|
+
/** The §7 exit code this error maps to. */
|
|
7
|
+
get exitCode(): ExitCode;
|
|
8
|
+
}
|
|
9
|
+
export interface JsonEnvelope {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
command: string;
|
|
12
|
+
data: Record<string, unknown> | null;
|
|
13
|
+
error: {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
/** Drop `undefined` values so absent optional fields are omitted from `data`, not `null` (§8). */
|
|
19
|
+
export declare function pruneUndefined(data: Record<string, unknown>): Record<string, unknown>;
|
|
20
|
+
/** Build a success envelope. `data` is pruned of undefined optionals. */
|
|
21
|
+
export declare function buildOk(command: string, data: Record<string, unknown>): JsonEnvelope;
|
|
22
|
+
/** Build a failure envelope. `data` is always null; the token is never present (§5.4). */
|
|
23
|
+
export declare function buildErr(command: string, code: string, message: string): JsonEnvelope;
|
|
24
|
+
/** Serialize an envelope to the single line printed on stdout under `--json`. */
|
|
25
|
+
export declare function serializeEnvelope(envelope: JsonEnvelope): string;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The uniform `--json` output envelope (docs/specs/cli.md §8) and the CliError type
|
|
2
|
+
// that carries a stable `error.code`. The envelope builders are the single choke point
|
|
3
|
+
// for machine output, so the redaction rule (§5.4 — the token is NEVER in the envelope)
|
|
4
|
+
// and the "absent optional fields are omitted, not null" rule are enforced in one place.
|
|
5
|
+
import { exitCodeForError } from "./exit-codes.js";
|
|
6
|
+
/** A command failure carrying a stable `error.code` (§8) → §7 exit code. */
|
|
7
|
+
export class CliError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "CliError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
/** The §7 exit code this error maps to. */
|
|
15
|
+
get exitCode() {
|
|
16
|
+
return exitCodeForError(this.code);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Drop `undefined` values so absent optional fields are omitted from `data`, not `null` (§8). */
|
|
20
|
+
export function pruneUndefined(data) {
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [key, value] of Object.entries(data)) {
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
out[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/** Build a success envelope. `data` is pruned of undefined optionals. */
|
|
30
|
+
export function buildOk(command, data) {
|
|
31
|
+
return { ok: true, command, data: pruneUndefined(data), error: null };
|
|
32
|
+
}
|
|
33
|
+
/** Build a failure envelope. `data` is always null; the token is never present (§5.4). */
|
|
34
|
+
export function buildErr(command, code, message) {
|
|
35
|
+
return { ok: false, command, data: null, error: { code, message } };
|
|
36
|
+
}
|
|
37
|
+
/** Serialize an envelope to the single line printed on stdout under `--json`. */
|
|
38
|
+
export function serializeEnvelope(envelope) {
|
|
39
|
+
return JSON.stringify(envelope);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,uFAAuF;AACvF,wFAAwF;AACxF,yFAAyF;AAEzF,OAAO,EAAE,gBAAgB,EAAiC,MAAM,iBAAiB,CAAC;AAElF,4EAA4E;AAC5E,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,IAAI,CAAY;IAEzB,YAAY,IAAe,EAAE,OAAe;QAC1C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,2CAA2C;IAC3C,IAAI,QAAQ;QACV,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;CACF;AASD,kGAAkG;AAClG,MAAM,UAAU,cAAc,CAAC,IAA6B;IAC1D,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,OAAO,CAAC,OAAe,EAAE,IAA6B;IACpE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACxE,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,IAAY,EAAE,OAAe;IACrE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;AACtE,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,QAAsB;IACtD,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const ExitCode: {
|
|
2
|
+
/** 0 — Success. */
|
|
3
|
+
readonly SUCCESS: 0;
|
|
4
|
+
/** 1 — Generic / unexpected error. */
|
|
5
|
+
readonly ERROR: 1;
|
|
6
|
+
/** 2 — Usage error (bad local flags/args, before any Hub call). */
|
|
7
|
+
readonly USAGE: 2;
|
|
8
|
+
/** 3 — Not authenticated / auth failed (Hub 401/403; no token). */
|
|
9
|
+
readonly AUTH: 3;
|
|
10
|
+
/** 4 — Not found (unknown id / resource; Hub 404). */
|
|
11
|
+
readonly NOT_FOUND: 4;
|
|
12
|
+
/** 5 — Network / offline (DNS, connection refused, unreachable). */
|
|
13
|
+
readonly NETWORK: 5;
|
|
14
|
+
/** 6 — Hub server error (5xx). */
|
|
15
|
+
readonly SERVER: 6;
|
|
16
|
+
/** 7 — Timeout (per-request `--timeout`, or `await` poll budget). */
|
|
17
|
+
readonly TIMEOUT: 7;
|
|
18
|
+
/** 8 — Conflict (Hub 409; duplicate idempotency key, different payload). */
|
|
19
|
+
readonly CONFLICT: 8;
|
|
20
|
+
/** 9 — Invalid request (Hub 400/422/413; version not supported). */
|
|
21
|
+
readonly INVALID_REQUEST: 9;
|
|
22
|
+
};
|
|
23
|
+
export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
|
|
24
|
+
/**
|
|
25
|
+
* Stable `error.code` strings (§8) → exit codes (§7). An exit code may cover more
|
|
26
|
+
* than one code string (e.g. the four request errors all map to 9). `not_implemented`
|
|
27
|
+
* is the scaffold's own code for commands that land in #106–#108 → generic exit 1.
|
|
28
|
+
*/
|
|
29
|
+
declare const ERROR_CODE_TO_EXIT: {
|
|
30
|
+
readonly error: 1;
|
|
31
|
+
readonly usage: 2;
|
|
32
|
+
readonly auth: 3;
|
|
33
|
+
readonly not_found: 4;
|
|
34
|
+
readonly network: 5;
|
|
35
|
+
readonly server: 6;
|
|
36
|
+
readonly timeout: 7;
|
|
37
|
+
readonly conflict: 8;
|
|
38
|
+
readonly bad_request: 9;
|
|
39
|
+
readonly validation_error: 9;
|
|
40
|
+
readonly payload_too_large: 9;
|
|
41
|
+
readonly version_not_supported: 9;
|
|
42
|
+
readonly not_implemented: 1;
|
|
43
|
+
};
|
|
44
|
+
export type ErrorCode = keyof typeof ERROR_CODE_TO_EXIT;
|
|
45
|
+
/** Map a (possibly unknown) `error.code` string to its exit code; unknown → generic 1. */
|
|
46
|
+
export declare function exitCodeForError(code: string): ExitCode;
|
|
47
|
+
/** True when `code` is one of the documented `error.code` strings (§8). Lets callers distinguish a
|
|
48
|
+
* recognized code from an arbitrary/undocumented one (which `exitCodeForError` flattens to 1), so a
|
|
49
|
+
* Hub-supplied `error.code` like `rate_limited` is never surfaced verbatim in the CLI envelope. */
|
|
50
|
+
export declare function isErrorCode(code: string): code is ErrorCode;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The stable exit-code table (docs/specs/cli.md §7) and the map from a `--json`
|
|
2
|
+
// envelope `error.code` string (§8) to its exit code. This is the machine contract
|
|
3
|
+
// every command branches on; keep it in lockstep with the spec.
|
|
4
|
+
export const ExitCode = {
|
|
5
|
+
/** 0 — Success. */
|
|
6
|
+
SUCCESS: 0,
|
|
7
|
+
/** 1 — Generic / unexpected error. */
|
|
8
|
+
ERROR: 1,
|
|
9
|
+
/** 2 — Usage error (bad local flags/args, before any Hub call). */
|
|
10
|
+
USAGE: 2,
|
|
11
|
+
/** 3 — Not authenticated / auth failed (Hub 401/403; no token). */
|
|
12
|
+
AUTH: 3,
|
|
13
|
+
/** 4 — Not found (unknown id / resource; Hub 404). */
|
|
14
|
+
NOT_FOUND: 4,
|
|
15
|
+
/** 5 — Network / offline (DNS, connection refused, unreachable). */
|
|
16
|
+
NETWORK: 5,
|
|
17
|
+
/** 6 — Hub server error (5xx). */
|
|
18
|
+
SERVER: 6,
|
|
19
|
+
/** 7 — Timeout (per-request `--timeout`, or `await` poll budget). */
|
|
20
|
+
TIMEOUT: 7,
|
|
21
|
+
/** 8 — Conflict (Hub 409; duplicate idempotency key, different payload). */
|
|
22
|
+
CONFLICT: 8,
|
|
23
|
+
/** 9 — Invalid request (Hub 400/422/413; version not supported). */
|
|
24
|
+
INVALID_REQUEST: 9,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Stable `error.code` strings (§8) → exit codes (§7). An exit code may cover more
|
|
28
|
+
* than one code string (e.g. the four request errors all map to 9). `not_implemented`
|
|
29
|
+
* is the scaffold's own code for commands that land in #106–#108 → generic exit 1.
|
|
30
|
+
*/
|
|
31
|
+
const ERROR_CODE_TO_EXIT = {
|
|
32
|
+
error: ExitCode.ERROR,
|
|
33
|
+
usage: ExitCode.USAGE,
|
|
34
|
+
auth: ExitCode.AUTH,
|
|
35
|
+
not_found: ExitCode.NOT_FOUND,
|
|
36
|
+
network: ExitCode.NETWORK,
|
|
37
|
+
server: ExitCode.SERVER,
|
|
38
|
+
timeout: ExitCode.TIMEOUT,
|
|
39
|
+
conflict: ExitCode.CONFLICT,
|
|
40
|
+
bad_request: ExitCode.INVALID_REQUEST,
|
|
41
|
+
validation_error: ExitCode.INVALID_REQUEST,
|
|
42
|
+
payload_too_large: ExitCode.INVALID_REQUEST,
|
|
43
|
+
version_not_supported: ExitCode.INVALID_REQUEST,
|
|
44
|
+
not_implemented: ExitCode.ERROR,
|
|
45
|
+
};
|
|
46
|
+
/** Map a (possibly unknown) `error.code` string to its exit code; unknown → generic 1. */
|
|
47
|
+
export function exitCodeForError(code) {
|
|
48
|
+
const table = ERROR_CODE_TO_EXIT;
|
|
49
|
+
return table[code] ?? ExitCode.ERROR;
|
|
50
|
+
}
|
|
51
|
+
/** True when `code` is one of the documented `error.code` strings (§8). Lets callers distinguish a
|
|
52
|
+
* recognized code from an arbitrary/undocumented one (which `exitCodeForError` flattens to 1), so a
|
|
53
|
+
* Hub-supplied `error.code` like `rate_limited` is never surfaced verbatim in the CLI envelope. */
|
|
54
|
+
export function isErrorCode(code) {
|
|
55
|
+
return Object.prototype.hasOwnProperty.call(ERROR_CODE_TO_EXIT, code);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=exit-codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,mFAAmF;AACnF,gEAAgE;AAEhE,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,mBAAmB;IACnB,OAAO,EAAE,CAAC;IACV,sCAAsC;IACtC,KAAK,EAAE,CAAC;IACR,mEAAmE;IACnE,KAAK,EAAE,CAAC;IACR,mEAAmE;IACnE,IAAI,EAAE,CAAC;IACP,sDAAsD;IACtD,SAAS,EAAE,CAAC;IACZ,oEAAoE;IACpE,OAAO,EAAE,CAAC;IACV,kCAAkC;IAClC,MAAM,EAAE,CAAC;IACT,qEAAqE;IACrE,OAAO,EAAE,CAAC;IACV,4EAA4E;IAC5E,QAAQ,EAAE,CAAC;IACX,oEAAoE;IACpE,eAAe,EAAE,CAAC;CACV,CAAC;AAIX;;;;GAIG;AACH,MAAM,kBAAkB,GAAG;IACzB,KAAK,EAAE,QAAQ,CAAC,KAAK;IACrB,KAAK,EAAE,QAAQ,CAAC,KAAK;IACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;IACnB,SAAS,EAAE,QAAQ,CAAC,SAAS;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO;IACzB,MAAM,EAAE,QAAQ,CAAC,MAAM;IACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;IACzB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;IAC3B,WAAW,EAAE,QAAQ,CAAC,eAAe;IACrC,gBAAgB,EAAE,QAAQ,CAAC,eAAe;IAC1C,iBAAiB,EAAE,QAAQ,CAAC,eAAe;IAC3C,qBAAqB,EAAE,QAAQ,CAAC,eAAe;IAC/C,eAAe,EAAE,QAAQ,CAAC,KAAK;CACvB,CAAC;AAIX,0FAA0F;AAC1F,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAA6B,kBAAkB,CAAC;IAC3D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;AACvC,CAAC;AAED;;oGAEoG;AACpG,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;AACxE,CAAC"}
|
package/dist/help.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TOP_LEVEL_HELP: string;
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Help text. The top-level copy mirrors docs/specs/cli.md §10; per-command help comes
|
|
2
|
+
// from the registry (read directly off the CommandSpec at dispatch).
|
|
3
|
+
export const TOP_LEVEL_HELP = [
|
|
4
|
+
"oh-hai — reach a human from your agent, through an MA2H Hub.",
|
|
5
|
+
"",
|
|
6
|
+
"Usage: oh-hai [global flags] <command> [subcommand] [flags]",
|
|
7
|
+
"",
|
|
8
|
+
"Auth: login | logout | whoami",
|
|
9
|
+
"Messaging: notify | ask | task",
|
|
10
|
+
"Inbox: inbox watch",
|
|
11
|
+
"Agents: agents list | create | revoke",
|
|
12
|
+
"Health: doctor",
|
|
13
|
+
"",
|
|
14
|
+
"Global flags: --base-url --account --json --quiet --verbose --no-color --timeout",
|
|
15
|
+
"Run 'oh-hai <command> --help' for details.",
|
|
16
|
+
].join("\n");
|
|
17
|
+
//# sourceMappingURL=help.js.map
|
package/dist/help.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help.js","sourceRoot":"","sources":["../src/help.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,qEAAqE;AAErE,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,8DAA8D;IAC9D,EAAE;IACF,6DAA6D;IAC7D,EAAE;IACF,qCAAqC;IACrC,iCAAiC;IACjC,yBAAyB;IACzB,2CAA2C;IAC3C,oBAAoB;IACpB,EAAE;IACF,kFAAkF;IAClF,4CAA4C;CAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Strip terminal control characters (C0/C1 + DEL) from a server-supplied string before echoing it.
|
|
2
|
+
* Built by code point to avoid a control-char regex literal. The raw value is kept for lookups and
|
|
3
|
+
* the `--json` envelope (JSON.stringify escapes control characters) — only terminal output is
|
|
4
|
+
* sanitized. */
|
|
5
|
+
export declare function sanitizeForTerminal(value: string): string;
|