@mytegroupinc/myte-core 0.0.2 → 0.0.4

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 +23 -2
  2. package/cli.js +449 -75
  3. package/package.json +9 -2
package/README.md CHANGED
@@ -3,8 +3,29 @@
3
3
  Internal implementation package for the `myte` CLI.
4
4
 
5
5
  Most users should install the unscoped wrapper instead:
6
- - `npm i -g myte`
6
+ - `npm install myte` then `npx myte query "..." --with-diff`
7
+ - `npm install myte` then `npm exec myte -- query "..." --with-diff`
8
+ - `npm i -g myte` then `myte query "..." --with-diff`
7
9
  - `npx myte@latest query "..." --with-diff`
10
+ - `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
11
+ - `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
8
12
 
9
- This package is published under the org scope for governance; the wrapper delegates here.
13
+ Requirements:
14
+ - Node `18+`
15
+ - macOS, Linux, or Windows
16
+ - `git` in `PATH` for `--with-diff`
17
+ - `MYTE_API_KEY=<project_api_key>` in env or `.env`
18
+ - repo folder names must match the project repo names configured in Myte, including casing on case-sensitive filesystems
10
19
 
20
+ Notes:
21
+ - `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
22
+ - `npm install myte` means the CLI is available locally; bare `myte ...` still requires a global install.
23
+ - `create-prd` is a deterministic PRD upload path, not an LLM generation command.
24
+ - `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
25
+ - `--with-diff` includes per-repo diagnostics in `print-context` payload:
26
+ - missing repo directories
27
+ - per-repo errors (for example fetch or command failures)
28
+ - clean/no-change repo summaries
29
+ - `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
30
+
31
+ This package is published under the org scope for governance; the public `myte` wrapper delegates here.
package/cli.js CHANGED
@@ -49,7 +49,7 @@ function loadEnv() {
49
49
  }
50
50
 
51
51
  function splitCommand(argv) {
52
- const known = new Set(["query", "ask", "chat", "config", "help", "--help", "-h"]);
52
+ const known = new Set(["query", "ask", "chat", "config", "create-prd", "add-prd", "prd", "help", "--help", "-h"]);
53
53
  const first = argv[0];
54
54
  if (first && known.has(first)) {
55
55
  const cmd = first === "--help" || first === "-h" ? "help" : first;
@@ -62,8 +62,8 @@ function parseArgs(argv) {
62
62
  try {
63
63
  // eslint-disable-next-line global-require
64
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"],
65
+ boolean: ["with-diff", "diff", "print-context", "dry-run", "fetch", "json", "stdin"],
66
+ string: ["query", "q", "context", "ctx", "base-url", "timeout-ms", "diff-limit", "title"],
67
67
  alias: {
68
68
  q: "query",
69
69
  d: "with-diff",
@@ -117,6 +117,15 @@ function printHelp() {
117
117
  " myte query \"<text>\" [--with-diff] [--context \"...\"]",
118
118
  " myte config [--json]",
119
119
  " myte chat",
120
+ " myte create-prd <file.md> [--json]",
121
+ " myte add-prd <file.md> [--json]",
122
+ " cat file.md | myte create-prd --stdin [--title \"...\"]",
123
+ "",
124
+ "Run forms:",
125
+ " npm install myte then npx myte query \"...\" --with-diff",
126
+ " npm install myte then npm exec myte -- query \"...\" --with-diff",
127
+ " npm i -g myte then myte query \"...\" --with-diff",
128
+ " npx myte@latest query \"What changed in logging?\" --with-diff",
120
129
  "",
121
130
  "Auth:",
122
131
  " - Set MYTE_API_KEY in a workspace .env (or env var)",
@@ -126,17 +135,49 @@ function printHelp() {
126
135
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
127
136
  " --timeout-ms <ms> Request timeout (default: 300000)",
128
137
  " --base-url <url> API base (default: https://api.myte.dev)",
138
+ " --stdin Read PRD content from stdin instead of a file path",
139
+ " --title <text> Required only when stdin/raw markdown has no H1 title",
129
140
  " --print-context Print JSON payload and exit (no query call)",
130
141
  " --no-fetch Don't git fetch origin main/master before diff",
131
142
  "",
132
143
  "Examples:",
133
144
  " myte query \"What changed in logging?\" --with-diff",
134
- " myte query \"...\" --with-diff --diff-limit 120000",
145
+ " myte create-prd ./drafts/auth-prd.md",
146
+ " cat ./drafts/auth-prd.md | myte create-prd --stdin",
135
147
  " myte config",
136
148
  ].join("\n");
137
149
  console.log(text);
138
150
  }
139
151
 
152
+ function extractMarkdownTitle(text) {
153
+ const raw = String(text || "");
154
+ const lines = raw.split(/\r?\n/);
155
+ for (const rawLine of lines) {
156
+ const line = rawLine.trim();
157
+ if (line.startsWith("#")) {
158
+ const title = line.replace(/^#+\s*/, "").trim();
159
+ if (title) return title;
160
+ }
161
+ }
162
+ return "";
163
+ }
164
+
165
+ function isMyteKanbanTicket(text) {
166
+ return /^\s*```myte-kanban\s*\{[\s\S]*?\}\s*```\s*/.test(String(text || ""));
167
+ }
168
+
169
+ async function readStdinText() {
170
+ return new Promise((resolve, reject) => {
171
+ let data = "";
172
+ process.stdin.setEncoding("utf8");
173
+ process.stdin.on("data", (chunk) => {
174
+ data += chunk;
175
+ });
176
+ process.stdin.on("end", () => resolve(data));
177
+ process.stdin.on("error", reject);
178
+ });
179
+ }
180
+
140
181
  async function getFetch() {
141
182
  if (typeof fetch !== "undefined") return fetch;
142
183
  const mod = await import("node-fetch");
@@ -170,10 +211,47 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
170
211
  }
171
212
  }
172
213
 
173
- function buildContext(args, diffText) {
214
+ function summarizeDiffDiagnosticsForContext(diagnostics) {
215
+ if (!diagnostics) return null;
216
+ const repos = Array.isArray(diagnostics.repo_summaries)
217
+ ? diagnostics.repo_summaries
218
+ : [];
219
+ return {
220
+ project_id: diagnostics.project_id || null,
221
+ mode: diagnostics.mode,
222
+ requested_repos: diagnostics.requested_repo_names || [],
223
+ found_repos: diagnostics.found_repos || [],
224
+ missing_repos: diagnostics.missing_repos || [],
225
+ collected_any: Boolean(diagnostics.collected_any),
226
+ truncation: diagnostics.truncated ? "truncated" : "full",
227
+ repos: repos.map((repo) => ({
228
+ name: repo.name,
229
+ status: repo.status || "ok",
230
+ head_branch: repo.head_branch || null,
231
+ base_ref: repo.base_ref || null,
232
+ has_changes: Boolean(repo.has_changes),
233
+ changed_blocks: repo.changed_blocks || {},
234
+ untracked_file_count: repo.untracked_file_count || 0,
235
+ error_count: Array.isArray(repo.errors) ? repo.errors.length : 0,
236
+ })),
237
+ warnings: diagnostics.warnings || [],
238
+ errors: diagnostics.errors || [],
239
+ };
240
+ }
241
+
242
+ function formatDiffDiagnosticText(diagnostics) {
243
+ const summary = summarizeDiffDiagnosticsForContext(diagnostics);
244
+ if (!summary) return "";
245
+ return `Git diff diagnostics:\n${JSON.stringify(summary, null, 2)}`;
246
+ }
247
+
248
+ function buildContext(args, diffText, diffDiagnostics) {
174
249
  const ctx = args.context ?? args.ctx ?? args.c;
175
250
  const extra = ctx === undefined ? [] : Array.isArray(ctx) ? ctx.map(String) : [String(ctx)];
176
251
  if (diffText) extra.push(`Current git diff snapshot (project-configured):\n${diffText}`);
252
+ if (diffDiagnostics) {
253
+ extra.push(formatDiffDiagnosticText(diffDiagnostics));
254
+ }
177
255
  return extra;
178
256
  }
179
257
 
@@ -203,7 +281,7 @@ function hasGitDir(repoPath) {
203
281
  return fs.existsSync(path.join(repoPath, ".git"));
204
282
  }
205
283
 
206
- function runGitDiff(repoPath, args, opts = {}) {
284
+ function runGitRaw(repoPath, args, opts = {}) {
207
285
  const res = spawnSync("git", args, {
208
286
  cwd: repoPath,
209
287
  encoding: "utf8",
@@ -211,13 +289,43 @@ function runGitDiff(repoPath, args, opts = {}) {
211
289
  maxBuffer: 64 * 1024 * 1024,
212
290
  ...opts,
213
291
  });
292
+ if (res.error && res.error.code === "ENOENT") {
293
+ const err = new Error("git executable not found in PATH. Install Git and ensure PATH includes git.");
294
+ err.code = "ENOENT";
295
+ return { ok: false, status: null, stdout: "", stderr: "", error: err };
296
+ }
214
297
  if (res.status !== 0 && res.status !== 1) {
215
298
  const msg = res.stderr || res.error?.message || "unknown git error";
216
299
  const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
217
300
  err.code = res.status;
218
- throw err;
301
+ return { ok: false, status: res.status, stdout: "", stderr: msg, error: err };
302
+ }
303
+ return { ok: true, status: res.status, stdout: (res.stdout || "").trim(), stderr: res.stderr || "" };
304
+ }
305
+
306
+ function runGitDiff(repoPath, args, opts = {}) {
307
+ const result = runGitRaw(repoPath, args, opts);
308
+ if (!result.ok) {
309
+ throw result.error;
310
+ }
311
+ return result.stdout;
312
+ }
313
+
314
+ function runGitDiffResult(repoPath, args, opts = {}) {
315
+ return runGitRaw(repoPath, args, opts);
316
+ }
317
+
318
+ function toPatchWithPrefix(diffText, prefix) {
319
+ if (!diffText) return "";
320
+ return patchPaths(diffText, prefix);
321
+ }
322
+
323
+ function safeText(value) {
324
+ try {
325
+ return String(value || "");
326
+ } catch (err) {
327
+ return "";
219
328
  }
220
- return (res.stdout || "").trim();
221
329
  }
222
330
 
223
331
  function runGitOk(repoPath, args, opts = {}) {
@@ -225,7 +333,7 @@ function runGitOk(repoPath, args, opts = {}) {
225
333
  cwd: repoPath,
226
334
  encoding: "utf8",
227
335
  stdio: ["ignore", "pipe", "pipe"],
228
- maxBuffer: 16 * 1024 * 1024,
336
+ maxBuffer: 64 * 1024 * 1024,
229
337
  ...opts,
230
338
  });
231
339
  return res.status === 0;
@@ -239,33 +347,54 @@ function runGitTry(repoPath, args) {
239
347
  }
240
348
  }
241
349
 
350
+ function runGitTryWithResult(repoPath, args, opts = {}) {
351
+ const result = runGitDiffResult(repoPath, args, opts);
352
+ if (!result.ok) {
353
+ return {
354
+ ok: false,
355
+ output: "",
356
+ error: safeText(result.error?.message),
357
+ status: result.status,
358
+ };
359
+ }
360
+ return { ok: true, output: result.stdout, error: "", status: result.status };
361
+ }
362
+
363
+ function runGitSafeDiff(repoPath, args, prefix) {
364
+ const patchText = runGitTry(repoPath, args);
365
+ if (!patchText) {
366
+ return "";
367
+ }
368
+ return patchPaths(patchText, prefix);
369
+ }
370
+
242
371
  function patchPaths(diff, prefix = "") {
243
372
  if (!diff) return "";
244
373
  return diff
245
374
  .replace(
246
375
  /^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
247
376
  (_, aPre, a, bPre, b) =>
248
- `diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix + a} ${
249
- bPre === "/dev/null" ? "/dev/null" : prefix + b
377
+ `diff --git ${aPre === "/dev/null" ? "/dev/null" : `${aPre}${prefix}${a}`} ${
378
+ bPre === "/dev/null" ? "/dev/null" : `${bPre}${prefix}${b}`
250
379
  }`
251
380
  )
252
381
  .replace(
253
382
  /^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
254
- (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
383
+ (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
255
384
  )
256
385
  .replace(
257
386
  /^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
258
- (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
387
+ (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
259
388
  );
260
389
  }
261
390
 
262
391
  function filterDiff(diff) {
263
392
  if (!diff) return "";
264
393
  return diff
265
- .split(/^diff --git /gm)
394
+ .split(/(?=^diff --git )/gm)
266
395
  .filter((block) => {
267
396
  if (!block.trim()) return false;
268
- const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
397
+ const m = block.match(/^diff --git ([ab]\/\S+?) ([ab]\/\S+?)\n/);
269
398
  if (
270
399
  m &&
271
400
  (shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
@@ -274,7 +403,6 @@ function filterDiff(diff) {
274
403
  }
275
404
  return true;
276
405
  })
277
- .map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
278
406
  .join("");
279
407
  }
280
408
 
@@ -349,78 +477,202 @@ function resolveBaseRef(repoPath) {
349
477
  return null;
350
478
  }
351
479
 
352
- function collectGitDiff({ projectId, repoNames, maxChars, fetchRemote = true } = {}) {
480
+ function collectGitDiffWithDiagnostics({
481
+ projectId,
482
+ repoNames,
483
+ maxChars,
484
+ fetchRemote = true,
485
+ } = {}) {
486
+ const configuredRepos = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
487
+ const diagnostics = {
488
+ project_id: projectId || null,
489
+ requested_repo_names: configuredRepos,
490
+ fetch_remote: Boolean(fetchRemote),
491
+ mode: "none",
492
+ search_root: null,
493
+ found_repos: [],
494
+ missing_repos: [],
495
+ repo_summaries: [],
496
+ collected_any: false,
497
+ collected_text_length: 0,
498
+ truncated: false,
499
+ warnings: [],
500
+ errors: [],
501
+ };
502
+
353
503
  try {
354
- const resolved = resolveConfiguredRepos(repoNames);
504
+ const resolved = resolveConfiguredRepos(configuredRepos);
505
+ diagnostics.mode = resolved.mode || "none";
506
+ diagnostics.search_root = resolved.root || null;
507
+ diagnostics.found_repos = (resolved.repos || []).map((r) => r.name);
508
+ diagnostics.missing_repos = resolved.missing || [];
355
509
  const repos = resolved.repos || [];
356
510
  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);
511
+ diagnostics.errors.push("No configured repos found locally.");
512
+ diagnostics.reason = "no_repos_found";
513
+ return {
514
+ text: "",
515
+ diagnostics,
516
+ };
364
517
  }
365
518
 
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`;
519
+ const sections = [];
520
+ if (projectId) sections.push(`# Project: ${projectId}`);
521
+ sections.push(`# Mode: ${resolved.mode}`);
522
+ sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
370
523
  if (resolved.missing && resolved.missing.length) {
371
- output += `# Missing locally (skipped): ${resolved.missing.join(", ")}\n`;
524
+ sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
372
525
  }
373
- output += "\n";
374
-
375
- for (const { name, dir, abs, prefix } of repos) {
376
- if (fetchRemote) fetchBaseBranches(abs);
526
+ sections.push("");
527
+
528
+ for (const repo of repos) {
529
+ const repoSummary = {
530
+ name: repo.name,
531
+ dir: repo.dir || ".",
532
+ root: repo.abs,
533
+ status: "ok",
534
+ head_branch: null,
535
+ base_ref: null,
536
+ has_changes: false,
537
+ changed_blocks: {
538
+ base_vs_head: false,
539
+ staged: false,
540
+ unstaged: false,
541
+ untracked_files: false,
542
+ },
543
+ untracked_file_count: 0,
544
+ errors: [],
545
+ };
546
+
547
+ const { name, dir, abs, prefix } = repo;
548
+ const fetchDiag = { attempted: false, ok: false, message: "" };
549
+ if (fetchRemote) {
550
+ fetchDiag.attempted = true;
551
+ fetchDiag.ok = fetchBaseBranches(abs);
552
+ if (!fetchDiag.ok) {
553
+ fetchDiag.message = "failed to refresh origin main/master";
554
+ repoSummary.errors.push(fetchDiag.message);
555
+ diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
556
+ }
557
+ }
377
558
 
378
559
  const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
379
560
  const baseRef = resolveBaseRef(abs);
561
+ repoSummary.head_branch = headBranch;
562
+ repoSummary.base_ref = baseRef || "base-unresolved";
563
+ repoSummary.fetch = fetchDiag;
564
+
565
+ let baseDiff = "";
566
+ if (baseRef) {
567
+ const baseDiffRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2", `${baseRef}...HEAD`], {
568
+ maxBuffer: 64 * 1024 * 1024,
569
+ });
570
+ baseDiff = filterDiff(toPatchWithPrefix(baseDiffRaw.output || "", prefix));
571
+ if (baseDiff) {
572
+ repoSummary.changed_blocks.base_vs_head = true;
573
+ repoSummary.has_changes = true;
574
+ } else if (baseDiffRaw.ok === false) {
575
+ repoSummary.errors.push(`base-diff: ${baseDiffRaw.error}`);
576
+ }
577
+ }
380
578
 
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"]);
579
+ const stagedRaw = runGitTryWithResult(abs, ["diff", "--cached", "--no-color", "-U2"]);
580
+ const staged = filterDiff(toPatchWithPrefix(stagedRaw.output || "", prefix));
581
+ if (staged) {
582
+ repoSummary.changed_blocks.staged = true;
583
+ repoSummary.has_changes = true;
584
+ } else if (stagedRaw.ok === false) {
585
+ repoSummary.errors.push(`staged-diff: ${stagedRaw.error}`);
586
+ }
384
587
 
385
- const baseDiff = patchPaths(filterDiff(baseDiffRaw), prefix);
386
- const staged = patchPaths(filterDiff(stagedRaw), prefix);
387
- const unstaged = patchPaths(filterDiff(unstagedRaw), prefix);
588
+ const unstagedRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2"]);
589
+ const unstaged = filterDiff(toPatchWithPrefix(unstagedRaw.output || "", prefix));
590
+ if (unstaged) {
591
+ repoSummary.changed_blocks.unstaged = true;
592
+ repoSummary.has_changes = true;
593
+ } else if (unstagedRaw.ok === false) {
594
+ repoSummary.errors.push(`unstaged-diff: ${unstagedRaw.error}`);
595
+ }
388
596
 
389
- const untracked = runGitTry(abs, ["ls-files", "--others", "--exclude-standard"])
390
- .split("\n")
391
- .filter(Boolean)
392
- .filter((f) => !shouldIgnore(f));
597
+ const untrackedListResult = runGitTryWithResult(abs, ["ls-files", "--others", "--exclude-standard"]);
598
+ const untracked = untrackedListResult.ok
599
+ ? untrackedListResult.output
600
+ .split("\n")
601
+ .map((f) => f.trim())
602
+ .filter(Boolean)
603
+ .filter((f) => !shouldIgnore(f))
604
+ : [];
605
+ if (!untrackedListResult.ok) {
606
+ repoSummary.errors.push(`untracked-list: ${untrackedListResult.error}`);
607
+ }
393
608
 
394
609
  let untrackedDiffs = "";
395
610
  for (const file of untracked) {
396
611
  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";
612
+ try {
613
+ if (!fs.existsSync(absFile) || !fs.statSync(absFile).isFile()) continue;
614
+ } catch (err) {
615
+ repoSummary.errors.push(`untracked-stat(${file}): ${safeText(err?.message || err)}`);
616
+ continue;
617
+ }
618
+ const patchResult = runGitTryWithResult(abs, ["diff", "--no-index", "--no-color", "/dev/null", file]);
619
+ if (patchResult.ok && patchResult.output) {
620
+ untrackedDiffs += toPatchWithPrefix(patchResult.output, prefix) + "\n";
621
+ repoSummary.untracked_file_count += 1;
622
+ repoSummary.changed_blocks.untracked_files = true;
623
+ repoSummary.has_changes = true;
624
+ } else if (!patchResult.ok) {
625
+ repoSummary.errors.push(`untracked-diff(${file}): ${patchResult.error}`);
626
+ }
627
+ }
628
+
629
+ if (repoSummary.errors.length) {
630
+ repoSummary.status = "partial";
400
631
  }
401
632
 
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`;
633
+ let section = `### ${name} (${dir || "."})\n\n`;
634
+ section += `# ?? Base vs HEAD (${repoSummary.base_ref}...HEAD | head=${headBranch})\n`;
635
+ if (baseDiff) section += `${baseDiff}\n\n`;
636
+ if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
637
+ if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
638
+ if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
408
639
 
409
- if (!baseDiff && !staged && !unstaged && !untrackedDiffs) {
410
- output += "_No local changes - working tree clean_\n\n";
640
+ if (!repoSummary.has_changes) {
641
+ section += "_No local changes - working tree clean_\n\n";
411
642
  }
643
+ sections.push(section);
644
+ diagnostics.repo_summaries.push(repoSummary);
645
+ if (repoSummary.has_changes) diagnostics.collected_any = true;
412
646
  }
413
647
 
648
+ let output = sections.join("\n");
649
+ diagnostics.collected_text_length = output.length;
650
+ if (!diagnostics.collected_any && !diagnostics.reason) {
651
+ diagnostics.reason = "no_changes_detected";
652
+ }
414
653
  if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
415
- return `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
654
+ diagnostics.truncated = true;
655
+ output = `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
416
656
  }
417
- return output.trim();
657
+ output = output.trim();
658
+ return {
659
+ text: output,
660
+ diagnostics,
661
+ };
418
662
  } catch (err) {
419
- console.warn("Failed to collect git diffs:", err?.message || err);
420
- return "";
663
+ diagnostics.errors.push(safeText(err?.message || err));
664
+ diagnostics.reason = "collection_error";
665
+ return {
666
+ text: "",
667
+ diagnostics,
668
+ };
421
669
  }
422
670
  }
423
671
 
672
+ function collectGitDiff(options = {}) {
673
+ return collectGitDiffWithDiagnostics(options).text;
674
+ }
675
+
424
676
  async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
425
677
  const fetchFn = await getFetch();
426
678
  const url = `${apiBase}/project-assistant/config`;
@@ -443,9 +695,9 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
443
695
  return body.data || {};
444
696
  }
445
697
 
446
- async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
698
+ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
447
699
  const fetchFn = await getFetch();
448
- const url = `${apiBase}/project-assistant/query`;
700
+ const url = `${apiBase}${endpoint}`;
449
701
  const { resp, body } = await fetchJsonWithTimeout(
450
702
  fetchFn,
451
703
  url,
@@ -469,6 +721,111 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
469
721
  return body.data || {};
470
722
  }
471
723
 
724
+ async function runCreatePrd(args) {
725
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
726
+ if (!key) {
727
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
728
+ process.exit(1);
729
+ }
730
+ const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
731
+
732
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
733
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
734
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
735
+
736
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
737
+ const apiBase = normalizeApiBase(baseRaw);
738
+
739
+ const providedPath = args.file || args.path || (Array.isArray(args._) ? args._[0] : "");
740
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !providedPath));
741
+
742
+ let sourceText = "";
743
+ let filePath = "";
744
+ if (useStdin) {
745
+ sourceText = String(await readStdinText() || "");
746
+ } else {
747
+ filePath = String(providedPath || "").trim();
748
+ if (!filePath) {
749
+ console.error("Missing PRD file path.");
750
+ printHelp();
751
+ process.exit(1);
752
+ }
753
+ const absPath = path.resolve(filePath);
754
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
755
+ console.error(`PRD file not found: ${absPath}`);
756
+ process.exit(1);
757
+ }
758
+ sourceText = fs.readFileSync(absPath, "utf8");
759
+ filePath = absPath;
760
+ }
761
+
762
+ const trimmedSource = String(sourceText || "").trim();
763
+ if (!trimmedSource) {
764
+ console.error("PRD content is empty.");
765
+ process.exit(1);
766
+ }
767
+
768
+ const inferredTitle = String(args.title || extractMarkdownTitle(trimmedSource) || (!useStdin && filePath ? path.parse(filePath).name : "")).trim();
769
+ const payload = isMyteKanbanTicket(trimmedSource)
770
+ ? {
771
+ ticket_markdown: trimmedSource,
772
+ }
773
+ : {
774
+ prd_markdown: trimmedSource,
775
+ title: inferredTitle,
776
+ };
777
+
778
+ if (!payload.ticket_markdown && !payload.title) {
779
+ console.error("A title is required when uploading raw markdown without a myte-kanban metadata block. Use --title or add a top-level # heading.");
780
+ process.exit(1);
781
+ }
782
+
783
+ if (printContext) {
784
+ console.log(JSON.stringify(payload, null, 2));
785
+ process.exit(0);
786
+ }
787
+
788
+ let data;
789
+ try {
790
+ data = await callAssistantQuery({
791
+ apiBase,
792
+ key,
793
+ payload,
794
+ timeoutMs,
795
+ endpoint: "/project-assistant/create-prd",
796
+ });
797
+ } catch (err) {
798
+ if (err?.name === "AbortError") {
799
+ console.error(`Request timed out after ${timeoutMs}ms`);
800
+ } else {
801
+ console.error("PRD upload failed:", err?.message || err);
802
+ }
803
+ process.exit(1);
804
+ }
805
+
806
+ if (args.json) {
807
+ console.log(
808
+ JSON.stringify(
809
+ {
810
+ feedback_id: data.feedback_id || null,
811
+ project_id: data.project_id || null,
812
+ title: data.title || null,
813
+ status: data.status || null,
814
+ deterministic: data.deterministic === true,
815
+ },
816
+ null,
817
+ 2
818
+ )
819
+ );
820
+ return;
821
+ }
822
+
823
+ if (data.feedback_id) console.log(`Feedback ID: ${data.feedback_id}`);
824
+ if (data.project_id) console.log(`Project ID: ${data.project_id}`);
825
+ if (data.title) console.log(`Title: ${data.title}`);
826
+ if (data.status) console.log(`Status: ${data.status}`);
827
+ }
828
+
472
829
  async function runConfig(args) {
473
830
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
474
831
  if (!key) {
@@ -548,31 +905,44 @@ async function runQuery(args) {
548
905
  const apiBase = normalizeApiBase(baseRaw);
549
906
 
550
907
  let diffText = "";
908
+ let diffDiagnostics = null;
551
909
  if (includeDiff) {
552
- let cfg;
910
+ let cfg = null;
553
911
  try {
554
912
  cfg = await fetchProjectConfig({ apiBase, key, timeoutMs });
555
913
  } catch (err) {
556
- console.error("Failed to fetch project config (needed for diffs):", err?.message || err);
557
- process.exit(1);
914
+ console.warn("Warning: project config unavailable for --with-diff. Continuing without diff context.");
915
+ console.warn(`Detail: ${err?.message || err}`);
916
+ cfg = null;
917
+ }
918
+
919
+ if (cfg) {
920
+ const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
921
+ const diffResult = collectGitDiffWithDiagnostics({
922
+ projectId: cfg.project_id,
923
+ repoNames,
924
+ maxChars: diffLimit,
925
+ fetchRemote,
926
+ });
927
+ diffText = diffResult.text;
928
+ diffDiagnostics = diffResult.diagnostics;
929
+ if (diffDiagnostics?.errors?.length) {
930
+ for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
931
+ }
932
+ } else {
933
+ diffText = "";
934
+ diffDiagnostics = null;
558
935
  }
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
936
  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);
937
+ console.error("Warning: no diff context collected. Continuing without --with-diff context.");
569
938
  }
570
939
  }
571
940
 
572
941
  const payload = {
573
942
  query,
574
- additional_context: buildContext(args, diffText),
943
+ additional_context: buildContext(args, diffText, diffDiagnostics),
575
944
  };
945
+ if (diffDiagnostics) payload.diff_diagnostics = diffDiagnostics;
576
946
 
577
947
  if (printContext) {
578
948
  console.log(JSON.stringify(payload, null, 2));
@@ -642,6 +1012,11 @@ async function main() {
642
1012
  return;
643
1013
  }
644
1014
 
1015
+ if (command === "create-prd" || command === "add-prd" || command === "prd") {
1016
+ await runCreatePrd(args);
1017
+ return;
1018
+ }
1019
+
645
1020
  // query/ask default
646
1021
  await runQuery(args);
647
1022
  }
@@ -650,4 +1025,3 @@ main().catch((err) => {
650
1025
  console.error("Unexpected error:", err?.message || err);
651
1026
  process.exit(1);
652
1027
  });
653
-
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Myte CLI core implementation (Project Assistant + deterministic diffs).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",
7
+ "files": [
8
+ "README.md",
9
+ "cli.js",
10
+ "package.json"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test"
14
+ },
7
15
  "license": "MIT",
8
16
  "engines": {
9
17
  "node": ">=18"
@@ -17,4 +25,3 @@
17
25
  "node-fetch": "^3.3.2"
18
26
  }
19
27
  }
20
-