copilot-reverse 0.1.0 → 0.2.1
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/cli/auth.js +17 -3
- package/dist/cli/index.js +35 -13
- package/dist/core/tool-xml.js +92 -0
- package/dist/providers/copilot/adapter.js +40 -2
- package/dist/tui/app.js +18 -1
- package/dist/tui/assistant/on-chat.js +10 -1
- package/dist/version.js +2 -0
- package/package.json +3 -1
package/dist/cli/auth.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { requestDeviceCode, pollForToken } from "../providers/copilot/auth.js";
|
|
2
2
|
import { writeGhToken } from "../shared/creds.js";
|
|
3
|
-
|
|
3
|
+
// Two-phase device login. `beginDeviceLogin` returns the verification code right away so a caller
|
|
4
|
+
// can surface it to the user; `complete()` then blocks on authorization and writes the token.
|
|
5
|
+
// Splitting these is what lets the TUI render the code while the poll is still pending — folding
|
|
6
|
+
// both into one call buffers the code behind the blocking poll, and the user can't authorize a
|
|
7
|
+
// code they can't see.
|
|
8
|
+
export async function beginDeviceLogin(dir, fetchFn = fetch) {
|
|
4
9
|
const code = await requestDeviceCode(fetchFn);
|
|
10
|
+
return {
|
|
11
|
+
code,
|
|
12
|
+
complete: async () => {
|
|
13
|
+
const token = await pollForToken(code.device_code, code.interval * 1000, fetchFn);
|
|
14
|
+
writeGhToken(token, dir);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function runDeviceLogin(dir, fetchFn = fetch, log = console.log) {
|
|
19
|
+
const { code, complete } = await beginDeviceLogin(dir, fetchFn);
|
|
5
20
|
log(`\nOpen ${code.verification_uri} and enter code: ${code.user_code}\n`);
|
|
6
|
-
|
|
7
|
-
writeGhToken(token, dir);
|
|
21
|
+
await complete();
|
|
8
22
|
log("GitHub authorization complete.");
|
|
9
23
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { App } from "../tui/app.js";
|
|
6
6
|
import { buildRegistry } from "../tui/slash/commands.js";
|
|
7
7
|
import { DaemonClient } from "../tui/daemon-client.js";
|
|
8
|
-
import { runDeviceLogin } from "./auth.js";
|
|
8
|
+
import { runDeviceLogin, beginDeviceLogin } from "./auth.js";
|
|
9
9
|
import { probeSupervisor } from "../daemon/lifecycle.js";
|
|
10
10
|
import { startSupervisor } from "../supervisor/index.js";
|
|
11
11
|
import { runAssistantTurn } from "../tui/assistant/runtime.js";
|
|
@@ -21,6 +21,7 @@ import { applyCodexToml } from "../tui/setup/codex-toml.js";
|
|
|
21
21
|
import { claudeCopilotReverseEnv } from "../tui/setup/clients.js";
|
|
22
22
|
import { dataDir } from "../shared/paths.js";
|
|
23
23
|
import { defaultConfig } from "../shared/config.js";
|
|
24
|
+
import { APP_VERSION } from "../version.js";
|
|
24
25
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
26
|
const DEFAULT_MODEL = "gpt-4o"; // a valid Copilot model id; pass-through routing uses it as-is
|
|
26
27
|
// Conservative context budget that drives the assistant's auto-compaction. Sized below the
|
|
@@ -70,17 +71,9 @@ async function launchTui() {
|
|
|
70
71
|
const registry = buildRegistry({ client, quit }, endpoint, {
|
|
71
72
|
dashboardUrl: `http://${cfg.bindHost}:${cfg.supervisorPort}/`,
|
|
72
73
|
reportRepo: cfg.reportRepo,
|
|
73
|
-
appVersion:
|
|
74
|
+
appVersion: APP_VERSION,
|
|
74
75
|
platform: `${process.platform} node-${process.version}`,
|
|
75
76
|
resetClient,
|
|
76
|
-
// Re-run device-code login, then restart the worker so it picks up the new token.
|
|
77
|
-
login: async () => {
|
|
78
|
-
const lines = [];
|
|
79
|
-
await runDeviceLogin(dataDir(), fetch, (m) => lines.push(m));
|
|
80
|
-
await client.restart().catch(() => { });
|
|
81
|
-
lines.push("worker restarting with the new token");
|
|
82
|
-
return lines;
|
|
83
|
-
},
|
|
84
77
|
// Clear the stored token and restart the worker (it will report unauthenticated until re-login).
|
|
85
78
|
logout: async () => {
|
|
86
79
|
clearGhToken(dataDir());
|
|
@@ -88,9 +81,22 @@ async function launchTui() {
|
|
|
88
81
|
return ["signed out — GitHub token removed", "run /login to sign in again"];
|
|
89
82
|
},
|
|
90
83
|
});
|
|
84
|
+
// Two-phase /login for the TUI: surface the device code immediately, poll in the background, then
|
|
85
|
+
// restart the worker so it picks up the new token. The blocking single-call form deadlocked the
|
|
86
|
+
// Repl (the code stayed hidden behind the poll, so the user could never authorize it).
|
|
87
|
+
const doLogin = async (show) => {
|
|
88
|
+
const { code, complete } = await beginDeviceLogin(dataDir());
|
|
89
|
+
show([`Open ${code.verification_uri} and enter code: ${code.user_code}`, "waiting for authorization…"]);
|
|
90
|
+
await complete();
|
|
91
|
+
// Re-point the token store at the freshly written GitHub token; the old store still holds the
|
|
92
|
+
// expired one and would 401 once its cached Copilot token rotates, breaking the model picker.
|
|
93
|
+
tokenStore = new CopilotTokenStore(readGhToken(dataDir()));
|
|
94
|
+
await client.restart().catch(() => { });
|
|
95
|
+
return ["GitHub authorization complete — worker restarting with the new token"];
|
|
96
|
+
};
|
|
91
97
|
// Filled in below once we have a token; the assistant prefers a model's real window over the default.
|
|
92
98
|
const modelLimits = {};
|
|
93
|
-
|
|
99
|
+
let tokenStore = new CopilotTokenStore(readGhToken(dataDir()));
|
|
94
100
|
const loadModels = async () => {
|
|
95
101
|
const token = await tokenStore.get();
|
|
96
102
|
const [ids, limits] = await Promise.all([fetchCopilotModels(token), fetchModelLimits(token)]);
|
|
@@ -121,7 +127,22 @@ async function launchTui() {
|
|
|
121
127
|
maxInputTokens: DEFAULT_MAX_INPUT_TOKENS, modelLimits,
|
|
122
128
|
listModels: loadModels,
|
|
123
129
|
setupClient: async (c, s, m) => applyClient(c, s, m),
|
|
124
|
-
}, (c, p, print, abort) => runAssistantTurn(c, p, print, undefined, abort)
|
|
130
|
+
}, (c, p, print, abort) => runAssistantTurn(c, p, print, undefined, abort), undefined,
|
|
131
|
+
// Pre-flight auth gate: block a turn (with an actionable hint) when there's no GitHub token, or
|
|
132
|
+
// the stored one no longer exchanges for a Copilot token — instead of firing a request that just
|
|
133
|
+
// hangs until the turn timeout. Reuses the long-lived tokenStore so a valid login is a cached,
|
|
134
|
+
// round-trip-free check between message bursts (its get() caches with a 60s skew).
|
|
135
|
+
async () => {
|
|
136
|
+
if (!readGhToken(dataDir()))
|
|
137
|
+
return "you're signed out — run /login to sign in before chatting";
|
|
138
|
+
try {
|
|
139
|
+
await tokenStore.get();
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return "your GitHub login has expired — run /login to sign in again";
|
|
144
|
+
}
|
|
145
|
+
});
|
|
125
146
|
const persistedModel = readChatModel(dataDir());
|
|
126
147
|
app = render(React.createElement(App, {
|
|
127
148
|
registry,
|
|
@@ -142,10 +163,11 @@ async function launchTui() {
|
|
|
142
163
|
},
|
|
143
164
|
onModelChange: (m) => writeChatModel(dataDir(), m),
|
|
144
165
|
pickModelOnStart: !persistedModel,
|
|
166
|
+
login: doLogin,
|
|
145
167
|
}));
|
|
146
168
|
}
|
|
147
169
|
const program = new Command();
|
|
148
|
-
program.name("copilot-reverse").description("copilot-reverse: interactive Copilot proxy").version(
|
|
170
|
+
program.name("copilot-reverse").description("copilot-reverse: interactive Copilot proxy").version(APP_VERSION);
|
|
149
171
|
program.command("login").description("GitHub device-code login").action(() => runDeviceLogin(dataDir()));
|
|
150
172
|
program.action(() => { void launchTui(); });
|
|
151
173
|
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
// Opening sentinels that switch the parser into capture mode. The `antml:` namespaced variants are
|
|
3
|
+
// the un-stripped originals; the bare forms are what survives when the namespace prefix is dropped.
|
|
4
|
+
const TRIGGER_RE = /<(?:antml:)?(?:function_calls>|invoke\b)/;
|
|
5
|
+
// Longest suffix of `s` that is a proper prefix of a trigger token — text we must hold back because
|
|
6
|
+
// it might be the front of a sentinel split across chunk boundaries (e.g. "…<inv" then "oke name=").
|
|
7
|
+
const PREFIX_TOKENS = ["<function_calls>", "<function_calls>", "<invoke", "<invoke"];
|
|
8
|
+
function heldBackLen(s) {
|
|
9
|
+
let max = 0;
|
|
10
|
+
for (const t of PREFIX_TOKENS) {
|
|
11
|
+
for (let k = Math.min(s.length, t.length - 1); k > 0; k--) {
|
|
12
|
+
if (s.endsWith(t.slice(0, k))) {
|
|
13
|
+
if (k > max)
|
|
14
|
+
max = k;
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return max;
|
|
20
|
+
}
|
|
21
|
+
// Index just past a `</tag>` (or `</tag>`) close, or -1 if not yet present in `s`.
|
|
22
|
+
function closeIndex(s, tag) {
|
|
23
|
+
const m = new RegExp(`</(?:antml:)?${tag}>`).exec(s);
|
|
24
|
+
return m ? m.index + m[0].length : -1;
|
|
25
|
+
}
|
|
26
|
+
// A scalar parameter value is raw text in the XML; recover its intended type by trying JSON, so
|
|
27
|
+
// `42`/`true`/`{"a":1}` become real values while a bare command string stays a string.
|
|
28
|
+
function coerce(raw) {
|
|
29
|
+
const v = raw.replace(/^\n/, "").replace(/\n$/, "");
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(v);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseInvokes(block) {
|
|
38
|
+
const tools = [];
|
|
39
|
+
const invokeRe = /<(?:antml:)?invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/(?:antml:)?invoke>/g;
|
|
40
|
+
for (let m = invokeRe.exec(block); m; m = invokeRe.exec(block)) {
|
|
41
|
+
const [, name, body] = m;
|
|
42
|
+
const input = {};
|
|
43
|
+
const paramRe = /<(?:antml:)?parameter\s+name="([^"]+)"\s*>([\s\S]*?)<\/(?:antml:)?parameter>/g;
|
|
44
|
+
for (let p = paramRe.exec(body); p; p = paramRe.exec(body))
|
|
45
|
+
input[p[1]] = coerce(p[2]);
|
|
46
|
+
tools.push({ id: `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`, name, input });
|
|
47
|
+
}
|
|
48
|
+
return tools;
|
|
49
|
+
}
|
|
50
|
+
export class ToolCallExtractor {
|
|
51
|
+
buf = "";
|
|
52
|
+
capturing = false;
|
|
53
|
+
feed(chunk) {
|
|
54
|
+
this.buf += chunk;
|
|
55
|
+
const events = [];
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (!this.capturing) {
|
|
58
|
+
const m = TRIGGER_RE.exec(this.buf);
|
|
59
|
+
if (!m) {
|
|
60
|
+
// No trigger; emit everything except a possible partial-sentinel tail.
|
|
61
|
+
const keep = heldBackLen(this.buf);
|
|
62
|
+
const emit = this.buf.slice(0, this.buf.length - keep);
|
|
63
|
+
if (emit)
|
|
64
|
+
events.push({ kind: "text", text: emit });
|
|
65
|
+
this.buf = keep ? this.buf.slice(this.buf.length - keep) : "";
|
|
66
|
+
return events;
|
|
67
|
+
}
|
|
68
|
+
if (m.index > 0)
|
|
69
|
+
events.push({ kind: "text", text: this.buf.slice(0, m.index) });
|
|
70
|
+
this.buf = this.buf.slice(m.index);
|
|
71
|
+
this.capturing = true;
|
|
72
|
+
}
|
|
73
|
+
const isWrapper = /^<(?:antml:)?function_calls>/.test(this.buf);
|
|
74
|
+
const end = closeIndex(this.buf, isWrapper ? "function_calls" : "invoke");
|
|
75
|
+
if (end < 0)
|
|
76
|
+
return events; // incomplete block — wait for more data
|
|
77
|
+
const block = this.buf.slice(0, end);
|
|
78
|
+
for (const tool of parseInvokes(block))
|
|
79
|
+
events.push({ kind: "tool", tool });
|
|
80
|
+
this.buf = this.buf.slice(end);
|
|
81
|
+
this.capturing = false; // a following <invoke> re-triggers via the passthrough branch
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Stream ended. Anything still buffered is an incomplete block we couldn't parse — emit it as
|
|
85
|
+
// text so nothing is silently dropped.
|
|
86
|
+
flush() {
|
|
87
|
+
const out = this.buf ? [{ kind: "text", text: this.buf }] : [];
|
|
88
|
+
this.buf = "";
|
|
89
|
+
this.capturing = false;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ToolCallExtractor } from "../../core/tool-xml.js";
|
|
2
3
|
const CHAT_URL = "https://api.githubcopilot.com/chat/completions";
|
|
3
4
|
// Canonical messages -> OpenAI wire messages (Copilot is OpenAI-shaped).
|
|
4
5
|
function toWireMessages(messages) {
|
|
@@ -88,6 +89,27 @@ export class CopilotAdapter {
|
|
|
88
89
|
let finishReason = "stop";
|
|
89
90
|
let usage;
|
|
90
91
|
const mapFinish = (f) => f === "tool_calls" ? "tool_use" : f === "length" ? "length" : "stop";
|
|
92
|
+
// Some models emit a tool call as inline XML text instead of native tool_calls (more likely on
|
|
93
|
+
// long/tool-heavy turns). When the request has tools, route assistant text through an extractor
|
|
94
|
+
// that recovers those blocks into structured tool calls; otherwise text passes straight through.
|
|
95
|
+
const extractor = req.tools?.length ? new ToolCallExtractor() : undefined;
|
|
96
|
+
let extractedTool = false;
|
|
97
|
+
let extIdx = 100; // separate index space so recovered tools never collide with native tool_calls
|
|
98
|
+
const toChunks = (events) => {
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const ev of events) {
|
|
101
|
+
if (ev.kind === "text") {
|
|
102
|
+
if (ev.text)
|
|
103
|
+
out.push({ kind: "text", delta: ev.text, done: false });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const index = extIdx++;
|
|
107
|
+
extractedTool = true;
|
|
108
|
+
out.push({ kind: "tool_use_start", index, id: ev.tool.id, name: ev.tool.name, done: false });
|
|
109
|
+
out.push({ kind: "tool_use_delta", index, argsDelta: JSON.stringify(ev.tool.input), done: false });
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
91
113
|
for (;;) {
|
|
92
114
|
const { value, done } = await reader.read();
|
|
93
115
|
if (done)
|
|
@@ -101,6 +123,11 @@ export class CopilotAdapter {
|
|
|
101
123
|
continue;
|
|
102
124
|
const payload = line.slice(6).trim();
|
|
103
125
|
if (payload === "[DONE]") {
|
|
126
|
+
if (extractor)
|
|
127
|
+
for (const ch of toChunks(extractor.flush()))
|
|
128
|
+
yield ch;
|
|
129
|
+
if (extractedTool && finishReason === "stop")
|
|
130
|
+
finishReason = "tool_use";
|
|
104
131
|
yield { kind: "done", done: true, finishReason, usage };
|
|
105
132
|
return;
|
|
106
133
|
}
|
|
@@ -122,8 +149,14 @@ export class CopilotAdapter {
|
|
|
122
149
|
const delta = choice.delta;
|
|
123
150
|
if (!delta)
|
|
124
151
|
continue;
|
|
125
|
-
if (delta.content)
|
|
126
|
-
|
|
152
|
+
if (delta.content) {
|
|
153
|
+
if (extractor) {
|
|
154
|
+
for (const ch of toChunks(extractor.feed(delta.content)))
|
|
155
|
+
yield ch;
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
yield { kind: "text", delta: delta.content, done: false };
|
|
159
|
+
}
|
|
127
160
|
for (const tc of delta.tool_calls ?? []) {
|
|
128
161
|
const idx = tc.index ?? 0;
|
|
129
162
|
if (!startedTools.has(idx) && tc.function?.name) {
|
|
@@ -135,6 +168,11 @@ export class CopilotAdapter {
|
|
|
135
168
|
}
|
|
136
169
|
}
|
|
137
170
|
}
|
|
171
|
+
if (extractor)
|
|
172
|
+
for (const ch of toChunks(extractor.flush()))
|
|
173
|
+
yield ch;
|
|
174
|
+
if (extractedTool && finishReason === "stop")
|
|
175
|
+
finishReason = "tool_use";
|
|
138
176
|
yield { kind: "done", done: true, finishReason, usage };
|
|
139
177
|
}
|
|
140
178
|
}
|
package/dist/tui/app.js
CHANGED
|
@@ -36,7 +36,7 @@ function ClientBadge({ name, status }) {
|
|
|
36
36
|
const cell = (label, on) => (_jsxs(Text, { color: on ? theme.ready : theme.muted, children: [label, ":", on ? "✓" : "○"] }));
|
|
37
37
|
return (_jsxs(Text, { color: theme.muted, children: [name, " ", cell("u", status.user), " ", cell("p", status.project)] }));
|
|
38
38
|
}
|
|
39
|
-
export function App({ registry, title, workerState = "starting", initialModel = "—", statusSource, readStatus, modelLimits, onChat, loadModels, setup, info, onModelChange, pickModelOnStart, }) {
|
|
39
|
+
export function App({ registry, title, workerState = "starting", initialModel = "—", statusSource, readStatus, modelLimits, onChat, loadModels, setup, info, onModelChange, pickModelOnStart, login, }) {
|
|
40
40
|
const cmds = registry.list().map((c) => ({ name: c.name, describe: c.describe }));
|
|
41
41
|
const [entries, setEntries] = useState([
|
|
42
42
|
{ type: "system", text: "Type a message to chat with the assistant, or /help for commands." },
|
|
@@ -47,6 +47,7 @@ export function App({ registry, title, workerState = "starting", initialModel =
|
|
|
47
47
|
const [screen, setScreen] = useState(pickModelOnStart && loadModels ? { kind: "model" } : null);
|
|
48
48
|
const [, setNow] = useState(0); // ticks the live loading line while the assistant streams
|
|
49
49
|
const abortRef = useRef(null); // current turn's interrupt handle
|
|
50
|
+
const loginInFlight = useRef(false); // guards against starting a second device-login flow
|
|
50
51
|
const add = (e) => setEntries((p) => [...p, e].slice(-100));
|
|
51
52
|
const refreshStatus = () => { if (readStatus)
|
|
52
53
|
setStatus(readStatus()); };
|
|
@@ -95,6 +96,22 @@ export function App({ registry, title, workerState = "starting", initialModel =
|
|
|
95
96
|
setScreen({ kind: "config" });
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
99
|
+
if (t === "/login" && login) {
|
|
100
|
+
// Show the verification URL + code right away, then resolve a completion card once the user
|
|
101
|
+
// authorizes. Done as a special case (not a registry command) because the slash registry only
|
|
102
|
+
// renders a command's final return value — it can't surface the code mid-poll. Guarded so a
|
|
103
|
+
// double Enter doesn't start two device-code flows (polling a superseded code 401s).
|
|
104
|
+
if (loginInFlight.current) {
|
|
105
|
+
add({ type: "card", title: "/login", tone: "info", lines: ["already waiting for authorization…"] });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
loginInFlight.current = true;
|
|
109
|
+
void login((lines) => add({ type: "card", title: "/login", tone: "info", lines }))
|
|
110
|
+
.then((lines) => add({ type: "card", title: "/login", tone: "ok", lines }))
|
|
111
|
+
.catch((e) => add({ type: "card", title: "/login", tone: "error", lines: [`login failed: ${e instanceof Error ? e.message : String(e)}`] }))
|
|
112
|
+
.finally(() => { loginInFlight.current = false; });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
98
115
|
if (setup && loadModels && (t === "/setup-claude" || t === "/setup-codex")) {
|
|
99
116
|
setScreen({ kind: "setup", client: t === "/setup-claude" ? "claude" : "codex" });
|
|
100
117
|
return;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
const DEFAULT_TURN_TIMEOUT_MS = 120_000; // 2 minutes — a turn that hasn't replied by then is given up on
|
|
2
|
-
export function makeOnChat(cfg, runner, timeoutMs = DEFAULT_TURN_TIMEOUT_MS) {
|
|
2
|
+
export function makeOnChat(cfg, runner, timeoutMs = DEFAULT_TURN_TIMEOUT_MS, precheck) {
|
|
3
3
|
return async (text, print, model, abort) => {
|
|
4
|
+
// Gate the turn on auth before firing a doomed request. Without this, a signed-out user's message
|
|
5
|
+
// hangs until the 120s timeout instead of getting an immediate, actionable hint.
|
|
6
|
+
if (precheck) {
|
|
7
|
+
const blocked = await precheck().catch(() => null); // a failed check must never wedge chat
|
|
8
|
+
if (blocked) {
|
|
9
|
+
print(blocked);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
4
13
|
const ctrl = abort ?? new AbortController();
|
|
5
14
|
let timedOut = false;
|
|
6
15
|
// Race the turn against a hard timeout so a hung SDK/upstream can never block the UI forever.
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-reverse",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Interactive terminal app that exposes your GitHub Copilot subscription as local OpenAI- and Anthropic-compatible endpoints, with a self-healing daemon and a built-in assistant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,12 +32,14 @@
|
|
|
32
32
|
"llm"
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
|
+
"prebuild": "node scripts/gen-version.mjs",
|
|
35
36
|
"build": "tsc -p tsconfig.json",
|
|
36
37
|
"test": "vitest run",
|
|
37
38
|
"test:coverage": "vitest run --coverage",
|
|
38
39
|
"test:e2e": "vitest run e2e/copilot-reverse.e2e.test.ts tests/e2e",
|
|
39
40
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
40
41
|
"dev": "tsx src/cli/index.ts",
|
|
42
|
+
"changeset": "node scripts/changesets.mjs new",
|
|
41
43
|
"prepublishOnly": "npm run build && npm run test"
|
|
42
44
|
},
|
|
43
45
|
"engines": {
|