@mytegroupinc/myte-core 0.0.2

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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/cli.js +653 -0
  3. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @mytegroupinc/myte-core
2
+
3
+ Internal implementation package for the `myte` CLI.
4
+
5
+ Most users should install the unscoped wrapper instead:
6
+ - `npm i -g myte`
7
+ - `npx myte@latest query "..." --with-diff`
8
+
9
+ This package is published under the org scope for governance; the wrapper delegates here.
10
+
package/cli.js ADDED
@@ -0,0 +1,653 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Myte CLI (core)
4
+ *
5
+ * - Auth: MYTE_API_KEY (project key) from `.env` or env
6
+ * - Default API: https://api.myte.dev (override with MYTE_API_BASE or --base-url)
7
+ * - Deterministic diffs: fetch /api/project-assistant/config to get repo folder names
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { spawnSync } = require("child_process");
13
+
14
+ const DEFAULT_API_BASE = "https://api.myte.dev";
15
+
16
+ function findEnvPath(startDir) {
17
+ let cur = startDir;
18
+ for (let i = 0; i < 8; i += 1) {
19
+ const candidate = path.join(cur, ".env");
20
+ if (fs.existsSync(candidate)) return candidate;
21
+ const parent = path.dirname(cur);
22
+ if (parent === cur) break;
23
+ cur = parent;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ function loadEnv() {
29
+ const envPath = findEnvPath(process.cwd());
30
+ try {
31
+ // eslint-disable-next-line global-require
32
+ const dotenv = require("dotenv");
33
+ dotenv.config(envPath ? { path: envPath } : undefined);
34
+ return;
35
+ } catch (err) {
36
+ if (!envPath || !fs.existsSync(envPath)) return;
37
+ const content = fs.readFileSync(envPath, "utf8");
38
+ content.split(/\r?\n/).forEach((line) => {
39
+ if (!line || line.trim().startsWith("#")) return;
40
+ const idx = line.indexOf("=");
41
+ if (idx === -1) return;
42
+ const key = line.slice(0, idx).trim();
43
+ const val = line.slice(idx + 1).trim();
44
+ if (key && !(key in process.env)) {
45
+ process.env[key] = val;
46
+ }
47
+ });
48
+ }
49
+ }
50
+
51
+ function splitCommand(argv) {
52
+ const known = new Set(["query", "ask", "chat", "config", "help", "--help", "-h"]);
53
+ const first = argv[0];
54
+ if (first && known.has(first)) {
55
+ const cmd = first === "--help" || first === "-h" ? "help" : first;
56
+ return { command: cmd, rest: argv.slice(1) };
57
+ }
58
+ return { command: "query", rest: argv };
59
+ }
60
+
61
+ function parseArgs(argv) {
62
+ try {
63
+ // eslint-disable-next-line global-require
64
+ return require("minimist")(argv, {
65
+ boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json"],
66
+ string: ["query", "q", "context", "ctx", "base-url", "timeout-ms", "diff-limit"],
67
+ alias: {
68
+ q: "query",
69
+ d: "with-diff",
70
+ c: "context",
71
+ h: "help",
72
+ },
73
+ default: {
74
+ fetch: true,
75
+ },
76
+ });
77
+ } catch (err) {
78
+ const parsed = { _: [] };
79
+ for (let i = 0; i < argv.length; i += 1) {
80
+ const token = argv[i];
81
+ if (token.startsWith("--no-")) {
82
+ const key = token.slice(5);
83
+ parsed[key] = false;
84
+ continue;
85
+ }
86
+ if (token.startsWith("--")) {
87
+ const key = token.slice(2);
88
+ const next = argv[i + 1];
89
+ if (next && !next.startsWith("-")) {
90
+ parsed[key] = next;
91
+ i += 1;
92
+ } else {
93
+ parsed[key] = true;
94
+ }
95
+ } else if (token.startsWith("-")) {
96
+ const key = token.slice(1);
97
+ const next = argv[i + 1];
98
+ if (next && !next.startsWith("-")) {
99
+ parsed[key] = next;
100
+ i += 1;
101
+ } else {
102
+ parsed[key] = true;
103
+ }
104
+ } else {
105
+ parsed._.push(token);
106
+ }
107
+ }
108
+ return parsed;
109
+ }
110
+ }
111
+
112
+ function printHelp() {
113
+ const text = [
114
+ "myte - Myte Project Assistant CLI",
115
+ "",
116
+ "Usage:",
117
+ " myte query \"<text>\" [--with-diff] [--context \"...\"]",
118
+ " myte config [--json]",
119
+ " myte chat",
120
+ "",
121
+ "Auth:",
122
+ " - Set MYTE_API_KEY in a workspace .env (or env var)",
123
+ "",
124
+ "Options:",
125
+ " --with-diff Include deterministic git diffs (project-scoped)",
126
+ " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
127
+ " --timeout-ms <ms> Request timeout (default: 300000)",
128
+ " --base-url <url> API base (default: https://api.myte.dev)",
129
+ " --print-context Print JSON payload and exit (no query call)",
130
+ " --no-fetch Don't git fetch origin main/master before diff",
131
+ "",
132
+ "Examples:",
133
+ " myte query \"What changed in logging?\" --with-diff",
134
+ " myte query \"...\" --with-diff --diff-limit 120000",
135
+ " myte config",
136
+ ].join("\n");
137
+ console.log(text);
138
+ }
139
+
140
+ async function getFetch() {
141
+ if (typeof fetch !== "undefined") return fetch;
142
+ const mod = await import("node-fetch");
143
+ return mod.default;
144
+ }
145
+
146
+ function normalizeApiBase(baseRaw) {
147
+ const baseTrim = String(baseRaw || "").trim().replace(/\/+$/, "");
148
+ const base = baseTrim || DEFAULT_API_BASE;
149
+ return base.endsWith("/api") ? base : `${base}/api`;
150
+ }
151
+
152
+ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
153
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
154
+ const timeoutId =
155
+ controller && timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
156
+ try {
157
+ const resp = await fetchFn(url, { ...options, signal: controller?.signal });
158
+ const text = await resp.text();
159
+ let body;
160
+ try {
161
+ body = JSON.parse(text);
162
+ } catch {
163
+ const err = new Error(`Non-JSON response (${resp.status}): ${text.slice(0, 500)}`);
164
+ err.status = resp.status;
165
+ throw err;
166
+ }
167
+ return { resp, body };
168
+ } finally {
169
+ if (timeoutId) clearTimeout(timeoutId);
170
+ }
171
+ }
172
+
173
+ function buildContext(args, diffText) {
174
+ const ctx = args.context ?? args.ctx ?? args.c;
175
+ const extra = ctx === undefined ? [] : Array.isArray(ctx) ? ctx.map(String) : [String(ctx)];
176
+ if (diffText) extra.push(`Current git diff snapshot (project-configured):\n${diffText}`);
177
+ return extra;
178
+ }
179
+
180
+ const IGNORED_PATTERNS = [
181
+ /^node_modules\//,
182
+ /\.node$/,
183
+ /package-lock\.json$/,
184
+ /yarn\.lock$/,
185
+ /pnpm-lock\.ya?ml$/,
186
+ /\.DS_Store$/,
187
+ /\.log$/,
188
+ /\.env$/,
189
+ /^\.vscode\//,
190
+ /^\.idea\//,
191
+ /\.gitignore$/,
192
+ /\.eslint/,
193
+ /^\.next\//,
194
+ /^dist\//,
195
+ /^out\//,
196
+ ];
197
+
198
+ function shouldIgnore(p) {
199
+ return IGNORED_PATTERNS.some((re) => re.test(p));
200
+ }
201
+
202
+ function hasGitDir(repoPath) {
203
+ return fs.existsSync(path.join(repoPath, ".git"));
204
+ }
205
+
206
+ function runGitDiff(repoPath, args, opts = {}) {
207
+ const res = spawnSync("git", args, {
208
+ cwd: repoPath,
209
+ encoding: "utf8",
210
+ stdio: ["ignore", "pipe", "pipe"],
211
+ maxBuffer: 64 * 1024 * 1024,
212
+ ...opts,
213
+ });
214
+ if (res.status !== 0 && res.status !== 1) {
215
+ const msg = res.stderr || res.error?.message || "unknown git error";
216
+ const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
217
+ err.code = res.status;
218
+ throw err;
219
+ }
220
+ return (res.stdout || "").trim();
221
+ }
222
+
223
+ function runGitOk(repoPath, args, opts = {}) {
224
+ const res = spawnSync("git", args, {
225
+ cwd: repoPath,
226
+ encoding: "utf8",
227
+ stdio: ["ignore", "pipe", "pipe"],
228
+ maxBuffer: 16 * 1024 * 1024,
229
+ ...opts,
230
+ });
231
+ return res.status === 0;
232
+ }
233
+
234
+ function runGitTry(repoPath, args) {
235
+ try {
236
+ return runGitDiff(repoPath, args);
237
+ } catch {
238
+ return "";
239
+ }
240
+ }
241
+
242
+ function patchPaths(diff, prefix = "") {
243
+ if (!diff) return "";
244
+ return diff
245
+ .replace(
246
+ /^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
247
+ (_, aPre, a, bPre, b) =>
248
+ `diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix + a} ${
249
+ bPre === "/dev/null" ? "/dev/null" : prefix + b
250
+ }`
251
+ )
252
+ .replace(
253
+ /^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
254
+ (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
255
+ )
256
+ .replace(
257
+ /^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
258
+ (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
259
+ );
260
+ }
261
+
262
+ function filterDiff(diff) {
263
+ if (!diff) return "";
264
+ return diff
265
+ .split(/^diff --git /gm)
266
+ .filter((block) => {
267
+ if (!block.trim()) return false;
268
+ const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
269
+ if (
270
+ m &&
271
+ (shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
272
+ ) {
273
+ return false;
274
+ }
275
+ return true;
276
+ })
277
+ .map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
278
+ .join("");
279
+ }
280
+
281
+ function resolveConfiguredRepos(repoNames) {
282
+ const names = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
283
+ if (!names.length) return { mode: "none", root: null, repos: [], missing: [] };
284
+
285
+ const cwd = process.cwd();
286
+ const cwdBase = path.basename(cwd);
287
+ const cwdIsConfiguredRepo = names.includes(cwdBase) && hasGitDir(cwd);
288
+
289
+ const ancestors = [];
290
+ const scanStart = cwdIsConfiguredRepo ? path.dirname(cwd) : cwd;
291
+ let cur = scanStart;
292
+ for (let i = 0; i < 8; i += 1) {
293
+ ancestors.push(cur);
294
+ const parent = path.dirname(cur);
295
+ if (parent === cur) break;
296
+ cur = parent;
297
+ }
298
+
299
+ for (const candidate of ancestors) {
300
+ const found = [];
301
+ for (const name of names) {
302
+ const abs = path.join(candidate, name);
303
+ if (hasGitDir(abs)) {
304
+ found.push({
305
+ name,
306
+ dir: name,
307
+ abs,
308
+ prefix: `${name.replace(/\\\\/g, "/")}/`,
309
+ });
310
+ }
311
+ }
312
+ if (found.length) {
313
+ const foundNames = new Set(found.map((r) => r.name));
314
+ const missing = names.filter((n) => !foundNames.has(n));
315
+ return { mode: "workspace", root: candidate, repos: found, missing };
316
+ }
317
+ }
318
+
319
+ if (cwdIsConfiguredRepo) {
320
+ const missing = names.filter((n) => n !== cwdBase);
321
+ return {
322
+ mode: "repo",
323
+ root: cwd,
324
+ repos: [
325
+ {
326
+ name: cwdBase,
327
+ dir: ".",
328
+ abs: cwd,
329
+ prefix: `${cwdBase.replace(/\\\\/g, "/")}/`,
330
+ },
331
+ ],
332
+ missing,
333
+ };
334
+ }
335
+
336
+ return { mode: "none", root: null, repos: [], missing: names };
337
+ }
338
+
339
+ function fetchBaseBranches(repoPath) {
340
+ if (runGitOk(repoPath, ["fetch", "origin", "main", "master", "--prune", "--quiet"])) return true;
341
+ return runGitOk(repoPath, ["fetch", "--all", "--prune", "--quiet"]);
342
+ }
343
+
344
+ function resolveBaseRef(repoPath) {
345
+ if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/remotes/origin/main"])) return "origin/main";
346
+ if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/remotes/origin/master"])) return "origin/master";
347
+ if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/heads/main"])) return "main";
348
+ if (runGitOk(repoPath, ["rev-parse", "--verify", "refs/heads/master"])) return "master";
349
+ return null;
350
+ }
351
+
352
+ function collectGitDiff({ projectId, repoNames, maxChars, fetchRemote = true } = {}) {
353
+ try {
354
+ const resolved = resolveConfiguredRepos(repoNames);
355
+ const repos = resolved.repos || [];
356
+ if (!repos.length) {
357
+ const configured = Array.isArray(repoNames) ? repoNames.join(", ") : "";
358
+ const msg = [
359
+ "[myte] No configured repos found locally.",
360
+ `Configured repos (from project ${projectId || "unknown"}): ${configured || "(none)"}`,
361
+ "Run from within a configured repo, or a parent folder that contains the repo folders by name.",
362
+ ].join("\n");
363
+ throw new Error(msg);
364
+ }
365
+
366
+ let output = "";
367
+ if (projectId) output += `# Project: ${projectId}\n`;
368
+ output += `# Mode: ${resolved.mode}\n`;
369
+ output += `# Configured repos: ${Array.isArray(repoNames) ? repoNames.join(", ") : ""}\n`;
370
+ if (resolved.missing && resolved.missing.length) {
371
+ output += `# Missing locally (skipped): ${resolved.missing.join(", ")}\n`;
372
+ }
373
+ output += "\n";
374
+
375
+ for (const { name, dir, abs, prefix } of repos) {
376
+ if (fetchRemote) fetchBaseBranches(abs);
377
+
378
+ const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
379
+ const baseRef = resolveBaseRef(abs);
380
+
381
+ const baseDiffRaw = baseRef ? runGitDiff(abs, ["diff", "--no-color", "-U2", `${baseRef}...HEAD`]) : "";
382
+ const stagedRaw = runGitDiff(abs, ["diff", "--cached", "--no-color", "-U2"]);
383
+ const unstagedRaw = runGitDiff(abs, ["diff", "--no-color", "-U2"]);
384
+
385
+ const baseDiff = patchPaths(filterDiff(baseDiffRaw), prefix);
386
+ const staged = patchPaths(filterDiff(stagedRaw), prefix);
387
+ const unstaged = patchPaths(filterDiff(unstagedRaw), prefix);
388
+
389
+ const untracked = runGitTry(abs, ["ls-files", "--others", "--exclude-standard"])
390
+ .split("\n")
391
+ .filter(Boolean)
392
+ .filter((f) => !shouldIgnore(f));
393
+
394
+ let untrackedDiffs = "";
395
+ for (const file of untracked) {
396
+ const absFile = path.join(abs, file);
397
+ if (!fs.existsSync(absFile) || !fs.statSync(absFile).isFile()) continue;
398
+ const patch = runGitDiff(abs, ["diff", "--no-index", "--no-color", "/dev/null", file]);
399
+ if (patch) untrackedDiffs += patchPaths(patch, prefix) + "\n";
400
+ }
401
+
402
+ output += `### ${name} (${dir || "."})\n\n`;
403
+ output += `# ?? Base vs HEAD (${baseRef || "base-unresolved"}...HEAD | head=${headBranch})\n`;
404
+ if (baseDiff) output += `${baseDiff}\n\n`;
405
+ if (staged) output += `# ?? Staged changes\n${staged}\n\n`;
406
+ if (unstaged) output += `# ?? Unstaged changes\n${unstaged}\n\n`;
407
+ if (untrackedDiffs) output += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
408
+
409
+ if (!baseDiff && !staged && !unstaged && !untrackedDiffs) {
410
+ output += "_No local changes - working tree clean_\n\n";
411
+ }
412
+ }
413
+
414
+ if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
415
+ return `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
416
+ }
417
+ return output.trim();
418
+ } catch (err) {
419
+ console.warn("Failed to collect git diffs:", err?.message || err);
420
+ return "";
421
+ }
422
+ }
423
+
424
+ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
425
+ const fetchFn = await getFetch();
426
+ const url = `${apiBase}/project-assistant/config`;
427
+ const { resp, body } = await fetchJsonWithTimeout(
428
+ fetchFn,
429
+ url,
430
+ {
431
+ method: "GET",
432
+ headers: { Authorization: `Bearer ${key}` },
433
+ },
434
+ timeoutMs
435
+ );
436
+
437
+ if (!resp.ok || body.status !== "success") {
438
+ const msg = body?.message || `Config request failed (${resp.status})`;
439
+ const err = new Error(msg);
440
+ err.status = resp.status;
441
+ throw err;
442
+ }
443
+ return body.data || {};
444
+ }
445
+
446
+ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
447
+ const fetchFn = await getFetch();
448
+ const url = `${apiBase}/project-assistant/query`;
449
+ const { resp, body } = await fetchJsonWithTimeout(
450
+ fetchFn,
451
+ url,
452
+ {
453
+ method: "POST",
454
+ headers: {
455
+ "Content-Type": "application/json",
456
+ Authorization: `Bearer ${key}`,
457
+ },
458
+ body: JSON.stringify(payload),
459
+ },
460
+ timeoutMs
461
+ );
462
+
463
+ if (!resp.ok || body.status !== "success") {
464
+ const msg = body?.message || `Query failed (${resp.status})`;
465
+ const err = new Error(msg);
466
+ err.status = resp.status;
467
+ throw err;
468
+ }
469
+ return body.data || {};
470
+ }
471
+
472
+ async function runConfig(args) {
473
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
474
+ if (!key) {
475
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
476
+ process.exit(1);
477
+ }
478
+
479
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
480
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
481
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
482
+
483
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
484
+ const apiBase = normalizeApiBase(baseRaw);
485
+
486
+ let cfg;
487
+ try {
488
+ cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
489
+ } catch (err) {
490
+ console.error("Failed to fetch config:", err?.message || err);
491
+ process.exit(1);
492
+ }
493
+
494
+ const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
495
+ const resolved = resolveConfiguredRepos(repoNames);
496
+ const payload = {
497
+ api_base: apiBase,
498
+ project_id: cfg.project_id,
499
+ repo_names: repoNames,
500
+ local: {
501
+ mode: resolved.mode,
502
+ root: resolved.root,
503
+ found: (resolved.repos || []).map((r) => r.name),
504
+ missing: resolved.missing || [],
505
+ },
506
+ };
507
+
508
+ if (args.json) {
509
+ console.log(JSON.stringify(payload, null, 2));
510
+ } else {
511
+ console.log(`Project: ${payload.project_id || "(unknown)"}`);
512
+ console.log(`API base: ${payload.api_base}`);
513
+ console.log(`Configured repos: ${repoNames.join(", ") || "(none)"}`);
514
+ console.log(`Local mode: ${payload.local.mode}`);
515
+ if (payload.local.found.length) console.log(`Found locally: ${payload.local.found.join(", ")}`);
516
+ if (payload.local.missing.length) console.log(`Missing locally: ${payload.local.missing.join(", ")}`);
517
+ }
518
+ }
519
+
520
+ async function runQuery(args) {
521
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
522
+ if (!key) {
523
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
524
+ process.exit(1);
525
+ }
526
+
527
+ const query = args.query || args.q || (Array.isArray(args._) ? args._.join(" ").trim() : "");
528
+ if (!query) {
529
+ console.error("Missing query text.");
530
+ printHelp();
531
+ process.exit(1);
532
+ }
533
+
534
+ const includeDiff = Boolean(args["with-diff"] || args.withDiff || args.diff || args.d);
535
+ const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
536
+
537
+ const fetchRemote = args.fetch !== undefined ? Boolean(args.fetch) : true;
538
+
539
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
540
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
541
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
542
+
543
+ const charLimitRaw = args["diff-limit"] || args.diffLimit || args.diff_limit;
544
+ const charLimitParsed = charLimitRaw !== undefined && charLimitRaw !== null ? Number(charLimitRaw) : 200_000;
545
+ const diffLimit = Number.isFinite(charLimitParsed) ? charLimitParsed : 200_000;
546
+
547
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
548
+ const apiBase = normalizeApiBase(baseRaw);
549
+
550
+ let diffText = "";
551
+ if (includeDiff) {
552
+ let cfg;
553
+ try {
554
+ cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
555
+ } catch (err) {
556
+ console.error("Failed to fetch project config (needed for diffs):", err?.message || err);
557
+ process.exit(1);
558
+ }
559
+ const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
560
+ diffText = collectGitDiff({
561
+ projectId: cfg.project_id,
562
+ repoNames,
563
+ maxChars: diffLimit,
564
+ fetchRemote,
565
+ });
566
+ if (!diffText) {
567
+ console.error("No diff context collected. Ensure you're running from the workspace root (or within a configured repo).");
568
+ process.exit(1);
569
+ }
570
+ }
571
+
572
+ const payload = {
573
+ query,
574
+ additional_context: buildContext(args, diffText),
575
+ };
576
+
577
+ if (printContext) {
578
+ console.log(JSON.stringify(payload, null, 2));
579
+ process.exit(0);
580
+ }
581
+
582
+ let data;
583
+ try {
584
+ data = await callAssistantQuery({ apiBase, key, payload, timeoutMs });
585
+ } catch (err) {
586
+ if (err?.name === "AbortError") {
587
+ console.error(`Request timed out after ${timeoutMs}ms`);
588
+ } else {
589
+ console.error("Assistant query failed:", err?.message || err);
590
+ }
591
+ process.exit(1);
592
+ }
593
+
594
+ console.log("Answer:\n", data.answer || "(no answer)");
595
+ if (data.context_blocks?.length) console.log(`\nContext blocks: ${data.context_blocks.length}`);
596
+ if (data.telemetry) {
597
+ const t = data.telemetry;
598
+ const usage = t.usage || {};
599
+ console.log(`\nTelemetry: model=${t.model || "n/a"}, rag_used=${t.rag_used}, tokens_total=${usage.total_tokens || "n/a"}`);
600
+ }
601
+ }
602
+
603
+ async function runChat(args) {
604
+ const readline = require("readline");
605
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
606
+ console.log("myte chat (Ctrl+C to exit)");
607
+ rl.setPrompt("> ");
608
+ rl.prompt();
609
+
610
+ rl.on("line", async (line) => {
611
+ const text = String(line || "").trim();
612
+ if (!text) {
613
+ rl.prompt();
614
+ return;
615
+ }
616
+ try {
617
+ await runQuery({ ...args, _: [text] });
618
+ } catch (err) {
619
+ console.error(err?.message || err);
620
+ }
621
+ rl.prompt();
622
+ });
623
+ }
624
+
625
+ async function main() {
626
+ loadEnv();
627
+
628
+ const { command, rest } = splitCommand(process.argv.slice(2));
629
+ const args = parseArgs(rest);
630
+ if (args.help || command === "help") {
631
+ printHelp();
632
+ return;
633
+ }
634
+
635
+ if (command === "config") {
636
+ await runConfig(args);
637
+ return;
638
+ }
639
+
640
+ if (command === "chat") {
641
+ await runChat(args);
642
+ return;
643
+ }
644
+
645
+ // query/ask default
646
+ await runQuery(args);
647
+ }
648
+
649
+ main().catch((err) => {
650
+ console.error("Unexpected error:", err?.message || err);
651
+ process.exit(1);
652
+ });
653
+
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@mytegroupinc/myte-core",
3
+ "version": "0.0.2",
4
+ "description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
5
+ "type": "commonjs",
6
+ "main": "cli.js",
7
+ "license": "MIT",
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "dependencies": {
15
+ "dotenv": "^16.5.0",
16
+ "minimist": "^1.2.8",
17
+ "node-fetch": "^3.3.2"
18
+ }
19
+ }
20
+