offrouter-cli 0.2.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/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +59 -3
- package/dist/commands/login.js.map +1 -1
- package/package.json +13 -8
- package/src/catalog-cache.test.ts +0 -258
- package/src/catalog-cache.ts +0 -169
- package/src/catalog-sample.json +0 -30
- package/src/cli.test.ts +0 -1705
- package/src/commands/config-error.ts +0 -31
- package/src/commands/detection.ts +0 -209
- package/src/commands/doctor.ts +0 -555
- package/src/commands/hooks.ts +0 -125
- package/src/commands/init.ts +0 -59
- package/src/commands/install.ts +0 -438
- package/src/commands/login.test.ts +0 -247
- package/src/commands/login.ts +0 -413
- package/src/commands/mcp.ts +0 -35
- package/src/commands/models.ts +0 -168
- package/src/commands/output.ts +0 -11
- package/src/commands/profiles.ts +0 -58
- package/src/commands/proxy.ts +0 -87
- package/src/commands/route.ts +0 -190
- package/src/commands/status.ts +0 -139
- package/src/commands/update.ts +0 -62
- package/src/index.ts +0 -174
- package/tsconfig.json +0 -17
package/src/commands/models.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { loadConfig, BUILTIN_PROVIDERS } from "offrouter-core";
|
|
3
|
-
import { formatConfigError, type CliConfigError } from "./config-error.js";
|
|
4
|
-
import { writeJson, writeText } from "./output.js";
|
|
5
|
-
import type { CommandContext } from "./init.js";
|
|
6
|
-
import { readCatalogCache, resolveCacheDir } from "../catalog-cache.js";
|
|
7
|
-
import type { CachedModelFamily } from "../catalog-cache.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A single model entry in the merged output.
|
|
11
|
-
* source: "builtin" from core package, "cached" from remote catalog, "config" from profile config.
|
|
12
|
-
*/
|
|
13
|
-
export interface ModelEntry {
|
|
14
|
-
providerId: string;
|
|
15
|
-
modelId: string;
|
|
16
|
-
displayName?: string;
|
|
17
|
-
family?: string;
|
|
18
|
-
source: "builtin" | "cached" | "config";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function builtinToEntries(): ModelEntry[] {
|
|
22
|
-
const entries: ModelEntry[] = [];
|
|
23
|
-
for (const provider of BUILTIN_PROVIDERS) {
|
|
24
|
-
for (const model of provider.knownModels) {
|
|
25
|
-
entries.push({
|
|
26
|
-
providerId: provider.providerId,
|
|
27
|
-
modelId: model.modelId,
|
|
28
|
-
displayName: model.displayName,
|
|
29
|
-
family: model.family,
|
|
30
|
-
source: "builtin",
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return entries;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function cachedToEntries(cached: { families: CachedModelFamily[] }): ModelEntry[] {
|
|
38
|
-
return cached.families.map((f) => ({
|
|
39
|
-
providerId: f.providerId,
|
|
40
|
-
modelId: f.modelId,
|
|
41
|
-
displayName: f.displayName,
|
|
42
|
-
family: f.family,
|
|
43
|
-
source: "cached" as const,
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Merge builtin and cached model entries.
|
|
49
|
-
* Cached models with the same (providerId, modelId) override builtin entries.
|
|
50
|
-
* Config provider entries are appended separately.
|
|
51
|
-
*/
|
|
52
|
-
function mergeModels(
|
|
53
|
-
builtin: ModelEntry[],
|
|
54
|
-
cached: ModelEntry[],
|
|
55
|
-
config: ModelEntry[],
|
|
56
|
-
): ModelEntry[] {
|
|
57
|
-
// Build a set of (providerId,modelId) from cached entries that override builtin
|
|
58
|
-
const cachedKeys = new Set<string>();
|
|
59
|
-
for (const entry of cached) {
|
|
60
|
-
cachedKeys.add(`${entry.providerId}:${entry.modelId}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Filter builtin entries that are not overridden by cached
|
|
64
|
-
const merged = builtin.filter(
|
|
65
|
-
(b) => !cachedKeys.has(`${b.providerId}:${b.modelId}`),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
// Add all cached entries
|
|
69
|
-
merged.push(...cached);
|
|
70
|
-
|
|
71
|
-
// Add config entries (providers from config.toml)
|
|
72
|
-
// Config entries with same (providerId, modelId) override both builtin and cached
|
|
73
|
-
const mergedKeys = new Set<string>();
|
|
74
|
-
for (const entry of merged) {
|
|
75
|
-
mergedKeys.add(`${entry.providerId}:${entry.modelId}`);
|
|
76
|
-
}
|
|
77
|
-
for (const entry of config) {
|
|
78
|
-
if (!mergedKeys.has(`${entry.providerId}:${entry.modelId}`)) {
|
|
79
|
-
merged.push(entry);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return merged;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function registerModelsCommand(
|
|
87
|
-
program: Command,
|
|
88
|
-
ctx: () => CommandContext,
|
|
89
|
-
): void {
|
|
90
|
-
program
|
|
91
|
-
.command("models")
|
|
92
|
-
.description(
|
|
93
|
-
"List known models from builtin catalog, cached remote catalog, and config",
|
|
94
|
-
)
|
|
95
|
-
.option("--json", "Emit JSON", false)
|
|
96
|
-
.action(async (opts: { json?: boolean }) => {
|
|
97
|
-
const { env, cwd, stdout } = ctx();
|
|
98
|
-
|
|
99
|
-
// 1. Builtin models from offrouter-core
|
|
100
|
-
const builtinEntries = builtinToEntries();
|
|
101
|
-
|
|
102
|
-
// 2. Cached models from remote catalog (if available)
|
|
103
|
-
let cachedEntries: ModelEntry[] = [];
|
|
104
|
-
const cacheDir = resolveCacheDir(env);
|
|
105
|
-
const cachedData = readCatalogCache(cacheDir);
|
|
106
|
-
if (cachedData) {
|
|
107
|
-
cachedEntries = cachedToEntries(cachedData);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 3. Config providers
|
|
111
|
-
let configEntries: ModelEntry[] = [];
|
|
112
|
-
let configError: CliConfigError | null = null;
|
|
113
|
-
try {
|
|
114
|
-
const config = await loadConfig({ env, workspaceDir: cwd });
|
|
115
|
-
configEntries = Object.keys(config.providers)
|
|
116
|
-
.sort()
|
|
117
|
-
.map((providerId) => ({
|
|
118
|
-
providerId,
|
|
119
|
-
modelId: "*",
|
|
120
|
-
source: "config" as const,
|
|
121
|
-
}));
|
|
122
|
-
} catch (err) {
|
|
123
|
-
configError = formatConfigError(err);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Merge: cached overrides builtin (same providerId+modelId), config appended
|
|
127
|
-
const models = mergeModels(builtinEntries, cachedEntries, configEntries);
|
|
128
|
-
|
|
129
|
-
if (opts.json) {
|
|
130
|
-
writeJson(stdout, {
|
|
131
|
-
models,
|
|
132
|
-
builtinCount: builtinEntries.length,
|
|
133
|
-
cachedCount: cachedEntries.length,
|
|
134
|
-
configCount: configEntries.length,
|
|
135
|
-
configError,
|
|
136
|
-
});
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (configError) {
|
|
140
|
-
writeText(stdout, `Config error: ${configError.message}`);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (models.length === 0) {
|
|
144
|
-
writeText(
|
|
145
|
-
stdout,
|
|
146
|
-
"No models available.",
|
|
147
|
-
);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
// Group by source for display
|
|
151
|
-
const builtinModels = models.filter((m) => m.source === "builtin");
|
|
152
|
-
const cachedModels = models.filter((m) => m.source === "cached");
|
|
153
|
-
const configProviderIds = models.filter((m) => m.source === "config");
|
|
154
|
-
|
|
155
|
-
writeText(stdout, `Models (${models.length} total):`);
|
|
156
|
-
writeText(stdout, ` Built-in: ${builtinModels.length} models`);
|
|
157
|
-
writeText(stdout, ` Cached: ${cachedModels.length} models`);
|
|
158
|
-
writeText(stdout, ` Config providers: ${configProviderIds.length}`);
|
|
159
|
-
writeText(stdout, "");
|
|
160
|
-
for (const m of models) {
|
|
161
|
-
const name = m.displayName ? `${m.displayName} (${m.modelId})` : m.modelId;
|
|
162
|
-
writeText(
|
|
163
|
-
stdout,
|
|
164
|
-
` ${m.providerId}/${name} (${m.source})`,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
}
|
package/src/commands/output.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface IoStreams {
|
|
2
|
-
write(chunk: string): boolean | void;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function writeJson(stream: IoStreams, value: unknown): void {
|
|
6
|
-
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function writeText(stream: IoStreams, text: string): void {
|
|
10
|
-
stream.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
11
|
-
}
|
package/src/commands/profiles.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { loadConfig } from "offrouter-core";
|
|
3
|
-
import { writeJson, writeText } from "./output.js";
|
|
4
|
-
import type { CommandContext } from "./init.js";
|
|
5
|
-
|
|
6
|
-
export function registerProfilesCommand(
|
|
7
|
-
program: Command,
|
|
8
|
-
ctx: () => CommandContext,
|
|
9
|
-
): void {
|
|
10
|
-
program
|
|
11
|
-
.command("profiles")
|
|
12
|
-
.description("List OffRouter profiles and allowlist status")
|
|
13
|
-
.option("--json", "Emit JSON", false)
|
|
14
|
-
.action(async (opts: { json?: boolean }) => {
|
|
15
|
-
const { env, cwd, stdout } = ctx();
|
|
16
|
-
try {
|
|
17
|
-
const config = await loadConfig({ env, workspaceDir: cwd });
|
|
18
|
-
const profiles = Object.values(config.profiles)
|
|
19
|
-
.map((p) => ({
|
|
20
|
-
id: p.id,
|
|
21
|
-
allowlisted: config.policy.allowlistedProfiles.includes(p.id),
|
|
22
|
-
deniedProviders: p.deniedProviders ?? [],
|
|
23
|
-
}))
|
|
24
|
-
.sort((a, b) => a.id.localeCompare(b.id));
|
|
25
|
-
|
|
26
|
-
const payload = {
|
|
27
|
-
allowlistedProfiles: [...config.policy.allowlistedProfiles].sort(),
|
|
28
|
-
deniedProfilePatterns: [
|
|
29
|
-
...(config.policy.deniedProfilePatterns ?? []),
|
|
30
|
-
].sort(),
|
|
31
|
-
profiles,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (opts.json) {
|
|
35
|
-
writeJson(stdout, payload);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
writeText(
|
|
39
|
-
stdout,
|
|
40
|
-
`Allowlisted: ${payload.allowlistedProfiles.join(", ") || "(none)"}`,
|
|
41
|
-
);
|
|
42
|
-
writeText(
|
|
43
|
-
stdout,
|
|
44
|
-
`Denied patterns: ${payload.deniedProfilePatterns.join(", ") || "(none)"}`,
|
|
45
|
-
);
|
|
46
|
-
for (const p of profiles) {
|
|
47
|
-
writeText(stdout, `profile ${p.id}: allowlisted=${p.allowlisted}`);
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
-
if (opts.json) {
|
|
52
|
-
writeJson(stdout, { error: message, profiles: [] });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
writeText(stdout, `Failed to load profiles: ${message}`);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
package/src/commands/proxy.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
createResponsesProxyContext,
|
|
4
|
-
loadConfig,
|
|
5
|
-
startResponsesProxy,
|
|
6
|
-
type HarnessRef,
|
|
7
|
-
} from "offrouter-core";
|
|
8
|
-
import type { CommandContext } from "./init.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Minimal CLI bridge for the local OpenAI Responses-compatible proxy.
|
|
12
|
-
* Provider discovery (candidates/adapters) lands in a later phase; this starts
|
|
13
|
-
* the transparent gateway with the configured policy so custom-base-URL apps
|
|
14
|
-
* can point at OffRouter today.
|
|
15
|
-
*/
|
|
16
|
-
export function registerProxyCommand(
|
|
17
|
-
program: Command,
|
|
18
|
-
ctx: () => CommandContext,
|
|
19
|
-
): void {
|
|
20
|
-
const proxy = program
|
|
21
|
-
.command("proxy")
|
|
22
|
-
.description("OffRouter transparent-gateway proxy bridges");
|
|
23
|
-
|
|
24
|
-
proxy
|
|
25
|
-
.command("serve")
|
|
26
|
-
.description(
|
|
27
|
-
"Start the local OpenAI Responses-compatible proxy (transparent gateway)",
|
|
28
|
-
)
|
|
29
|
-
.option(
|
|
30
|
-
"-p, --port <port>",
|
|
31
|
-
"TCP port (default 8787, env OFFROUTER_PROXY_PORT)",
|
|
32
|
-
(value: string) => Number.parseInt(value, 10),
|
|
33
|
-
)
|
|
34
|
-
.option("-H, --host <host>", "Bind host (default 127.0.0.1)")
|
|
35
|
-
.option(
|
|
36
|
-
"--profile <profile>",
|
|
37
|
-
"Harness profile to route as (must be allowlisted; default proxy)",
|
|
38
|
-
)
|
|
39
|
-
.action(
|
|
40
|
-
async (opts: {
|
|
41
|
-
port?: number;
|
|
42
|
-
host?: string;
|
|
43
|
-
profile?: string;
|
|
44
|
-
}) => {
|
|
45
|
-
const { env, cwd, stderr } = ctx();
|
|
46
|
-
const config = await loadConfig({
|
|
47
|
-
env,
|
|
48
|
-
workspaceDir: cwd,
|
|
49
|
-
workspaceTrusted: false,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const profile = opts.profile ?? "proxy";
|
|
53
|
-
const harness: HarnessRef = { kind: "other", profile };
|
|
54
|
-
const context = createResponsesProxyContext({
|
|
55
|
-
policy: config.policy,
|
|
56
|
-
// Provider discovery supplies candidates/adapters in a later phase.
|
|
57
|
-
candidates: [],
|
|
58
|
-
adapters: {},
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const port =
|
|
62
|
-
opts.port ??
|
|
63
|
-
Number.parseInt(env.OFFROUTER_PROXY_PORT ?? "8787", 10);
|
|
64
|
-
const host = opts.host ?? env.OFFROUTER_PROXY_HOST ?? "127.0.0.1";
|
|
65
|
-
const token = env.OFFROUTER_PROXY_TOKEN;
|
|
66
|
-
|
|
67
|
-
const handle = await startResponsesProxy(context, {
|
|
68
|
-
port,
|
|
69
|
-
host,
|
|
70
|
-
token,
|
|
71
|
-
harness,
|
|
72
|
-
workspace: { cwd, trusted: true },
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
stderr.write(
|
|
76
|
-
`OffRouter proxy listening on http://${host}:${handle.port} (profile=${profile})\n`,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const shutdown = async (): Promise<void> => {
|
|
80
|
-
await handle.close();
|
|
81
|
-
process.exit(0);
|
|
82
|
-
};
|
|
83
|
-
process.on("SIGINT", () => void shutdown());
|
|
84
|
-
process.on("SIGTERM", () => void shutdown());
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
}
|
package/src/commands/route.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { Command } from "commander";
|
|
4
|
-
import {
|
|
5
|
-
FileUsageStore,
|
|
6
|
-
loadConfig,
|
|
7
|
-
resolveOffRouterHome,
|
|
8
|
-
routeRequest,
|
|
9
|
-
type HarnessKind,
|
|
10
|
-
type RouteRequest,
|
|
11
|
-
} from "offrouter-core";
|
|
12
|
-
import { formatConfigError } from "./config-error.js";
|
|
13
|
-
import { writeJson, writeText } from "./output.js";
|
|
14
|
-
import type { CommandContext } from "./init.js";
|
|
15
|
-
|
|
16
|
-
function harnessKindFromProfile(profile: string): HarnessKind {
|
|
17
|
-
if (profile.startsWith("claude")) return "claude";
|
|
18
|
-
if (profile.startsWith("codex")) return "codex";
|
|
19
|
-
if (profile.startsWith("gemini")) return "gemini";
|
|
20
|
-
if (profile.startsWith("grok")) return "grok";
|
|
21
|
-
if (profile.startsWith("cursor")) return "cursor";
|
|
22
|
-
if (profile.startsWith("pi")) return "pi";
|
|
23
|
-
return "other";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildRequest(
|
|
27
|
-
prompt: string,
|
|
28
|
-
profile: string,
|
|
29
|
-
cwd: string,
|
|
30
|
-
): RouteRequest {
|
|
31
|
-
const preview = prompt.length > 200 ? `${prompt.slice(0, 200)}...` : prompt;
|
|
32
|
-
const digest = createHash("sha256").update(prompt).digest("hex");
|
|
33
|
-
return {
|
|
34
|
-
protocolVersion: "offrouter.route.v1",
|
|
35
|
-
requestId: `cli_route_${digest.slice(0, 12)}`,
|
|
36
|
-
harness: {
|
|
37
|
-
kind: harnessKindFromProfile(profile),
|
|
38
|
-
profile,
|
|
39
|
-
},
|
|
40
|
-
task: {
|
|
41
|
-
promptPreview: preview,
|
|
42
|
-
promptDigest: digest,
|
|
43
|
-
kind: "explain",
|
|
44
|
-
risk: "low",
|
|
45
|
-
},
|
|
46
|
-
workspace: {
|
|
47
|
-
cwd,
|
|
48
|
-
// CLI explain is an explicit local dry run and does not load project config.
|
|
49
|
-
// Harness adapters will pass real workspace trust when they route live work.
|
|
50
|
-
trusted: true,
|
|
51
|
-
},
|
|
52
|
-
constraints: {
|
|
53
|
-
subscriptionFirst: true,
|
|
54
|
-
allowApiKeyFallback: false,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function registerRouteCommand(
|
|
60
|
-
program: Command,
|
|
61
|
-
ctx: () => CommandContext,
|
|
62
|
-
): void {
|
|
63
|
-
const route = program
|
|
64
|
-
.command("route")
|
|
65
|
-
.description("Route or explain routing decisions (no live provider calls)");
|
|
66
|
-
|
|
67
|
-
route
|
|
68
|
-
.command("explain")
|
|
69
|
-
.description("Explain how OffRouter would route a prompt under policy")
|
|
70
|
-
.argument(
|
|
71
|
-
"<prompt>",
|
|
72
|
-
"Prompt text used only for preview + digest (not sent to providers)",
|
|
73
|
-
)
|
|
74
|
-
.requiredOption(
|
|
75
|
-
"--profile <profile>",
|
|
76
|
-
"Harness profile id (e.g. claude-personal)",
|
|
77
|
-
)
|
|
78
|
-
.option("--json", "Emit JSON decision", false)
|
|
79
|
-
.action(
|
|
80
|
-
async (prompt: string, opts: { profile: string; json?: boolean }) => {
|
|
81
|
-
const { env, cwd, stdout } = ctx();
|
|
82
|
-
const request = buildRequest(prompt, opts.profile, cwd);
|
|
83
|
-
|
|
84
|
-
let policyConfig;
|
|
85
|
-
let config;
|
|
86
|
-
try {
|
|
87
|
-
config = await loadConfig({ env, workspaceDir: cwd });
|
|
88
|
-
policyConfig = config.policy;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
const configError = formatConfigError(err);
|
|
91
|
-
const decision = {
|
|
92
|
-
protocolVersion: request.protocolVersion,
|
|
93
|
-
decisionId: `dec_${request.requestId}_config_error`,
|
|
94
|
-
requestId: request.requestId,
|
|
95
|
-
route: null,
|
|
96
|
-
reason: "config_error",
|
|
97
|
-
explanation: `Invalid OffRouter config: ${configError.message}`,
|
|
98
|
-
fallbackChain: [],
|
|
99
|
-
policyDenials: [],
|
|
100
|
-
confidence: 1,
|
|
101
|
-
auditSummary: `Blocked config_error profile=${request.harness.profile} digest=${request.task.promptDigest}`,
|
|
102
|
-
blocked: true,
|
|
103
|
-
needsConfiguration: false,
|
|
104
|
-
setupIncomplete: false,
|
|
105
|
-
setupMessage: null,
|
|
106
|
-
configError,
|
|
107
|
-
};
|
|
108
|
-
if (opts.json) {
|
|
109
|
-
writeJson(stdout, decision);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
writeText(
|
|
113
|
-
stdout,
|
|
114
|
-
`Cannot route (config_error): ${decision.explanation}`,
|
|
115
|
-
);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// No provider catalog in this milestone - empty candidates yield needsConfiguration
|
|
120
|
-
// or a profile hard block for work/denied profiles.
|
|
121
|
-
const decision = routeRequest(request, [], policyConfig);
|
|
122
|
-
const setupIncomplete = decision.needsConfiguration === true;
|
|
123
|
-
|
|
124
|
-
// Check near-limit status for configured accounts.
|
|
125
|
-
let nearLimitWarning: string | null = null;
|
|
126
|
-
try {
|
|
127
|
-
const usageStore = new FileUsageStore({
|
|
128
|
-
filePath: join(
|
|
129
|
-
resolveOffRouterHome({ env }),
|
|
130
|
-
"state",
|
|
131
|
-
"usage.json",
|
|
132
|
-
),
|
|
133
|
-
});
|
|
134
|
-
const nearAccounts = await usageStore.getNearLimitAccounts(
|
|
135
|
-
policyConfig.nearLimitThreshold,
|
|
136
|
-
);
|
|
137
|
-
if (nearAccounts.length > 0) {
|
|
138
|
-
nearLimitWarning = `Account ${nearAccounts[0]!.providerId}/${nearAccounts[0]!.accountId} is near its limit (${
|
|
139
|
-
nearAccounts[0]!.percentRemaining ?? 0
|
|
140
|
-
}% remaining)`;
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
// Non-fatal: if the usage store cannot be read, skip the warning.
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (opts.json) {
|
|
147
|
-
writeJson(stdout, {
|
|
148
|
-
...decision,
|
|
149
|
-
setupIncomplete,
|
|
150
|
-
setupMessage: setupIncomplete
|
|
151
|
-
? "Setup incomplete: configure a subscription, local runtime, or API-key provider, and allowlist a personal profile."
|
|
152
|
-
: null,
|
|
153
|
-
...(nearLimitWarning !== null ? { nearLimitWarning } : {}),
|
|
154
|
-
});
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
decision.blocked ||
|
|
160
|
-
decision.needsConfiguration ||
|
|
161
|
-
!decision.route
|
|
162
|
-
) {
|
|
163
|
-
writeText(
|
|
164
|
-
stdout,
|
|
165
|
-
`Cannot route (${decision.reason}): ${decision.explanation}`,
|
|
166
|
-
);
|
|
167
|
-
if (decision.policyDenials && decision.policyDenials.length > 0) {
|
|
168
|
-
for (const d of decision.policyDenials) {
|
|
169
|
-
writeText(stdout, `- [${d.code}] ${d.message}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (setupIncomplete) {
|
|
173
|
-
writeText(
|
|
174
|
-
stdout,
|
|
175
|
-
"Setup incomplete: configure a subscription, local runtime, or API-key provider, and allowlist a personal profile.",
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
if (nearLimitWarning !== null) {
|
|
179
|
-
writeText(stdout, `! ${nearLimitWarning}`);
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
writeText(
|
|
185
|
-
stdout,
|
|
186
|
-
`Route: ${decision.route.provider}/${decision.route.model} (${decision.route.authTier}) - ${decision.explanation}`,
|
|
187
|
-
);
|
|
188
|
-
},
|
|
189
|
-
);
|
|
190
|
-
}
|
package/src/commands/status.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
FileUsageStore,
|
|
5
|
-
loadConfig,
|
|
6
|
-
resolveOffRouterHome,
|
|
7
|
-
} from "offrouter-core";
|
|
8
|
-
import { formatConfigError, type CliConfigError } from "./config-error.js";
|
|
9
|
-
import { detectEnvironment, V1_HONESTY_LINE } from "./detection.js";
|
|
10
|
-
import { writeJson, writeText } from "./output.js";
|
|
11
|
-
import type { CommandContext } from "./init.js";
|
|
12
|
-
|
|
13
|
-
export function registerStatusCommand(
|
|
14
|
-
program: Command,
|
|
15
|
-
ctx: () => CommandContext,
|
|
16
|
-
): void {
|
|
17
|
-
program
|
|
18
|
-
.command("status")
|
|
19
|
-
.description("Show OffRouter status")
|
|
20
|
-
.option("--providers", "Include per-provider readiness detail", false)
|
|
21
|
-
.option("--json", "Emit JSON", false)
|
|
22
|
-
.action(async (opts: { providers?: boolean; json?: boolean }) => {
|
|
23
|
-
const { env, cwd, stdout } = ctx();
|
|
24
|
-
const detection = await detectEnvironment({ env, cwd });
|
|
25
|
-
|
|
26
|
-
let providers: Array<{
|
|
27
|
-
id: string;
|
|
28
|
-
enabled: boolean;
|
|
29
|
-
authTierHints: string[];
|
|
30
|
-
subscriptionStatus: string;
|
|
31
|
-
apiKeyReady: boolean;
|
|
32
|
-
localReady: boolean;
|
|
33
|
-
limitStatus: string;
|
|
34
|
-
lastError: string | null;
|
|
35
|
-
accounts: { accountId: string; label: string; quotaRemaining: number | null; nearLimit: boolean | null }[];
|
|
36
|
-
}> = [];
|
|
37
|
-
let configError: CliConfigError | null = null;
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const config = await loadConfig({ env, workspaceDir: cwd });
|
|
41
|
-
const usageStore = new FileUsageStore({
|
|
42
|
-
filePath: join(resolveOffRouterHome({ env }), "state", "usage.json"),
|
|
43
|
-
});
|
|
44
|
-
const allUsage = await usageStore.listAccountUsage();
|
|
45
|
-
const usageByKey = new Map(
|
|
46
|
-
allUsage.map((u) => [`${u.providerId}:${u.accountId}`, u]),
|
|
47
|
-
);
|
|
48
|
-
providers = Object.values(config.providers)
|
|
49
|
-
.map((p) => ({
|
|
50
|
-
id: p.id,
|
|
51
|
-
enabled: p.enabled,
|
|
52
|
-
authTierHints: [
|
|
53
|
-
...(detection.authTiers.subscription
|
|
54
|
-
? (["subscription"] as const)
|
|
55
|
-
: []),
|
|
56
|
-
...(detection.authTiers.local ? (["local"] as const) : []),
|
|
57
|
-
...(detection.authTiers["api-key"]
|
|
58
|
-
? (["api-key"] as const)
|
|
59
|
-
: []),
|
|
60
|
-
],
|
|
61
|
-
subscriptionStatus: detection.subscription.status,
|
|
62
|
-
apiKeyReady: detection.apiKeyReadiness.anyReady,
|
|
63
|
-
localReady: detection.localRuntime.health !== "unconfigured",
|
|
64
|
-
limitStatus: detection.limits.status,
|
|
65
|
-
lastError: detection.lastProviderError,
|
|
66
|
-
accounts: p.accounts.map((a) => {
|
|
67
|
-
const snap = usageByKey.get(`${p.id}:${a.id}`);
|
|
68
|
-
return {
|
|
69
|
-
accountId: a.id,
|
|
70
|
-
label: a.label ?? a.id,
|
|
71
|
-
quotaRemaining: snap?.remaining ?? null,
|
|
72
|
-
nearLimit: snap?.nearLimit ?? null,
|
|
73
|
-
};
|
|
74
|
-
}),
|
|
75
|
-
}))
|
|
76
|
-
.sort((a, b) => a.id.localeCompare(b.id));
|
|
77
|
-
} catch (err) {
|
|
78
|
-
configError = formatConfigError(err);
|
|
79
|
-
providers = [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// When no providers are configured, still expose a readiness row for status --providers.
|
|
83
|
-
if (opts.providers && providers.length === 0) {
|
|
84
|
-
providers = [
|
|
85
|
-
{
|
|
86
|
-
id: "(none-configured)",
|
|
87
|
-
enabled: false,
|
|
88
|
-
authTierHints: Object.entries(detection.authTiers)
|
|
89
|
-
.filter(([, v]) => v)
|
|
90
|
-
.map(([k]) => k),
|
|
91
|
-
subscriptionStatus: detection.subscription.status,
|
|
92
|
-
apiKeyReady: detection.apiKeyReadiness.anyReady,
|
|
93
|
-
localReady: detection.localRuntime.health !== "unconfigured",
|
|
94
|
-
limitStatus: detection.limits.status,
|
|
95
|
-
lastError: detection.lastProviderError,
|
|
96
|
-
accounts: [],
|
|
97
|
-
},
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const payload = {
|
|
102
|
-
honesty: V1_HONESTY_LINE,
|
|
103
|
-
harnessProfiles: detection.harnessProfiles,
|
|
104
|
-
authTiers: detection.authTiers,
|
|
105
|
-
subscription: detection.subscription,
|
|
106
|
-
apiKeyReadiness: detection.apiKeyReadiness,
|
|
107
|
-
localRuntime: detection.localRuntime,
|
|
108
|
-
limits: detection.limits,
|
|
109
|
-
lastProviderError: detection.lastProviderError,
|
|
110
|
-
configError,
|
|
111
|
-
...(opts.providers ? { providers } : {}),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (opts.json) {
|
|
115
|
-
writeJson(stdout, payload);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
writeText(stdout, V1_HONESTY_LINE);
|
|
120
|
-
writeText(
|
|
121
|
-
stdout,
|
|
122
|
-
`Subscription: ${detection.subscription.status}; API-key ready: ${detection.apiKeyReadiness.anyReady}; local: ${detection.localRuntime.health}`,
|
|
123
|
-
);
|
|
124
|
-
if (opts.providers) {
|
|
125
|
-
for (const p of providers) {
|
|
126
|
-
writeText(
|
|
127
|
-
stdout,
|
|
128
|
-
`provider ${p.id}: enabled=${p.enabled} subscription=${p.subscriptionStatus} apiKeyReady=${p.apiKeyReady} localReady=${p.localReady} limits=${p.limitStatus}`,
|
|
129
|
-
);
|
|
130
|
-
for (const a of p.accounts) {
|
|
131
|
-
writeText(stdout, ` account ${a.accountId}: ${a.label}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (configError) {
|
|
136
|
-
writeText(stdout, `Config error: ${configError.message}`);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|