@mytegroupinc/myte-core 0.0.3 → 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 +421 -66
  3. package/package.json +9 -1
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",
@@ -214,15 +292,40 @@ function runGitDiff(repoPath, args, opts = {}) {
214
292
  if (res.error && res.error.code === "ENOENT") {
215
293
  const err = new Error("git executable not found in PATH. Install Git and ensure PATH includes git.");
216
294
  err.code = "ENOENT";
217
- throw err;
295
+ return { ok: false, status: null, stdout: "", stderr: "", error: err };
218
296
  }
219
297
  if (res.status !== 0 && res.status !== 1) {
220
298
  const msg = res.stderr || res.error?.message || "unknown git error";
221
299
  const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
222
300
  err.code = res.status;
223
- 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 "";
224
328
  }
225
- return (res.stdout || "").trim();
226
329
  }
227
330
 
228
331
  function runGitOk(repoPath, args, opts = {}) {
@@ -230,7 +333,7 @@ function runGitOk(repoPath, args, opts = {}) {
230
333
  cwd: repoPath,
231
334
  encoding: "utf8",
232
335
  stdio: ["ignore", "pipe", "pipe"],
233
- maxBuffer: 16 * 1024 * 1024,
336
+ maxBuffer: 64 * 1024 * 1024,
234
337
  ...opts,
235
338
  });
236
339
  return res.status === 0;
@@ -244,6 +347,19 @@ function runGitTry(repoPath, args) {
244
347
  }
245
348
  }
246
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
+
247
363
  function runGitSafeDiff(repoPath, args, prefix) {
248
364
  const patchText = runGitTry(repoPath, args);
249
365
  if (!patchText) {
@@ -258,27 +374,27 @@ function patchPaths(diff, prefix = "") {
258
374
  .replace(
259
375
  /^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
260
376
  (_, aPre, a, bPre, b) =>
261
- `diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix + a} ${
262
- 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}`
263
379
  }`
264
380
  )
265
381
  .replace(
266
382
  /^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
267
- (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
383
+ (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
268
384
  )
269
385
  .replace(
270
386
  /^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
271
- (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
387
+ (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
272
388
  );
273
389
  }
274
390
 
275
391
  function filterDiff(diff) {
276
392
  if (!diff) return "";
277
393
  return diff
278
- .split(/^diff --git /gm)
394
+ .split(/(?=^diff --git )/gm)
279
395
  .filter((block) => {
280
396
  if (!block.trim()) return false;
281
- const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
397
+ const m = block.match(/^diff --git ([ab]\/\S+?) ([ab]\/\S+?)\n/);
282
398
  if (
283
399
  m &&
284
400
  (shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
@@ -287,7 +403,6 @@ function filterDiff(diff) {
287
403
  }
288
404
  return true;
289
405
  })
290
- .map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
291
406
  .join("");
292
407
  }
293
408
 
@@ -362,80 +477,202 @@ function resolveBaseRef(repoPath) {
362
477
  return null;
363
478
  }
364
479
 
365
- 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
+
366
503
  try {
367
- 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 || [];
368
509
  const repos = resolved.repos || [];
369
510
  if (!repos.length) {
370
- const configured = Array.isArray(repoNames) ? repoNames.join(", ") : "";
371
- const msg = [
372
- "[myte] No configured repos found locally.",
373
- `Configured repos (from project ${projectId || "unknown"}): ${configured || "(none)"}`,
374
- "Run from within a configured repo, or a parent folder that contains the repo folders by name.",
375
- ].join("\n");
376
- 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
+ };
377
517
  }
378
518
 
379
- let output = "";
380
- if (projectId) output += `# Project: ${projectId}\n`;
381
- output += `# Mode: ${resolved.mode}\n`;
382
- 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(", ") || ""}`);
383
523
  if (resolved.missing && resolved.missing.length) {
384
- output += `# Missing locally (skipped): ${resolved.missing.join(", ")}\n`;
524
+ sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
385
525
  }
386
- output += "\n";
387
-
388
- for (const { name, dir, abs, prefix } of repos) {
389
- 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
+ }
390
558
 
391
559
  const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
392
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
+ }
393
578
 
394
- const baseDiffRaw = baseRef
395
- ? runGitSafeDiff(abs, ["diff", "--no-color", "-U2", `${baseRef}...HEAD`], prefix)
396
- : "";
397
- const stagedRaw = runGitSafeDiff(abs, ["diff", "--cached", "--no-color", "-U2"], prefix);
398
- const unstagedRaw = runGitSafeDiff(abs, ["diff", "--no-color", "-U2"], prefix);
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
+ }
399
587
 
400
- const baseDiff = filterDiff(baseDiffRaw);
401
- const staged = filterDiff(stagedRaw);
402
- const unstaged = filterDiff(unstagedRaw);
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
+ }
403
596
 
404
- const untracked = runGitTry(abs, ["ls-files", "--others", "--exclude-standard"])
405
- .split("\n")
406
- .filter(Boolean)
407
- .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
+ }
408
608
 
409
609
  let untrackedDiffs = "";
410
610
  for (const file of untracked) {
411
611
  const absFile = path.join(abs, file);
412
- if (!fs.existsSync(absFile) || !fs.statSync(absFile).isFile()) continue;
413
- const patch = runGitTry(abs, ["diff", "--no-index", "--no-color", "/dev/null", file]);
414
- 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
+ }
415
627
  }
416
628
 
417
- output += `### ${name} (${dir || "."})\n\n`;
418
- output += `# ?? Base vs HEAD (${baseRef || "base-unresolved"}...HEAD | head=${headBranch})\n`;
419
- if (baseDiff) output += `${baseDiff}\n\n`;
420
- if (staged) output += `# ?? Staged changes\n${staged}\n\n`;
421
- if (unstaged) output += `# ?? Unstaged changes\n${unstaged}\n\n`;
422
- if (untrackedDiffs) output += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
629
+ if (repoSummary.errors.length) {
630
+ repoSummary.status = "partial";
631
+ }
632
+
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`;
423
639
 
424
- if (!baseDiff && !staged && !unstaged && !untrackedDiffs) {
425
- output += "_No local changes - working tree clean_\n\n";
640
+ if (!repoSummary.has_changes) {
641
+ section += "_No local changes - working tree clean_\n\n";
426
642
  }
643
+ sections.push(section);
644
+ diagnostics.repo_summaries.push(repoSummary);
645
+ if (repoSummary.has_changes) diagnostics.collected_any = true;
427
646
  }
428
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
+ }
429
653
  if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
430
- 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]`;
431
656
  }
432
- return output.trim();
657
+ output = output.trim();
658
+ return {
659
+ text: output,
660
+ diagnostics,
661
+ };
433
662
  } catch (err) {
434
- console.warn("Failed to collect git diffs:", err?.message || err);
435
- return "";
663
+ diagnostics.errors.push(safeText(err?.message || err));
664
+ diagnostics.reason = "collection_error";
665
+ return {
666
+ text: "",
667
+ diagnostics,
668
+ };
436
669
  }
437
670
  }
438
671
 
672
+ function collectGitDiff(options = {}) {
673
+ return collectGitDiffWithDiagnostics(options).text;
674
+ }
675
+
439
676
  async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
440
677
  const fetchFn = await getFetch();
441
678
  const url = `${apiBase}/project-assistant/config`;
@@ -458,9 +695,9 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
458
695
  return body.data || {};
459
696
  }
460
697
 
461
- async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
698
+ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
462
699
  const fetchFn = await getFetch();
463
- const url = `${apiBase}/project-assistant/query`;
700
+ const url = `${apiBase}${endpoint}`;
464
701
  const { resp, body } = await fetchJsonWithTimeout(
465
702
  fetchFn,
466
703
  url,
@@ -484,6 +721,111 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
484
721
  return body.data || {};
485
722
  }
486
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
+
487
829
  async function runConfig(args) {
488
830
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
489
831
  if (!key) {
@@ -563,6 +905,7 @@ async function runQuery(args) {
563
905
  const apiBase = normalizeApiBase(baseRaw);
564
906
 
565
907
  let diffText = "";
908
+ let diffDiagnostics = null;
566
909
  if (includeDiff) {
567
910
  let cfg = null;
568
911
  try {
@@ -575,14 +918,20 @@ async function runQuery(args) {
575
918
 
576
919
  if (cfg) {
577
920
  const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
578
- diffText = collectGitDiff({
921
+ const diffResult = collectGitDiffWithDiagnostics({
579
922
  projectId: cfg.project_id,
580
923
  repoNames,
581
924
  maxChars: diffLimit,
582
925
  fetchRemote,
583
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
+ }
584
932
  } else {
585
933
  diffText = "";
934
+ diffDiagnostics = null;
586
935
  }
587
936
  if (!diffText) {
588
937
  console.error("Warning: no diff context collected. Continuing without --with-diff context.");
@@ -591,8 +940,9 @@ async function runQuery(args) {
591
940
 
592
941
  const payload = {
593
942
  query,
594
- additional_context: buildContext(args, diffText),
943
+ additional_context: buildContext(args, diffText, diffDiagnostics),
595
944
  };
945
+ if (diffDiagnostics) payload.diff_diagnostics = diffDiagnostics;
596
946
 
597
947
  if (printContext) {
598
948
  console.log(JSON.stringify(payload, null, 2));
@@ -662,6 +1012,11 @@ async function main() {
662
1012
  return;
663
1013
  }
664
1014
 
1015
+ if (command === "create-prd" || command === "add-prd" || command === "prd") {
1016
+ await runCreatePrd(args);
1017
+ return;
1018
+ }
1019
+
665
1020
  // query/ask default
666
1021
  await runQuery(args);
667
1022
  }
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.3",
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"