@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 +7 -1
- package/cli.js +245 -11
- package/lib/ai-gateway.js +112 -0
- package/package.json +3 -2
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`
|
|
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
|
|
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
|
|
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 ||
|
|
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:
|
|
1913
|
-
|
|
1914
|
-
:
|
|
1915
|
-
|
|
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.
|
|
4
|
-
"description": "Myte CLI core implementation (Project Assistant +
|
|
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": {
|