copilot-relay 0.2.3 → 0.2.5
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 +38 -4
- package/config.default.yaml +4 -4
- package/dist/main.js +226 -38
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,7 +69,6 @@ the top-level `model` field in `~/.claude/settings.json`. Exact
|
|
|
69
69
|
Config lives at `~/.copilot-relay/config.yaml` and is hot-reloaded:
|
|
70
70
|
|
|
71
71
|
```yaml
|
|
72
|
-
configVersion: 2
|
|
73
72
|
host: 127.0.0.1
|
|
74
73
|
port: 4142
|
|
75
74
|
copilotBaseUrl: https://api.githubcopilot.com
|
|
@@ -81,9 +80,10 @@ upstreamTimeoutSeconds: 180
|
|
|
81
80
|
webSearchBackend:
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
copilot-relay writes the resolved config back to this file, so every key is
|
|
84
|
+
present after the first start. Shipped defaults therefore apply to fresh
|
|
85
|
+
installs only — once a value is in your `config.yaml` it is never rewritten by
|
|
86
|
+
an upgrade. To pick up a changed default, edit the key yourself.
|
|
87
87
|
|
|
88
88
|
`logLevel` controls verbosity:
|
|
89
89
|
|
|
@@ -112,9 +112,32 @@ The same folder stores `copilot_token.json` for the cached Copilot bearer token,
|
|
|
112
112
|
copilot-relay auth
|
|
113
113
|
copilot-relay start
|
|
114
114
|
copilot-relay restart
|
|
115
|
+
copilot-relay status
|
|
115
116
|
copilot-relay stop
|
|
116
117
|
```
|
|
117
118
|
|
|
119
|
+
`status` reports whether a relay is running, where it is listening, and whether
|
|
120
|
+
it is reachable:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
copilot-relay 0.2.5
|
|
124
|
+
process running (pid 93744, up 1h 16m)
|
|
125
|
+
listening http://127.0.0.1:4142
|
|
126
|
+
health ok (9ms)
|
|
127
|
+
models gpt-5.6-sol[1m], claude-opus-5
|
|
128
|
+
upstream not checked (use --deep)
|
|
129
|
+
log ~/.copilot-relay/logs/copilot-relay.2026-07-25.log
|
|
130
|
+
config ~/.copilot-relay/config.yaml (logLevel=info, thinkEffort=max)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`--deep` additionally sends a real request through Copilot. That is the only
|
|
134
|
+
check that proves the relay can actually serve Claude Code — a relay whose
|
|
135
|
+
Copilot token expired an hour ago still answers `/healthz` and `/v1/models`,
|
|
136
|
+
because neither contacts upstream. It is opt-in because it spends a few tokens.
|
|
137
|
+
|
|
138
|
+
`--json` emits machine-readable output. Exit codes: `0` running, `1` not
|
|
139
|
+
running, `2` running but the deep check failed.
|
|
140
|
+
|
|
118
141
|
## Logging
|
|
119
142
|
|
|
120
143
|
At `debug`, every model request logs the requested model, upstream model, requested think effort, requested thinking, and effective think effort.
|
|
@@ -139,12 +162,23 @@ grep -n "Failed to create\\|Startup preflight failed" ~/.copilot-relay/logs/copi
|
|
|
139
162
|
|
|
140
163
|
See [`docs/troubleshooting.md`](docs/troubleshooting.md) for common debugging workflows.
|
|
141
164
|
|
|
165
|
+
## Running it as a service
|
|
166
|
+
|
|
167
|
+
To keep the relay running across reboots, see [`wiki/`](wiki/) — per-platform
|
|
168
|
+
setup for macOS, Windows, and Linux, in English and 中文, each covering
|
|
169
|
+
registration, how to verify it is *actually* working rather than merely
|
|
170
|
+
listening, and how to stop it.
|
|
171
|
+
|
|
172
|
+
The same pages are published to the [wiki tab](https://github.com/D0n9X1n/copilot-relay/wiki),
|
|
173
|
+
which is generated from `wiki/` on every merge. Edit the folder, not the tab.
|
|
174
|
+
|
|
142
175
|
## Development
|
|
143
176
|
|
|
144
177
|
Developer notes live in [`docs/development.md`](docs/development.md).
|
|
145
178
|
Architecture notes live in [`docs/architecture.md`](docs/architecture.md).
|
|
146
179
|
Logging notes live in [`docs/logging.md`](docs/logging.md).
|
|
147
180
|
Troubleshooting notes live in [`docs/troubleshooting.md`](docs/troubleshooting.md).
|
|
181
|
+
User-facing guides live in [`wiki/`](wiki/).
|
|
148
182
|
|
|
149
183
|
```sh
|
|
150
184
|
npm install
|
package/config.default.yaml
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
# Edit the user config file, not this template, for local changes.
|
|
5
5
|
# Most values hot-reload while the service is running. host and port still
|
|
6
6
|
# require restart because the listening socket is already bound.
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
#
|
|
8
|
+
# Values here apply to a fresh install only. copilot-relay writes the resolved
|
|
9
|
+
# config back to disk, so once a key exists in your config.yaml it is yours:
|
|
10
|
+
# changing a shipped default never rewrites it.
|
|
11
11
|
|
|
12
12
|
# Local host for the Claude Code-compatible HTTP server.
|
|
13
13
|
host: 127.0.0.1
|
package/dist/main.js
CHANGED
|
@@ -436,24 +436,15 @@ const logLevels = [
|
|
|
436
436
|
"debug"
|
|
437
437
|
];
|
|
438
438
|
/**
|
|
439
|
-
*
|
|
439
|
+
* Values used when a key is absent from the user's config.
|
|
440
440
|
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
441
|
+
* readAppConfig() writes the resolved config back to disk, so an existing
|
|
442
|
+
* install has every key materialized and never consults these again. Changing
|
|
443
|
+
* one therefore affects fresh installs only, by design: copilot-relay does not
|
|
444
|
+
* rewrite a value the user's config already holds.
|
|
443
445
|
*/
|
|
444
|
-
const currentConfigVersion = 2;
|
|
445
|
-
/**
|
|
446
|
-
* Opus defaults shipped by earlier releases.
|
|
447
|
-
*
|
|
448
|
-
* readAppConfig() writes the resolved config back to disk, so every install
|
|
449
|
-
* that ever ran an older build has the superseded default sitting in its
|
|
450
|
-
* config.yaml. Without this set, bumping defaultConfig.opusModel would only
|
|
451
|
-
* ever reach fresh installs.
|
|
452
|
-
*/
|
|
453
|
-
const supersededOpusModels = new Set(["claude-opus-4.8"]);
|
|
454
446
|
const defaultConfig = {
|
|
455
447
|
claudeSetup: true,
|
|
456
|
-
configVersion: currentConfigVersion,
|
|
457
448
|
copilotBaseUrl: "https://api.githubcopilot.com",
|
|
458
449
|
gptModel: "gpt-5.6-sol",
|
|
459
450
|
host: "127.0.0.1",
|
|
@@ -543,10 +534,6 @@ const parseConfigYaml = (content) => {
|
|
|
543
534
|
case "claude_setup":
|
|
544
535
|
config.claudeSetup = unquoteYamlScalar(value);
|
|
545
536
|
break;
|
|
546
|
-
case "configVersion":
|
|
547
|
-
case "config_version":
|
|
548
|
-
config.configVersion = unquoteYamlScalar(value);
|
|
549
|
-
break;
|
|
550
537
|
case "copilotBaseUrl":
|
|
551
538
|
case "copilot_base_url":
|
|
552
539
|
config.copilotBaseUrl = unquoteYamlScalar(value);
|
|
@@ -601,9 +588,6 @@ const serializeConfig = (config) => [
|
|
|
601
588
|
"#",
|
|
602
589
|
"# This file is hot-reloaded while copilot-relay is running.",
|
|
603
590
|
"",
|
|
604
|
-
"# Config schema version. Managed by copilot-relay; do not edit.",
|
|
605
|
-
`configVersion: ${config.configVersion}`,
|
|
606
|
-
"",
|
|
607
591
|
"# Local host for the Claude Code-compatible HTTP server.",
|
|
608
592
|
`host: ${config.host}`,
|
|
609
593
|
"",
|
|
@@ -645,17 +629,6 @@ const serializeConfig = (config) => [
|
|
|
645
629
|
`opusModel: ${config.opusModel}`,
|
|
646
630
|
""
|
|
647
631
|
].join("\n");
|
|
648
|
-
/**
|
|
649
|
-
* Resolve opusModel, applying the one-time superseded-default migration.
|
|
650
|
-
*
|
|
651
|
-
* Gated on configVersion so it runs exactly once. Without that gate the rewrite
|
|
652
|
-
* would re-run on every start, which would make a superseded model impossible
|
|
653
|
-
* to pin deliberately - the user's edit would be reverted underneath them.
|
|
654
|
-
*/
|
|
655
|
-
const resolveOpusModel = (rawOpusModel, configVersion) => {
|
|
656
|
-
if (rawOpusModel === void 0) return defaultConfig.opusModel;
|
|
657
|
-
return configVersion < currentConfigVersion && supersededOpusModels.has(rawOpusModel) ? defaultConfig.opusModel : rawOpusModel;
|
|
658
|
-
};
|
|
659
632
|
async function readAppConfig() {
|
|
660
633
|
const raw = await readRawConfig();
|
|
661
634
|
const claudeSetup = normalizeBoolean(raw.claudeSetup);
|
|
@@ -665,16 +638,14 @@ async function readAppConfig() {
|
|
|
665
638
|
const port = normalizePort(raw.port);
|
|
666
639
|
const thinkEffort = normalizeThinkEffort(raw.thinkEffort);
|
|
667
640
|
const upstreamTimeoutSeconds = normalizeUpstreamTimeoutSeconds(raw.upstreamTimeoutSeconds);
|
|
668
|
-
const configVersion = normalizePositiveInteger(raw.configVersion) ?? 1;
|
|
669
641
|
const config = {
|
|
670
642
|
claudeSetup: claudeSetup ?? defaultConfig.claudeSetup,
|
|
671
|
-
configVersion: currentConfigVersion,
|
|
672
643
|
copilotBaseUrl: normalizeString(raw.copilotBaseUrl) ?? defaultConfig.copilotBaseUrl,
|
|
673
644
|
gptModel: normalizeString(raw.gptModel) ?? defaultConfig.gptModel,
|
|
674
645
|
host: host ?? defaultConfig.host,
|
|
675
646
|
logLevel: logLevel ?? defaultConfig.logLevel,
|
|
676
647
|
logRetentionDays: logRetentionDays$1 ?? defaultConfig.logRetentionDays,
|
|
677
|
-
opusModel:
|
|
648
|
+
opusModel: normalizeString(raw.opusModel) ?? defaultConfig.opusModel,
|
|
678
649
|
port: port ?? defaultConfig.port,
|
|
679
650
|
thinkEffort: thinkEffort ?? defaultConfig.thinkEffort,
|
|
680
651
|
upstreamTimeoutSeconds: upstreamTimeoutSeconds ?? defaultConfig.upstreamTimeoutSeconds,
|
|
@@ -780,6 +751,33 @@ const isProcessAlive = (pid) => {
|
|
|
780
751
|
return isNodeErrno(error) && error.code === "EPERM";
|
|
781
752
|
}
|
|
782
753
|
};
|
|
754
|
+
/**
|
|
755
|
+
* Full pid-file record, for callers that need the listening address or start
|
|
756
|
+
* time rather than just the pid.
|
|
757
|
+
*
|
|
758
|
+
* Deliberately tolerant: a corrupt or half-written pid file yields undefined
|
|
759
|
+
* rather than throwing, because `status` must be able to report "not running"
|
|
760
|
+
* instead of crashing on a file the user may have been mid-edit on.
|
|
761
|
+
*
|
|
762
|
+
* Returns undefined for the legacy bare-pid format, which carries no address.
|
|
763
|
+
*/
|
|
764
|
+
const readRelayPidFileEntry = async () => {
|
|
765
|
+
try {
|
|
766
|
+
const trimmed = (await fs.readFile(paths.pidPath, "utf8")).trim();
|
|
767
|
+
if (!trimmed || !trimmed.startsWith("{")) return;
|
|
768
|
+
const payload = JSON.parse(trimmed);
|
|
769
|
+
const pid = parsePid(payload.pid);
|
|
770
|
+
if (pid === void 0 || typeof payload.host !== "string" || typeof payload.port !== "number") return;
|
|
771
|
+
return {
|
|
772
|
+
host: payload.host,
|
|
773
|
+
pid,
|
|
774
|
+
port: payload.port,
|
|
775
|
+
startedAt: typeof payload.startedAt === "string" ? payload.startedAt : ""
|
|
776
|
+
};
|
|
777
|
+
} catch {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
783
781
|
const readRelayPidFile = async () => {
|
|
784
782
|
try {
|
|
785
783
|
const trimmed = (await fs.readFile(paths.pidPath, "utf8")).trim();
|
|
@@ -2869,9 +2867,9 @@ const getLoggedHeaders = (request) => Object.fromEntries(loggedRequestHeaders.fl
|
|
|
2869
2867
|
const value = request.headers.get(name);
|
|
2870
2868
|
return value ? [[name, value]] : [];
|
|
2871
2869
|
}));
|
|
2872
|
-
const formatStatusLog = (requestId, method, path$1, status, ms, errorMessage) => {
|
|
2873
|
-
const base = `request_id=${requestId} ${method} ${path$1} -> ${status} ${ms}ms`;
|
|
2874
|
-
return status >= 400 && errorMessage ? `${base} error=${JSON.stringify(errorMessage)}` : base;
|
|
2870
|
+
const formatStatusLog = (requestId, method, path$1, status$1, ms, errorMessage) => {
|
|
2871
|
+
const base = `request_id=${requestId} ${method} ${path$1} -> ${status$1} ${ms}ms`;
|
|
2872
|
+
return status$1 >= 400 && errorMessage ? `${base} error=${JSON.stringify(errorMessage)}` : base;
|
|
2875
2873
|
};
|
|
2876
2874
|
const createServer = (config) => {
|
|
2877
2875
|
const app = new Hono();
|
|
@@ -3049,6 +3047,195 @@ const restart = defineCommand({
|
|
|
3049
3047
|
}
|
|
3050
3048
|
});
|
|
3051
3049
|
|
|
3050
|
+
//#endregion
|
|
3051
|
+
//#region src/status.ts
|
|
3052
|
+
/**
|
|
3053
|
+
* Exit codes, so `status` is usable in a health check or a script.
|
|
3054
|
+
*
|
|
3055
|
+
* 2 is distinct from 1 on purpose: "running but broken" and "not running" call
|
|
3056
|
+
* for different responses - restart the service versus re-authenticate.
|
|
3057
|
+
*/
|
|
3058
|
+
const exitCodes = {
|
|
3059
|
+
deepCheckFailed: 2,
|
|
3060
|
+
notRunning: 1,
|
|
3061
|
+
running: 0
|
|
3062
|
+
};
|
|
3063
|
+
const localProbeTimeoutMs = 5e3;
|
|
3064
|
+
const deepProbeTimeoutMs = 6e4;
|
|
3065
|
+
const formatUptime = (startedAt) => {
|
|
3066
|
+
if (!startedAt) return "unknown";
|
|
3067
|
+
const started = Date.parse(startedAt);
|
|
3068
|
+
if (Number.isNaN(started)) return "unknown";
|
|
3069
|
+
const seconds = Math.max(0, Math.floor((Date.now() - started) / 1e3));
|
|
3070
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3071
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
3072
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
|
|
3073
|
+
return `${Math.floor(seconds / 86400)}d ${Math.floor(seconds % 86400 / 3600)}h`;
|
|
3074
|
+
};
|
|
3075
|
+
/**
|
|
3076
|
+
* Pure state-to-lines mapping, kept free of IO so it can be tested directly
|
|
3077
|
+
* without spawning a relay or opening a socket.
|
|
3078
|
+
*/
|
|
3079
|
+
const renderStatus = (status$1) => {
|
|
3080
|
+
const lines = [`copilot-relay ${status$1.version}`];
|
|
3081
|
+
if (!status$1.running) {
|
|
3082
|
+
lines.push(" process not running", ` config ${status$1.configPath} (logLevel=${status$1.logLevel}, thinkEffort=${status$1.thinkEffort})`, ` log ${status$1.logPath}`, "", " Start it with: copilot-relay start");
|
|
3083
|
+
return lines;
|
|
3084
|
+
}
|
|
3085
|
+
lines.push(` process running (pid ${status$1.pid}, up ${formatUptime(status$1.startedAt)})`, ` listening ${status$1.baseUrl}`);
|
|
3086
|
+
const health = status$1.health;
|
|
3087
|
+
lines.push(health?.ok ? ` health ok (${health.ms}ms)` : ` health FAILED${health?.detail ? ` — ${health.detail}` : ""}`);
|
|
3088
|
+
lines.push(status$1.models?.length ? ` models ${status$1.models.join(", ")}` : " models unavailable");
|
|
3089
|
+
if (status$1.deep) lines.push(status$1.deep.ok ? ` upstream ok (${status$1.deep.ms}ms) — end-to-end Copilot round trip` : ` upstream FAILED${status$1.deep.detail ? ` — ${status$1.deep.detail}` : ""}`);
|
|
3090
|
+
else lines.push(" upstream not checked (use --deep)");
|
|
3091
|
+
lines.push(` log ${status$1.logPath}`, ` config ${status$1.configPath} (logLevel=${status$1.logLevel}, thinkEffort=${status$1.thinkEffort})`);
|
|
3092
|
+
if (status$1.health?.ok && status$1.deep && !status$1.deep.ok) lines.push("", " The relay is listening but cannot reach Copilot.", " Try: copilot-relay auth");
|
|
3093
|
+
return lines;
|
|
3094
|
+
};
|
|
3095
|
+
const probe = async (url, init, timeoutMs) => {
|
|
3096
|
+
const started = performance.now();
|
|
3097
|
+
const response = await fetch(url, {
|
|
3098
|
+
...init,
|
|
3099
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3100
|
+
});
|
|
3101
|
+
return {
|
|
3102
|
+
body: await response.json().catch(() => void 0),
|
|
3103
|
+
ms: Math.round(performance.now() - started),
|
|
3104
|
+
ok: response.ok,
|
|
3105
|
+
status: response.status
|
|
3106
|
+
};
|
|
3107
|
+
};
|
|
3108
|
+
const describeError = (error) => error instanceof Error ? error.name === "TimeoutError" ? "timed out" : error.message : String(error);
|
|
3109
|
+
const checkHealth = async (baseUrl) => {
|
|
3110
|
+
try {
|
|
3111
|
+
const result = await probe(`${baseUrl}/healthz`, {}, localProbeTimeoutMs);
|
|
3112
|
+
return result.ok ? {
|
|
3113
|
+
ms: result.ms,
|
|
3114
|
+
ok: true
|
|
3115
|
+
} : {
|
|
3116
|
+
detail: `http ${result.status}`,
|
|
3117
|
+
ms: result.ms,
|
|
3118
|
+
ok: false
|
|
3119
|
+
};
|
|
3120
|
+
} catch (error) {
|
|
3121
|
+
return {
|
|
3122
|
+
detail: describeError(error),
|
|
3123
|
+
ok: false
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
};
|
|
3127
|
+
const readModels = async (baseUrl) => {
|
|
3128
|
+
try {
|
|
3129
|
+
const data = (await probe(`${baseUrl}/v1/models`, {}, localProbeTimeoutMs)).body?.data;
|
|
3130
|
+
return Array.isArray(data) ? data.flatMap((model) => typeof model.id === "string" ? [model.id] : []) : [];
|
|
3131
|
+
} catch {
|
|
3132
|
+
return [];
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
3135
|
+
/**
|
|
3136
|
+
* Layer 3: a real request through Copilot.
|
|
3137
|
+
*
|
|
3138
|
+
* Opt-in because it spends tokens. It is the only check that exercises token
|
|
3139
|
+
* refresh, the upstream call, and translation back to Claude shape.
|
|
3140
|
+
*/
|
|
3141
|
+
const checkDeep = async (baseUrl, model) => {
|
|
3142
|
+
try {
|
|
3143
|
+
const result = await probe(`${baseUrl}/v1/messages`, {
|
|
3144
|
+
body: JSON.stringify({
|
|
3145
|
+
max_tokens: 16,
|
|
3146
|
+
messages: [{
|
|
3147
|
+
content: "Reply with the single word: ok",
|
|
3148
|
+
role: "user"
|
|
3149
|
+
}],
|
|
3150
|
+
model
|
|
3151
|
+
}),
|
|
3152
|
+
headers: {
|
|
3153
|
+
"anthropic-version": "2023-06-01",
|
|
3154
|
+
"content-type": "application/json"
|
|
3155
|
+
},
|
|
3156
|
+
method: "POST"
|
|
3157
|
+
}, deepProbeTimeoutMs);
|
|
3158
|
+
if (!result.ok) {
|
|
3159
|
+
const message = result.body?.error?.message;
|
|
3160
|
+
return {
|
|
3161
|
+
detail: message ? `http ${result.status}: ${message}` : `http ${result.status}`,
|
|
3162
|
+
ms: result.ms,
|
|
3163
|
+
ok: false
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
const content = result.body?.content;
|
|
3167
|
+
return Array.isArray(content) && content.length > 0 ? {
|
|
3168
|
+
ms: result.ms,
|
|
3169
|
+
ok: true
|
|
3170
|
+
} : {
|
|
3171
|
+
detail: "empty response",
|
|
3172
|
+
ms: result.ms,
|
|
3173
|
+
ok: false
|
|
3174
|
+
};
|
|
3175
|
+
} catch (error) {
|
|
3176
|
+
return {
|
|
3177
|
+
detail: describeError(error),
|
|
3178
|
+
ok: false
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
const collectStatus = async (options) => {
|
|
3183
|
+
const appConfig = await readAppConfig();
|
|
3184
|
+
setLogLevel(appConfig.logLevel);
|
|
3185
|
+
const base = {
|
|
3186
|
+
configPath: paths.configPath,
|
|
3187
|
+
logLevel: appConfig.logLevel,
|
|
3188
|
+
logPath: getLogPath(),
|
|
3189
|
+
thinkEffort: appConfig.thinkEffort,
|
|
3190
|
+
version: appVersion
|
|
3191
|
+
};
|
|
3192
|
+
const pids = await findRelayProcessIds(readProxyConfig(appConfig));
|
|
3193
|
+
if (pids.length === 0) return {
|
|
3194
|
+
...base,
|
|
3195
|
+
running: false
|
|
3196
|
+
};
|
|
3197
|
+
const entry = await readRelayPidFileEntry();
|
|
3198
|
+
const host = entry?.host ?? appConfig.host;
|
|
3199
|
+
const port = entry?.port ?? appConfig.port;
|
|
3200
|
+
const baseUrl = `http://${host}:${port}`;
|
|
3201
|
+
const health = await checkHealth(baseUrl);
|
|
3202
|
+
const models = health.ok ? await readModels(baseUrl) : [];
|
|
3203
|
+
const deep = options.deep && health.ok ? await checkDeep(baseUrl, models[0] ?? appConfig.gptModel) : void 0;
|
|
3204
|
+
return {
|
|
3205
|
+
...base,
|
|
3206
|
+
baseUrl,
|
|
3207
|
+
...deep ? { deep } : {},
|
|
3208
|
+
health,
|
|
3209
|
+
models,
|
|
3210
|
+
pid: entry?.pid ?? pids[0],
|
|
3211
|
+
port,
|
|
3212
|
+
running: true,
|
|
3213
|
+
...entry?.startedAt ? { startedAt: entry.startedAt } : {}
|
|
3214
|
+
};
|
|
3215
|
+
};
|
|
3216
|
+
const status = defineCommand({
|
|
3217
|
+
meta: {
|
|
3218
|
+
name: "status",
|
|
3219
|
+
description: "Show whether copilot-relay is running and reachable."
|
|
3220
|
+
},
|
|
3221
|
+
args: {
|
|
3222
|
+
deep: {
|
|
3223
|
+
description: "Also send a real request through Copilot. Proves the relay works end to end; spends a few tokens.",
|
|
3224
|
+
type: "boolean"
|
|
3225
|
+
},
|
|
3226
|
+
json: {
|
|
3227
|
+
description: "Emit machine-readable JSON.",
|
|
3228
|
+
type: "boolean"
|
|
3229
|
+
}
|
|
3230
|
+
},
|
|
3231
|
+
async run({ args }) {
|
|
3232
|
+
const result = await collectStatus({ deep: Boolean(args.deep) });
|
|
3233
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
3234
|
+
else console.log(renderStatus(result).join("\n"));
|
|
3235
|
+
process.exitCode = !result.running ? exitCodes.notRunning : result.deep && !result.deep.ok ? exitCodes.deepCheckFailed : exitCodes.running;
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3052
3239
|
//#endregion
|
|
3053
3240
|
//#region src/stop.ts
|
|
3054
3241
|
const stop = defineCommand({
|
|
@@ -3076,6 +3263,7 @@ await runMain(defineCommand({
|
|
|
3076
3263
|
auth,
|
|
3077
3264
|
restart,
|
|
3078
3265
|
start,
|
|
3266
|
+
status,
|
|
3079
3267
|
stop
|
|
3080
3268
|
}
|
|
3081
3269
|
}));
|