@mytegroupinc/myte-core 0.0.14 → 0.0.16

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
 
@@ -73,7 +76,8 @@ Notes:
73
76
  - `sync-qaqc` keeps QAQC state in one deterministic file so the working set grows and shrinks with current active-mission reality.
74
77
  - `sync-qaqc` fully rewrites `MyteCommandCenter/data/qaqc.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
75
78
  - `feedback-sync` writes one deterministic feedback snapshot under `MyteCommandCenter/data/feedback.yml`.
76
- - `feedback-sync` includes readable PRD text inline when PRD text exists.
79
+ - `feedback-sync` keeps feedback metadata plus comment turns in `MyteCommandCenter/data/feedback.yml`.
80
+ - `feedback-sync` writes full PRD context into `MyteCommandCenter/PRD/feedback-sync/*.md` and points to those files from `feedback.yml`.
77
81
  - `feedback-sync` fully replaces the feedback-owned sync file to avoid stale local feedback noise.
78
82
  - `feedback-sync` fully rewrites `MyteCommandCenter/data/feedback.yml` on every sync and does not delete `MyteCommandCenter/data/missions/*.yml`.
79
83
  - `suggestions sync` writes one merge-safe workflow file at `MyteCommandCenter/data/mission-ops.yml`.
@@ -114,6 +118,8 @@ Deterministic `create-prd` contract:
114
118
  - Optional structured fields: `priority`, `status`, `tags`, `assigned_user_email`, `assigned_user_id`, `due_date`, `repo_name`, `repo_id`, `preview_url`, `source`.
115
119
 
116
120
  Examples:
121
+ - `npx myte ai "Explain what this project does"`
122
+ - `npx myte ai "Return a JSON object with risks and next_steps" --json-response`
117
123
  - `npx myte bootstrap`
118
124
  - `npx myte run-qaqc --mission-ids "M001,M002" --wait --sync`
119
125
  - `npx myte sync-qaqc`
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,6 +185,7 @@ 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
191
  " myte run-qaqc --mission-ids \"M001[,M002...]\" [--wait] [--sync] [--force] [--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",
@@ -246,13 +266,18 @@ function printHelp() {
246
266
  "",
247
267
  "feedback-sync contract:",
248
268
  " - Runs from the wrapper root that contains the project's configured repo folders",
249
- " - Writes open project feedback and inline PRD context into one deterministic file: MyteCommandCenter/data/feedback.yml",
269
+ " - Writes open project feedback metadata and conversation turns into MyteCommandCenter/data/feedback.yml",
270
+ " - Stores full PRD context in MyteCommandCenter/PRD/feedback-sync/*.md and points to those files from feedback.yml",
250
271
  "",
251
272
  "Options:",
252
273
  " --with-diff Include deterministic git diffs (project-scoped)",
253
274
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
254
275
  " --timeout-ms <ms> Request timeout (default: 300000)",
255
276
  " --base-url <url> API base (default: https://api.myte.dev)",
277
+ " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
278
+ " --json-response Ask the Myte AI gateway to return clean JSON only",
279
+ " --max-output-tokens Output token cap for `myte ai` simple queries",
280
+ " --temperature <num> Temperature for `myte ai` simple queries",
256
281
  " --output-dir <path> Command Center output directory (default: <wrapper-root>/MyteCommandCenter)",
257
282
  " --file <path> YAML/JSON payload file for suggestions create/revise/review",
258
283
  " --stdin Read supported command content from stdin instead of inline text or a file path",
@@ -266,7 +291,7 @@ function printHelp() {
266
291
  " --target-contact-ids Comma-separated client contact ObjectIds",
267
292
  " --status <value> Feedback status filter for feedback-sync (default: Pending)",
268
293
  " --source <value> Feedback source filter for feedback-sync",
269
- " --with-prd-text Include extracted PRD text in feedback-sync (default: on)",
294
+ " --with-prd-text Include extracted PRD text so local PRD files can be materialized during feedback-sync (default: on)",
270
295
  " --mission-ids <ids> Comma-separated mission business ids for run-qaqc (quote multi-id values on PowerShell)",
271
296
  " --actor-scope <id> Actor workspace key inside mission-ops.yml (defaults to machine-cwd slug)",
272
297
  " --wait Poll batch status until terminal completion for run-qaqc",
@@ -278,6 +303,8 @@ function printHelp() {
278
303
  "",
279
304
  "Examples:",
280
305
  " myte query \"What changed in logging?\" --with-diff",
306
+ " myte ai \"Explain what this repository does\"",
307
+ " myte ai \"Return a JSON object with risks and next_steps\" --json-response",
281
308
  " myte bootstrap",
282
309
  " myte suggestions sync",
283
310
  " myte suggestions create",
@@ -1056,6 +1083,9 @@ async function fetchFeedbackSyncSnapshot({ apiBase, key, timeoutMs, filters = {}
1056
1083
  if (filters.includePrdText !== undefined) {
1057
1084
  url.searchParams.set("include_prd_text", filters.includePrdText ? "true" : "false");
1058
1085
  }
1086
+ if (filters.includeCommentTurns !== undefined) {
1087
+ url.searchParams.set("include_comment_turns", filters.includeCommentTurns ? "true" : "false");
1088
+ }
1059
1089
  const { resp, body } = await fetchJsonWithTimeout(
1060
1090
  fetchFn,
1061
1091
  url.toString(),
@@ -1678,6 +1708,44 @@ function writeTextFile(filePath, value) {
1678
1708
  fs.writeFileSync(filePath, String(value || ""), "utf8");
1679
1709
  }
1680
1710
 
1711
+ function sanitizeFileSegment(value, fallback = "item") {
1712
+ const cleaned = String(value || "")
1713
+ .trim()
1714
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
1715
+ .replace(/\s+/g, "-")
1716
+ .replace(/-+/g, "-")
1717
+ .replace(/^\.+|\.+$/g, "")
1718
+ .replace(/^-+|-+$/g, "");
1719
+ return cleaned || fallback;
1720
+ }
1721
+
1722
+ function toPosixRelativePath(rootPath, targetPath) {
1723
+ return path.relative(rootPath, targetPath).split(path.sep).join("/");
1724
+ }
1725
+
1726
+ function ensureTrailingNewline(value) {
1727
+ const text = String(value || "");
1728
+ if (!text) return "";
1729
+ return text.endsWith("\n") ? text : `${text}\n`;
1730
+ }
1731
+
1732
+ function normalizeFeedbackConversationTurns(turns) {
1733
+ if (!Array.isArray(turns)) return [];
1734
+ return turns
1735
+ .map((turn) => {
1736
+ const content = String(turn?.content || "").trim();
1737
+ if (!content) return null;
1738
+ const normalized = {
1739
+ sender_name: firstNonEmptyString(turn?.sender_name, turn?.user_name, turn?.author_name) || "Unknown",
1740
+ content,
1741
+ };
1742
+ const createdAt = firstNonEmptyString(turn?.created_at, turn?.timestamp);
1743
+ if (createdAt) normalized.created_at = createdAt;
1744
+ return normalized;
1745
+ })
1746
+ .filter(Boolean);
1747
+ }
1748
+
1681
1749
  function readJsonFile(filePath) {
1682
1750
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
1683
1751
  try {
@@ -1895,8 +1963,11 @@ function writeQaqcSnapshot({ snapshot, wrapperRoot, outputDir }) {
1895
1963
 
1896
1964
  function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1897
1965
  const { targetRoot, dataRoot } = resolveCommandCenterRoots(wrapperRoot, outputDir);
1966
+ const prdSyncDir = path.join(targetRoot, "PRD", "feedback-sync");
1898
1967
 
1899
1968
  ensureDir(dataRoot);
1969
+ ensureDir(prdSyncDir);
1970
+ clearFileDirectory(prdSyncDir, [".md", ".markdown", ".txt"]);
1900
1971
  pruneLegacyCommandCenterArtifacts(dataRoot, { bootstrap: true, feedback: true });
1901
1972
 
1902
1973
  if (snapshot.project && typeof snapshot.project === "object") {
@@ -1904,15 +1975,58 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1904
1975
  }
1905
1976
 
1906
1977
  const items = Array.isArray(snapshot.items) ? snapshot.items : [];
1978
+ let prdFileCount = 0;
1979
+ const materializedItems = items.map((rawItem, index) => {
1980
+ const item = isPlainObject(rawItem) ? { ...rawItem } : {};
1981
+ const feedbackId = stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`);
1982
+ const conversationTurns = normalizeFeedbackConversationTurns(item.conversation_turns);
1983
+ const prdText = String(item.prd_text || "").trim();
1984
+
1985
+ let contextSource = "description_only";
1986
+ let contextNote = "No separate PRD. Use feedback_text as the context for this feedback item.";
1987
+ let prdFile = null;
1988
+
1989
+ if (prdText) {
1990
+ const prdFilename = `${sanitizeFileSegment(feedbackId, `feedback-${index + 1}`)}.md`;
1991
+ const prdPath = path.join(prdSyncDir, prdFilename);
1992
+ writeTextFile(prdPath, ensureTrailingNewline(prdText));
1993
+ prdFile = toPosixRelativePath(targetRoot, prdPath);
1994
+ contextSource = "prd_file";
1995
+ contextNote = "Full PRD context is stored in the linked file.";
1996
+ prdFileCount += 1;
1997
+ } else if (item.has_prd_text) {
1998
+ contextSource = "prd_declared_but_unavailable";
1999
+ contextNote = "A separate PRD exists for this feedback item, but readable PRD text was not included in this sync snapshot.";
2000
+ }
2001
+
2002
+ return {
2003
+ ...item,
2004
+ feedback_id: feedbackId,
2005
+ conversation_turns: conversationTurns,
2006
+ context_source: contextSource,
2007
+ context_note: contextNote,
2008
+ prd_file: prdFile,
2009
+ };
2010
+ });
2011
+ const snapshotCounts = snapshot.counts && typeof snapshot.counts === "object"
2012
+ ? { ...snapshot.counts }
2013
+ : { total_feedback: materializedItems.length };
2014
+ snapshotCounts.with_prd_files = prdFileCount;
2015
+ if (snapshotCounts.with_conversation_turns === undefined) {
2016
+ snapshotCounts.with_conversation_turns = materializedItems.filter((item) => Array.isArray(item?.conversation_turns) && item.conversation_turns.length > 0).length;
2017
+ }
2018
+
1907
2019
  const payload = scrubBootstrapValue({
1908
- schema_version: snapshot.schema_version || 1,
2020
+ schema_version: snapshot.schema_version || 2,
1909
2021
  project: snapshot.project || null,
1910
2022
  repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
1911
2023
  filters: snapshot.filters && typeof snapshot.filters === "object" ? snapshot.filters : {},
1912
- counts: snapshot.counts && typeof snapshot.counts === "object"
1913
- ? snapshot.counts
1914
- : { total_feedback: items.length },
1915
- queue: items
2024
+ counts: snapshotCounts,
2025
+ artifacts: {
2026
+ feedback_prd_root: "PRD/feedback-sync",
2027
+ with_prd_files: prdFileCount,
2028
+ },
2029
+ queue: materializedItems
1916
2030
  .filter((item) => String(item?.status || "").trim().toLowerCase() !== "resolved")
1917
2031
  .map((item, index) => ({
1918
2032
  feedback_id: stableItemId(item, ["feedback_id", "id"], `F${String(index + 1).padStart(3, "0")}`),
@@ -1920,7 +2034,11 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1920
2034
  priority: item?.priority || null,
1921
2035
  title: item?.title || null,
1922
2036
  })),
1923
- items,
2037
+ items: materializedItems.map((item) => {
2038
+ const nextItem = { ...item };
2039
+ delete nextItem.prd_text;
2040
+ return nextItem;
2041
+ }),
1924
2042
  pagination: snapshot.pagination && typeof snapshot.pagination === "object" ? snapshot.pagination : undefined,
1925
2043
  generated_at: snapshot.generated_at || null,
1926
2044
  snapshot_hash: snapshot.snapshot_hash || null,
@@ -1930,6 +2048,7 @@ function writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir }) {
1930
2048
  return {
1931
2049
  targetRoot,
1932
2050
  dataRoot,
2051
+ prdSyncDir,
1933
2052
  manifest: payload,
1934
2053
  };
1935
2054
  }
@@ -2998,6 +3117,7 @@ async function runFeedbackSync(args) {
2998
3117
  status: firstNonEmptyString(args.status) || "Pending",
2999
3118
  source: firstNonEmptyString(args.source) || "",
3000
3119
  includePrdText,
3120
+ includeCommentTurns: true,
3001
3121
  };
3002
3122
 
3003
3123
  let snapshot;
@@ -3053,7 +3173,7 @@ async function runFeedbackSync(args) {
3053
3173
  console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
3054
3174
  console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
3055
3175
  if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
3056
- console.log(`Counts: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}`);
3176
+ console.log(`Counts: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}, with_conversation_turns=${summary.counts.with_conversation_turns || 0}`);
3057
3177
  console.log("Dry run only - no files written.");
3058
3178
  }
3059
3179
  return;
@@ -3061,6 +3181,9 @@ async function runFeedbackSync(args) {
3061
3181
 
3062
3182
  const writeResult = writeFeedbackSnapshot({ snapshot, wrapperRoot, outputDir });
3063
3183
  summary.data_root = writeResult.dataRoot;
3184
+ summary.prd_root = writeResult.prdSyncDir;
3185
+ summary.counts = writeResult.manifest?.counts || summary.counts;
3186
+ summary.artifacts = writeResult.manifest?.artifacts || null;
3064
3187
 
3065
3188
  if (args.json) {
3066
3189
  console.log(JSON.stringify(summary, null, 2));
@@ -3073,7 +3196,8 @@ async function runFeedbackSync(args) {
3073
3196
  console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
3074
3197
  console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
3075
3198
  if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
3076
- console.log(`Wrote feedback: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}`);
3199
+ console.log(`Wrote feedback: total_feedback=${summary.counts.total_feedback || 0}, with_prd_text=${summary.counts.with_prd_text || 0}, with_conversation_turns=${summary.counts.with_conversation_turns || 0}`);
3200
+ console.log(`PRD root: ${summary.prd_root}`);
3077
3201
  console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
3078
3202
  }
3079
3203
 
@@ -3509,6 +3633,111 @@ async function runSuggestions(args) {
3509
3633
  process.exit(1);
3510
3634
  }
3511
3635
 
3636
+ async function runAi(args) {
3637
+ const key = getMyteAiKey(process.env);
3638
+ if (!key) {
3639
+ console.error("Missing MYTEAI_API_KEY in environment/.env");
3640
+ process.exit(1);
3641
+ }
3642
+
3643
+ const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
3644
+ const timeoutMs = resolveTimeoutMs(args);
3645
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTEAI_API_BASE || process.env.MYTE_AI_API_BASE || DEFAULT_MYTEAI_BASE;
3646
+ const apiBase = normalizeMyteAiBase(baseRaw);
3647
+ const jsonResponse = Boolean(args["json-response"] || args.jsonResponse || args.json_response);
3648
+ const payloadFile = firstNonEmptyString(args["payload-file"], args.payloadFile, args.payload_file);
3649
+
3650
+ let payload;
3651
+ if (payloadFile) {
3652
+ const absPath = resolveInputFile(payloadFile, "AI payload");
3653
+ try {
3654
+ payload = JSON.parse(String(fs.readFileSync(absPath, "utf8") || ""));
3655
+ } catch (err) {
3656
+ console.error(`AI payload must be valid JSON: ${err?.message || err}`);
3657
+ process.exit(1);
3658
+ }
3659
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
3660
+ console.error("AI payload file must contain one JSON object.");
3661
+ process.exit(1);
3662
+ }
3663
+ if (jsonResponse && payload.myte_json_response === undefined) {
3664
+ payload.myte_json_response = true;
3665
+ }
3666
+ } else {
3667
+ const inlineQuery = firstNonEmptyString(args.query, args.q, Array.isArray(args._) ? args._.join(" ") : args._);
3668
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !inlineQuery));
3669
+ let query = inlineQuery;
3670
+ if (useStdin) {
3671
+ query = String((await readStdinText()) || "").trim();
3672
+ }
3673
+ if (!query) {
3674
+ console.error("Missing AI query text.");
3675
+ printHelp();
3676
+ process.exit(1);
3677
+ }
3678
+ payload = buildSimpleAiPayload({
3679
+ query,
3680
+ jsonResponse,
3681
+ maxOutputTokens: firstNonEmptyString(args["max-output-tokens"], args.maxOutputTokens, args.max_output_tokens, args["max-tokens"], args.maxTokens, args.max_tokens),
3682
+ temperature: firstNonEmptyString(args.temperature),
3683
+ });
3684
+ }
3685
+
3686
+ if (printContext) {
3687
+ console.log(JSON.stringify(payload, null, 2));
3688
+ return;
3689
+ }
3690
+
3691
+ const fetchFn = await getFetch();
3692
+ let responseBody;
3693
+ try {
3694
+ responseBody = await callMyteAiChat({
3695
+ fetchFn,
3696
+ apiBase,
3697
+ apiKey: key,
3698
+ payload,
3699
+ timeoutMs,
3700
+ });
3701
+ } catch (err) {
3702
+ if (err?.name === "AbortError") {
3703
+ console.error(`Request timed out after ${timeoutMs}ms`);
3704
+ } else {
3705
+ console.error("Myte AI request failed:", err?.message || err);
3706
+ }
3707
+ process.exit(1);
3708
+ }
3709
+
3710
+ let content = extractAssistantTextFromChatCompletion(responseBody);
3711
+ let normalizedJson = null;
3712
+ if (jsonResponse) {
3713
+ try {
3714
+ normalizedJson = normalizeJsonAssistantText(content);
3715
+ content = normalizedJson.text;
3716
+ } catch {
3717
+ content = String(content || "").trim();
3718
+ }
3719
+ }
3720
+
3721
+ if (args.json) {
3722
+ console.log(
3723
+ JSON.stringify(
3724
+ {
3725
+ id: responseBody.id || null,
3726
+ model: responseBody.model || null,
3727
+ usage: responseBody.usage || null,
3728
+ content,
3729
+ parsed_json: normalizedJson ? normalizedJson.parsed : null,
3730
+ },
3731
+ null,
3732
+ 2
3733
+ )
3734
+ );
3735
+ return;
3736
+ }
3737
+
3738
+ console.log(content || "");
3739
+ }
3740
+
3512
3741
  async function runQuery(args) {
3513
3742
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
3514
3743
  if (!key) {
@@ -3629,6 +3858,11 @@ async function main() {
3629
3858
  return;
3630
3859
  }
3631
3860
 
3861
+ if (command === "ai") {
3862
+ await runAi(args);
3863
+ return;
3864
+ }
3865
+
3632
3866
  if (command === "bootstrap") {
3633
3867
  await runBootstrap(args);
3634
3868
  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.14",
4
- "description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
3
+ "version": "0.0.16",
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": {