@melaya/runner 1.1.27 → 1.1.28

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.js CHANGED
@@ -17,14 +17,6 @@ import { Command } from "commander";
17
17
  import chalk from "chalk";
18
18
  import { connect } from "./connection.js";
19
19
  import { detectModels } from "./detect.js";
20
- const program = new Command()
21
- .name("melaya-runner")
22
- .description("Run Melaya AI pipelines locally with your own models")
23
- .requiredOption("--token <token>", "Runner token from the Melaya platform")
24
- .option("--server <url>", "Server URL", "wss://api.melaya.org")
25
- .option("--verbose", "Show detailed logs", false)
26
- .parse(process.argv);
27
- const opts = program.opts();
28
20
  // Logo colors: cyan → blue → indigo gradient (matching melaya.org)
29
21
  const c1 = chalk.hex("#22D3EE").bold;
30
22
  const c2 = chalk.hex("#10B0F0").bold;
@@ -40,7 +32,7 @@ import { dirname, join } from "path";
40
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
41
33
  const { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
42
34
  const BANNER = `
43
- ${c1("███╗ ███╗")} ${c2("███████╗")} ${c3("██╗")} ${c4("█████╗")} ${c5("██╗ ██╗")} ${c6("█████╗")}
35
+ ${c1("███╗ ███╗")} ${c2("███████╗")} ${c3("██╗")} ${c4("█████╗")} ${c5("██╗ ██╗")} ${c6("█████╗")}
44
36
  ${c1("████╗ ████║")} ${c2("██╔════╝")} ${c3("██║")} ${c4("██╔══██╗")} ${c5("╚██╗ ██╔╝")} ${c6("██╔══██╗")}
45
37
  ${c1("██╔████╔██║")} ${c2("█████╗")} ${c3("██║")} ${c4("███████║")} ${c5("╚████╔╝")} ${c6("███████║")}
46
38
  ${c1("██║╚██╔╝██║")} ${c2("██╔══╝")} ${c3("██║")} ${c4("██╔══██║")} ${c5("╚██╔╝")} ${c6("██╔══██║")}
@@ -51,6 +43,14 @@ const BANNER = `
51
43
  ${br("─────────────────────────────────────────────────────")}
52
44
  `;
53
45
  async function main() {
46
+ const program = new Command()
47
+ .name("melaya-runner")
48
+ .description("Run Melaya AI pipelines locally with your own models")
49
+ .requiredOption("--token <token>", "Runner token from the Melaya platform")
50
+ .option("--server <url>", "Server URL", "wss://api.melaya.org")
51
+ .option("--verbose", "Show detailed logs", false)
52
+ .parse(process.argv);
53
+ const opts = program.opts();
54
54
  console.log(BANNER);
55
55
  // Detect Python
56
56
  const python = await findPython();
@@ -63,8 +63,8 @@ async function main() {
63
63
  // Detect local models
64
64
  const models = await detectModels();
65
65
  if (models.length === 0) {
66
- console.log(chalk.yellow(" ⚠ No local models detected (LM Studio / Ollama not running, Claude Code not logged in)"));
67
- console.log(chalk.gray(" Start LM Studio or Ollama (or log in to Claude Code), then restart the runner"));
66
+ console.log(chalk.yellow(" ⚠ No local models detected (LM Studio / Ollama not running; Claude Code / Codex / GitHub Copilot not logged in)"));
67
+ console.log(chalk.gray(" Start LM Studio or Ollama, log in to Claude Code / Codex, or run `melaya-runner copilot login`, then restart the runner"));
68
68
  }
69
69
  else {
70
70
  for (const m of models) {
@@ -93,7 +93,17 @@ async function findPython() {
93
93
  }
94
94
  return null;
95
95
  }
96
- main().catch((e) => {
96
+ // `melaya-runner copilot login` — GitHub Copilot device-flow sign-in, cached
97
+ // locally. Handled before the runner arg-parse so it needs no --token.
98
+ async function bootstrap() {
99
+ if (process.argv[2] === "copilot") {
100
+ const { copilotLogin } = await import("./copilotLogin.js");
101
+ await copilotLogin();
102
+ return;
103
+ }
104
+ await main();
105
+ }
106
+ bootstrap().catch((e) => {
97
107
  console.error(chalk.red(`\n Fatal: ${e.message}\n`));
98
108
  process.exit(1);
99
109
  });
@@ -0,0 +1 @@
1
+ export declare function copilotLogin(): Promise<void>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * GitHub Copilot device-flow login for the runner.
3
+ *
4
+ * github_copilot is a runner CLI provider (like claude_code/codex): the runtime
5
+ * reads a GitHub OAuth token from disk on THIS machine. The standalone
6
+ * `@github/copilot` CLI stores its token via MSAL (encrypted, unreadable), so
7
+ * for users who don't already have a readable token (editor plugin apps.json /
8
+ * gh), this command runs GitHub's device flow ONCE and caches the durable
9
+ * `gho_` token at ~/.melaya-runner/github_copilot.json — the same file the
10
+ * runtime (model.py::_read_github_copilot_token) and detect.ts read.
11
+ *
12
+ * Nothing here touches the Melaya cloud: the token is minted against GitHub and
13
+ * stored locally. Run: `npx @melaya/runner copilot login`.
14
+ */
15
+ import { writeFile, mkdir } from "fs/promises";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+ import chalk from "chalk";
19
+ const CLIENT_ID = "Iv1.b507a08c87ecfe98"; // the well-known Copilot (copilot.vim) device-flow client id
20
+ const UA = "GithubCopilot/1.155.0";
21
+ export async function copilotLogin() {
22
+ console.log(chalk.cyan("\n GitHub Copilot — device sign-in\n"));
23
+ // 1) Request a device code.
24
+ const startRes = await fetch("https://github.com/login/device/code", {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": UA },
27
+ body: new URLSearchParams({ client_id: CLIENT_ID, scope: "read:user" }),
28
+ });
29
+ if (!startRes.ok) {
30
+ console.log(chalk.red(` ✗ Could not start GitHub device flow (HTTP ${startRes.status}).`));
31
+ process.exit(1);
32
+ }
33
+ const dev = (await startRes.json());
34
+ const interval = Math.max(1, Number(dev.interval) || 5);
35
+ const deadline = Date.now() + Math.max(60, Number(dev.expires_in) || 900) * 1000;
36
+ console.log(` 1. Open: ${chalk.underline(dev.verification_uri || "https://github.com/login/device")}`);
37
+ console.log(` 2. Enter: ${chalk.bold.yellow(dev.user_code)}`);
38
+ console.log(chalk.gray("\n Waiting for you to authorize…\n"));
39
+ // 2) Poll for the token.
40
+ let token = "";
41
+ while (Date.now() < deadline) {
42
+ await new Promise((r) => setTimeout(r, interval * 1000));
43
+ const pollRes = await fetch("https://github.com/login/oauth/access_token", {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": UA },
46
+ body: new URLSearchParams({
47
+ client_id: CLIENT_ID,
48
+ device_code: dev.device_code,
49
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
50
+ }),
51
+ });
52
+ const data = (await pollRes.json().catch(() => ({})));
53
+ if (data.access_token) {
54
+ token = data.access_token;
55
+ break;
56
+ }
57
+ if (data.error && data.error !== "authorization_pending" && data.error !== "slow_down") {
58
+ console.log(chalk.red(` ✗ ${data.error === "expired_token" ? "The code expired — run the command again." : data.error === "access_denied" ? "Sign-in was denied." : `Sign-in failed (${data.error}).`}`));
59
+ process.exit(1);
60
+ }
61
+ }
62
+ if (!token) {
63
+ console.log(chalk.red(" ✗ Timed out waiting for authorization. Run the command again."));
64
+ process.exit(1);
65
+ }
66
+ // 3) Confirm the token actually has a Copilot seat (exchange), then cache it.
67
+ let login = "";
68
+ try {
69
+ const ex = await fetch("https://api.github.com/copilot_internal/v2/token", {
70
+ headers: { Authorization: `token ${token}`, "User-Agent": UA, "Editor-Version": "vscode/1.95.0", "Editor-Plugin-Version": "copilot-chat/0.22.0" },
71
+ });
72
+ if (!ex.ok) {
73
+ console.log(chalk.red(` ✗ Signed in, but this GitHub account has no active Copilot subscription (HTTP ${ex.status}).`));
74
+ process.exit(1);
75
+ }
76
+ }
77
+ catch { /* transient — still cache; the runtime re-exchanges */ }
78
+ try {
79
+ const who = await fetch("https://api.github.com/user", { headers: { Authorization: `token ${token}`, "User-Agent": UA, Accept: "application/vnd.github+json" } });
80
+ if (who.ok)
81
+ login = String((await who.json()).login || "");
82
+ }
83
+ catch { /* optional */ }
84
+ const dir = join(homedir(), ".melaya-runner");
85
+ await mkdir(dir, { recursive: true });
86
+ const file = join(dir, "github_copilot.json");
87
+ await writeFile(file, JSON.stringify({ oauth_token: token, user: login, host: "github.com" }, null, 2), { mode: 0o600 });
88
+ console.log(chalk.green(`\n ✓ GitHub Copilot connected${login ? ` as ${login}` : ""}.`));
89
+ console.log(chalk.gray(` Token cached at ${file} (this machine only). Restart the runner to pick it up.\n`));
90
+ }
package/dist/detect.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface DetectedModel {
2
- provider: "lmstudio" | "ollama" | "claude_code" | "codex";
2
+ provider: "lmstudio" | "ollama" | "claude_code" | "codex" | "github_copilot";
3
3
  name: string;
4
4
  }
5
5
  export declare function detectModels(): Promise<DetectedModel[]>;
package/dist/detect.js CHANGED
@@ -8,6 +8,7 @@ import { readFile } from "fs/promises";
8
8
  import { homedir } from "os";
9
9
  import { join } from "path";
10
10
  const execFileAsync = promisify(execFile);
11
+ const COPILOT_MODELS_FALLBACK = ["gpt-4o", "gpt-4.1", "o3-mini", "claude-3.5-sonnet", "gemini-2.0-flash"];
11
12
  /** Fallbacks used ONLY when live discovery fails (offline runner, transient
12
13
  * error, missing cache). The REAL lists are fetched dynamically below so a new
13
14
  * claude/codex release is available on Melaya automatically — no code change,
@@ -136,6 +137,94 @@ async function detectCodex() {
136
137
  catch { /* malformed creds → treat as logged out */ }
137
138
  return []; // not logged in → don't advertise models
138
139
  }
140
+ /** Read the user's GitHub OAuth token for Copilot from a readable source on
141
+ * THIS machine, in priority order. The standalone `@github/copilot` CLI stores
142
+ * its token via MSAL (encrypted, unreadable), so we read the editor-plugin
143
+ * token, the gh CLI token, or a token cached by the runner's own device-flow
144
+ * login (`melaya-runner copilot login` → ~/.melaya-runner/github_copilot.json).
145
+ * Mirrors model.py::_read_github_copilot_token — keep the source list in sync. */
146
+ async function readGithubCopilotToken() {
147
+ // 1) Editor plugins (copilot.vim / JetBrains / Neovim): plaintext oauth_token.
148
+ for (const rel of [
149
+ join(homedir(), ".config", "github-copilot", "apps.json"),
150
+ join(homedir(), ".config", "github-copilot", "hosts.json"),
151
+ ]) {
152
+ try {
153
+ const j = JSON.parse(await readFile(rel, "utf8"));
154
+ for (const v of Object.values(j)) {
155
+ if (v?.oauth_token)
156
+ return v.oauth_token;
157
+ }
158
+ }
159
+ catch { /* absent → next source */ }
160
+ }
161
+ // 2) Runner-cached token from our own device-flow login.
162
+ try {
163
+ const j = JSON.parse(await readFile(join(homedir(), ".melaya-runner", "github_copilot.json"), "utf8"));
164
+ if (j?.oauth_token)
165
+ return j.oauth_token;
166
+ }
167
+ catch { /* not logged in via runner */ }
168
+ // 3) gh CLI env token (broad-scope PAT/OAuth; works for the exchange).
169
+ if (process.env.GH_TOKEN)
170
+ return process.env.GH_TOKEN;
171
+ if (process.env.GITHUB_TOKEN)
172
+ return process.env.GITHUB_TOKEN;
173
+ return "";
174
+ }
175
+ /** GitHub Copilot as a runner CLI provider: when a readable GitHub OAuth token
176
+ * is present on THIS machine, exchange it for a short-lived Copilot token and
177
+ * list the models the subscription serves, so the picker shows TRUE local
178
+ * availability (copilot pipelines execute HERE, same as claude_code/codex). */
179
+ async function detectGithubCopilot() {
180
+ const gh = await readGithubCopilotToken();
181
+ if (!gh)
182
+ return [];
183
+ try {
184
+ const ex = await fetch("https://api.github.com/copilot_internal/v2/token", {
185
+ headers: {
186
+ Authorization: `token ${gh}`,
187
+ "User-Agent": "GithubCopilot/1.155.0",
188
+ "Editor-Version": "vscode/1.95.0",
189
+ "Editor-Plugin-Version": "copilot-chat/0.22.0",
190
+ },
191
+ signal: AbortSignal.timeout(6000),
192
+ });
193
+ if (!ex.ok)
194
+ return []; // no Copilot seat / revoked → don't advertise
195
+ const exd = (await ex.json());
196
+ if (!exd.token)
197
+ return [];
198
+ const base = (exd.endpoints?.api || "https://api.githubcopilot.com").replace(/\/+$/, "");
199
+ try {
200
+ const r = await fetch(`${base}/models`, {
201
+ headers: {
202
+ Authorization: `Bearer ${exd.token}`,
203
+ "Copilot-Integration-Id": "vscode-chat",
204
+ "Editor-Version": "vscode/1.95.0",
205
+ "Editor-Plugin-Version": "copilot-chat/0.22.0",
206
+ },
207
+ signal: AbortSignal.timeout(6000),
208
+ });
209
+ if (r.ok) {
210
+ const d = (await r.json());
211
+ // Keep ONLY real chat models the agent runtime can drive (tool calling
212
+ // is required), and drop Copilot-internal routing/search/compaction ids
213
+ // and embedding models that the /models list also advertises.
214
+ const ids = (d.data ?? [])
215
+ .filter((m) => m?.capabilities?.type === "chat" && m?.capabilities?.supports?.tool_calls === true)
216
+ .map((m) => m?.id ?? "")
217
+ .filter((s) => s && !/^copilot-search|^exec-agent|^trajectory-compaction|-free-auto$|-flash-(picker|secondary|tertiary)$|embedding/i.test(s));
218
+ if (ids.length)
219
+ return ids.map((name) => ({ provider: "github_copilot", name }));
220
+ }
221
+ }
222
+ catch { /* models list failed → fallback below */ }
223
+ return COPILOT_MODELS_FALLBACK.map((name) => ({ provider: "github_copilot", name }));
224
+ }
225
+ catch { /* offline / transient → not advertised */ }
226
+ return [];
227
+ }
139
228
  export async function detectModels() {
140
229
  const models = [];
141
230
  // LM Studio — OpenAI-compatible at :1234
@@ -164,5 +253,7 @@ export async function detectModels() {
164
253
  models.push(...await detectClaudeCode());
165
254
  // OpenAI Codex CLI — subscription auth on THIS machine
166
255
  models.push(...await detectCodex());
256
+ // GitHub Copilot — GitHub OAuth token readable on THIS machine
257
+ models.push(...await detectGithubCopilot());
167
258
  return models;
168
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.27",
3
+ "version": "1.1.28",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,