@mytegroupinc/myte-core 0.0.13 → 0.0.15

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 CHANGED
@@ -3,6 +3,8 @@
3
3
  Internal implementation package for the `myte` CLI.
4
4
 
5
5
  Most users should install the unscoped wrapper instead:
6
+ - `npm install myte` then `npx myte ai "Explain this repository"`
7
+ - `npm install myte` then `npx myte ai "Return a JSON object with risks and next_steps" --json-response`
6
8
  - `npm install myte` then `npx myte bootstrap`
7
9
  - `npm install myte` then `npx myte run-qaqc --mission-ids M001 --wait --sync`
8
10
  - `npm install myte` then `npx myte sync-qaqc`
@@ -48,6 +50,7 @@ Requirements:
48
50
  - Node `18+`
49
51
  - macOS, Linux, or Windows
50
52
  - `git` in `PATH` for `--with-diff`
53
+ - `MYTEAI_API_KEY=<inference_api_key>` in env or `.env` for `myte ai`
51
54
  - `MYTE_API_KEY=<project_api_key>` in env or `.env`
52
55
  - repo folder names must match the project repo names configured in Myte, including casing on case-sensitive filesystems
53
56
 
@@ -62,6 +65,7 @@ Notes:
62
65
  - `bootstrap` excludes internal keys like `_id`, `org_id`, `project_id`, `created_by`, `assigned_to`, and raw `qa_qc_results`.
63
66
  - rerunning current commands on an older workspace automatically prunes legacy artifacts like `bootstrap-manifest.json`, `data/qaqc/`, and `data/feedback/` as the new files are written.
64
67
  - `run-qaqc` queues QAQC for up to 10 explicit mission ids through `/api/project-assistant/run-qaqc`.
68
+ - On PowerShell, quote comma-separated multi-id values: `--mission-ids "M001,M002"`.
65
69
  - `run-qaqc --wait` polls `/api/project-assistant/run-qaqc/<batch_id>` until the batch is terminal.
66
70
  - `run-qaqc --sync` refreshes `MyteCommandCenter/data/qaqc.yml` after a completed batch.
67
71
  - project-key QAQC runs through the dedicated `project_api_qaqc` queue inside the existing Celery service, with a global budget of `20` dispatch starts/minute and `20` live jobs.
@@ -95,6 +99,7 @@ Notes:
95
99
  - `update-client` creates a client update draft through `/api/project-assistant/client-update-drafts`.
96
100
  - `update-client` requires `--subject` plus body markdown from `--body-markdown`, `--body-file`, positional input, or `--stdin`.
97
101
  - `update-client` accepts optional `--target-contact-id` repeats or `--target-contact-ids <id1,id2>`.
102
+ - If no linked client contacts exist, the backend falls back to the project owner for internal projects.
98
103
  - `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
99
104
  - `--with-diff` includes per-repo diagnostics in `print-context` payload:
100
105
  - missing repo directories
@@ -112,8 +117,10 @@ Deterministic `create-prd` contract:
112
117
  - Optional structured fields: `priority`, `status`, `tags`, `assigned_user_email`, `assigned_user_id`, `due_date`, `repo_name`, `repo_id`, `preview_url`, `source`.
113
118
 
114
119
  Examples:
120
+ - `npx myte ai "Explain what this project does"`
121
+ - `npx myte ai "Return a JSON object with risks and next_steps" --json-response`
115
122
  - `npx myte bootstrap`
116
- - `npx myte run-qaqc --mission-ids M001,M002 --wait --sync`
123
+ - `npx myte run-qaqc --mission-ids "M001,M002" --wait --sync`
117
124
  - `npx myte sync-qaqc`
118
125
  - `npx myte feedback-sync`
119
126
  - `npx myte suggestions sync`
package/cli.js CHANGED
@@ -11,6 +11,15 @@ const fs = require("fs");
11
11
  const path = require("path");
12
12
  const { createHash } = require("crypto");
13
13
  const { spawnSync } = require("child_process");
14
+ const {
15
+ DEFAULT_MYTEAI_BASE,
16
+ buildSimpleAiPayload,
17
+ callMyteAiChat,
18
+ extractAssistantTextFromChatCompletion,
19
+ getMyteAiKey,
20
+ normalizeJsonAssistantText,
21
+ normalizeMyteAiBase,
22
+ } = require("./lib/ai-gateway");
14
23
 
15
24
  const DEFAULT_API_BASE = "https://api.myte.dev";
16
25
  const REMOVED_COMMAND_MESSAGES = {
@@ -59,6 +68,7 @@ function loadEnv() {
59
68
  function splitCommand(argv) {
60
69
  const known = new Set([
61
70
  "query",
71
+ "ai",
62
72
  "ask",
63
73
  "chat",
64
74
  "config",
@@ -91,10 +101,11 @@ function parseArgs(argv) {
91
101
  try {
92
102
  // eslint-disable-next-line global-require
93
103
  const parsed = require("minimist")(argv, {
94
- boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin", "with-prd-text", "wait", "sync", "force"],
104
+ boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin", "with-prd-text", "wait", "sync", "force", "json-response"],
95
105
  string: [
96
106
  "query",
97
107
  "q",
108
+ "payload-file",
98
109
  "context",
99
110
  "ctx",
100
111
  "file",
@@ -102,6 +113,9 @@ function parseArgs(argv) {
102
113
  "base-url",
103
114
  "timeout-ms",
104
115
  "diff-limit",
116
+ "max-output-tokens",
117
+ "max-tokens",
118
+ "temperature",
105
119
  "title",
106
120
  "description",
107
121
  "feedback-text",
@@ -171,9 +185,10 @@ function printHelp() {
171
185
  "",
172
186
  "Usage:",
173
187
  " myte query \"<text>\" [--with-diff] [--context \"...\"]",
188
+ " myte ai \"<text>\" [--json-response] [--max-output-tokens 500]",
174
189
  " myte config [--json]",
175
190
  " myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
176
- " myte run-qaqc --mission-ids M001[,M002...] [--wait] [--sync] [--force] [--json]",
191
+ " myte run-qaqc --mission-ids \"M001[,M002...]\" [--wait] [--sync] [--force] [--json]",
177
192
  " myte sync-qaqc [--output-dir ./MyteCommandCenter] [--json]",
178
193
  " myte suggestions sync [--output-dir ./MyteCommandCenter] [--json]",
179
194
  " myte suggestions create [--file ./payload.yml] [--no-sync] [--json]",
@@ -190,12 +205,17 @@ function printHelp() {
190
205
  "",
191
206
  "Run forms:",
192
207
  " npm install myte then npx myte query \"...\" --with-diff",
208
+ " npm install myte then npx myte ai \"Explain this code path\"",
193
209
  " npm install myte then npm exec myte -- query \"...\" --with-diff",
210
+ " npm install myte then npm exec myte -- ai \"Return JSON only\" --json-response",
194
211
  " npm i -g myte then myte query \"...\" --with-diff",
212
+ " npm i -g myte then myte ai \"Summarize this file\"",
195
213
  " npx myte@latest query \"What changed in logging?\" --with-diff",
214
+ " npx myte@latest ai \"Return a JSON checklist\" --json-response",
196
215
  "",
197
216
  "Auth:",
198
217
  " - Set MYTE_API_KEY in a workspace .env (or env var)",
218
+ " - Set MYTEAI_API_KEY in a workspace .env (or env var) for `myte ai`",
199
219
  "",
200
220
  "bootstrap contract:",
201
221
  " - Run from the wrapper root that contains the project's configured repo folders",
@@ -219,6 +239,7 @@ function printHelp() {
219
239
  "run-qaqc contract:",
220
240
  " - Queues QAQC for up to 10 explicit mission business ids through /api/project-assistant/run-qaqc",
221
241
  " - Use --wait to poll batch status and --sync to refresh MyteCommandCenter/data/qaqc.yml after completion",
242
+ " - On PowerShell, quote comma-separated mission ids: --mission-ids \"M001,M002\"",
222
243
  "",
223
244
  "create-prd contract:",
224
245
  " - Required: valid MYTE_API_KEY, PRD markdown body, title",
@@ -241,6 +262,7 @@ function printHelp() {
241
262
  " - Creates a client update draft through /api/project-assistant/client-update-drafts",
242
263
  " - Required: --subject and body markdown (via --body-markdown, --body-file, positional file/text, or --stdin)",
243
264
  " - Optional: --target-contact-id <id> (repeatable) or --target-contact-ids <id1,id2>",
265
+ " - If the project has no linked client contacts, the backend falls back to the project owner for internal projects",
244
266
  "",
245
267
  "feedback-sync contract:",
246
268
  " - Runs from the wrapper root that contains the project's configured repo folders",
@@ -251,6 +273,10 @@ function printHelp() {
251
273
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
252
274
  " --timeout-ms <ms> Request timeout (default: 300000)",
253
275
  " --base-url <url> API base (default: https://api.myte.dev)",
276
+ " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
277
+ " --json-response Ask the Myte AI gateway to return clean JSON only",
278
+ " --max-output-tokens Output token cap for `myte ai` simple queries",
279
+ " --temperature <num> Temperature for `myte ai` simple queries",
254
280
  " --output-dir <path> Command Center output directory (default: <wrapper-root>/MyteCommandCenter)",
255
281
  " --file <path> YAML/JSON payload file for suggestions create/revise/review",
256
282
  " --stdin Read supported command content from stdin instead of inline text or a file path",
@@ -265,7 +291,7 @@ function printHelp() {
265
291
  " --status <value> Feedback status filter for feedback-sync (default: Pending)",
266
292
  " --source <value> Feedback source filter for feedback-sync",
267
293
  " --with-prd-text Include extracted PRD text in feedback-sync (default: on)",
268
- " --mission-ids <ids> Comma-separated mission business ids for run-qaqc",
294
+ " --mission-ids <ids> Comma-separated mission business ids for run-qaqc (quote multi-id values on PowerShell)",
269
295
  " --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
270
296
  " --wait Poll batch status until terminal completion for run-qaqc",
271
297
  " --sync After run-qaqc completes, refresh local QAQC file",
@@ -276,12 +302,14 @@ function printHelp() {
276
302
  "",
277
303
  "Examples:",
278
304
  " myte query \"What changed in logging?\" --with-diff",
305
+ " myte ai \"Explain what this repository does\"",
306
+ " myte ai \"Return a JSON object with risks and next_steps\" --json-response",
279
307
  " myte bootstrap",
280
308
  " myte suggestions sync",
281
309
  " myte suggestions create",
282
310
  " myte suggestions revise --no-sync",
283
311
  " myte suggestions review --file ./review.yml",
284
- " myte run-qaqc --mission-ids M001,M002 --wait --sync",
312
+ " myte run-qaqc --mission-ids \"M001,M002\" --wait --sync",
285
313
  " myte bootstrap --output-dir ./MyteCommandCenter",
286
314
  " myte sync-qaqc",
287
315
  " myte update-team \"Backend deploy completed; QAQC rerun queued.\"",
@@ -3507,6 +3535,111 @@ async function runSuggestions(args) {
3507
3535
  process.exit(1);
3508
3536
  }
3509
3537
 
3538
+ async function runAi(args) {
3539
+ const key = getMyteAiKey(process.env);
3540
+ if (!key) {
3541
+ console.error("Missing MYTEAI_API_KEY in environment/.env");
3542
+ process.exit(1);
3543
+ }
3544
+
3545
+ const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
3546
+ const timeoutMs = resolveTimeoutMs(args);
3547
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTEAI_API_BASE || process.env.MYTE_AI_API_BASE || DEFAULT_MYTEAI_BASE;
3548
+ const apiBase = normalizeMyteAiBase(baseRaw);
3549
+ const jsonResponse = Boolean(args["json-response"] || args.jsonResponse || args.json_response);
3550
+ const payloadFile = firstNonEmptyString(args["payload-file"], args.payloadFile, args.payload_file);
3551
+
3552
+ let payload;
3553
+ if (payloadFile) {
3554
+ const absPath = resolveInputFile(payloadFile, "AI payload");
3555
+ try {
3556
+ payload = JSON.parse(String(fs.readFileSync(absPath, "utf8") || ""));
3557
+ } catch (err) {
3558
+ console.error(`AI payload must be valid JSON: ${err?.message || err}`);
3559
+ process.exit(1);
3560
+ }
3561
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
3562
+ console.error("AI payload file must contain one JSON object.");
3563
+ process.exit(1);
3564
+ }
3565
+ if (jsonResponse && payload.myte_json_response === undefined) {
3566
+ payload.myte_json_response = true;
3567
+ }
3568
+ } else {
3569
+ const inlineQuery = firstNonEmptyString(args.query, args.q, Array.isArray(args._) ? args._.join(" ") : args._);
3570
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !inlineQuery));
3571
+ let query = inlineQuery;
3572
+ if (useStdin) {
3573
+ query = String((await readStdinText()) || "").trim();
3574
+ }
3575
+ if (!query) {
3576
+ console.error("Missing AI query text.");
3577
+ printHelp();
3578
+ process.exit(1);
3579
+ }
3580
+ payload = buildSimpleAiPayload({
3581
+ query,
3582
+ jsonResponse,
3583
+ maxOutputTokens: firstNonEmptyString(args["max-output-tokens"], args.maxOutputTokens, args.max_output_tokens, args["max-tokens"], args.maxTokens, args.max_tokens),
3584
+ temperature: firstNonEmptyString(args.temperature),
3585
+ });
3586
+ }
3587
+
3588
+ if (printContext) {
3589
+ console.log(JSON.stringify(payload, null, 2));
3590
+ return;
3591
+ }
3592
+
3593
+ const fetchFn = await getFetch();
3594
+ let responseBody;
3595
+ try {
3596
+ responseBody = await callMyteAiChat({
3597
+ fetchFn,
3598
+ apiBase,
3599
+ apiKey: key,
3600
+ payload,
3601
+ timeoutMs,
3602
+ });
3603
+ } catch (err) {
3604
+ if (err?.name === "AbortError") {
3605
+ console.error(`Request timed out after ${timeoutMs}ms`);
3606
+ } else {
3607
+ console.error("Myte AI request failed:", err?.message || err);
3608
+ }
3609
+ process.exit(1);
3610
+ }
3611
+
3612
+ let content = extractAssistantTextFromChatCompletion(responseBody);
3613
+ let normalizedJson = null;
3614
+ if (jsonResponse) {
3615
+ try {
3616
+ normalizedJson = normalizeJsonAssistantText(content);
3617
+ content = normalizedJson.text;
3618
+ } catch {
3619
+ content = String(content || "").trim();
3620
+ }
3621
+ }
3622
+
3623
+ if (args.json) {
3624
+ console.log(
3625
+ JSON.stringify(
3626
+ {
3627
+ id: responseBody.id || null,
3628
+ model: responseBody.model || null,
3629
+ usage: responseBody.usage || null,
3630
+ content,
3631
+ parsed_json: normalizedJson ? normalizedJson.parsed : null,
3632
+ },
3633
+ null,
3634
+ 2
3635
+ )
3636
+ );
3637
+ return;
3638
+ }
3639
+
3640
+ console.log(content || "");
3641
+ }
3642
+
3510
3643
  async function runQuery(args) {
3511
3644
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
3512
3645
  if (!key) {
@@ -3627,6 +3760,11 @@ async function main() {
3627
3760
  return;
3628
3761
  }
3629
3762
 
3763
+ if (command === "ai") {
3764
+ await runAi(args);
3765
+ return;
3766
+ }
3767
+
3630
3768
  if (command === "bootstrap") {
3631
3769
  await runBootstrap(args);
3632
3770
  return;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_MYTEAI_BASE = "https://api.myte.ai/v1";
4
+
5
+ function normalizeMyteAiBase(baseRaw) {
6
+ const baseTrim = String(baseRaw || "").trim().replace(/\/+$/, "");
7
+ const base = baseTrim || DEFAULT_MYTEAI_BASE;
8
+ return /\/v1$/i.test(base) ? base : `${base}/v1`;
9
+ }
10
+
11
+ function getMyteAiKey(env = process.env) {
12
+ return String(env.MYTEAI_API_KEY || env.MYTE_AI_API_KEY || "").trim();
13
+ }
14
+
15
+ function buildSimpleAiPayload({ query, jsonResponse = false, maxOutputTokens, temperature }) {
16
+ const payload = {
17
+ messages: [{ role: "user", content: String(query || "").trim() }],
18
+ };
19
+ if (jsonResponse) payload.myte_json_response = true;
20
+ if (Number.isFinite(Number(maxOutputTokens)) && Number(maxOutputTokens) > 0) {
21
+ payload.max_tokens = Number(maxOutputTokens);
22
+ }
23
+ if (temperature !== undefined && temperature !== null && temperature !== "") {
24
+ const parsed = Number(temperature);
25
+ if (Number.isFinite(parsed)) payload.temperature = parsed;
26
+ }
27
+ return payload;
28
+ }
29
+
30
+ function extractAssistantTextFromChatCompletion(payload) {
31
+ const choices = Array.isArray(payload?.choices) ? payload.choices : [];
32
+ const first = choices[0] && typeof choices[0] === "object" ? choices[0] : {};
33
+ const message = first.message && typeof first.message === "object" ? first.message : {};
34
+ const content = message.content;
35
+ if (typeof content === "string") return content.trim();
36
+ if (Array.isArray(content)) {
37
+ return content
38
+ .map((item) => {
39
+ if (typeof item === "string") return item;
40
+ if (!item || typeof item !== "object") return "";
41
+ if (typeof item.text === "string") return item.text;
42
+ if (typeof item.content === "string") return item.content;
43
+ return "";
44
+ })
45
+ .filter(Boolean)
46
+ .join("\n")
47
+ .trim();
48
+ }
49
+ return "";
50
+ }
51
+
52
+ function stripJsonFences(text) {
53
+ const raw = String(text || "").trim();
54
+ const match = raw.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
55
+ if (match) return String(match[1] || "").trim();
56
+ return raw;
57
+ }
58
+
59
+ function normalizeJsonAssistantText(text) {
60
+ const stripped = stripJsonFences(text);
61
+ const parsed = JSON.parse(stripped);
62
+ return {
63
+ parsed,
64
+ text: JSON.stringify(parsed, null, 2),
65
+ };
66
+ }
67
+
68
+ async function callMyteAiChat({ fetchFn, apiBase, apiKey, payload, timeoutMs }) {
69
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
70
+ const timeoutId =
71
+ controller && timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
72
+ try {
73
+ const response = await fetchFn(`${apiBase}/chat/completions`, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ Authorization: `Bearer ${apiKey}`,
78
+ },
79
+ signal: controller?.signal,
80
+ body: JSON.stringify(payload),
81
+ });
82
+ const text = await response.text();
83
+ let body;
84
+ try {
85
+ body = JSON.parse(text);
86
+ } catch (error) {
87
+ const err = new Error(`Non-JSON response (${response.status}): ${text.slice(0, 500)}`);
88
+ err.status = response.status;
89
+ throw err;
90
+ }
91
+ if (!response.ok) {
92
+ const err = new Error(body?.error?.message || body?.message || `Request failed (${response.status})`);
93
+ err.status = response.status;
94
+ err.body = body;
95
+ throw err;
96
+ }
97
+ return body;
98
+ } finally {
99
+ if (timeoutId) clearTimeout(timeoutId);
100
+ }
101
+ }
102
+
103
+ module.exports = {
104
+ DEFAULT_MYTEAI_BASE,
105
+ buildSimpleAiPayload,
106
+ callMyteAiChat,
107
+ extractAssistantTextFromChatCompletion,
108
+ getMyteAiKey,
109
+ normalizeJsonAssistantText,
110
+ normalizeMyteAiBase,
111
+ stripJsonFences,
112
+ };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.13",
4
- "description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
3
+ "version": "0.0.15",
4
+ "description": "Myte CLI core implementation (Project Assistant + Myte AI gateway).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",
7
7
  "files": [
8
8
  "README.md",
9
9
  "cli.js",
10
+ "lib",
10
11
  "package.json"
11
12
  ],
12
13
  "scripts": {