@mytegroupinc/myte-core 0.0.3 → 0.0.5

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 +43 -2
  2. package/cli.js +678 -66
  3. package/package.json +11 -2
package/README.md CHANGED
@@ -3,8 +3,49 @@
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 bootstrap`
7
+ - `npm install myte` then `npx myte query "..." --with-diff`
8
+ - `npm install myte` then `npm exec myte -- query "..." --with-diff`
9
+ - `npm i -g myte` then `myte bootstrap`
10
+ - `npm i -g myte` then `myte query "..." --with-diff`
11
+ - `npx myte@latest bootstrap`
7
12
  - `npx myte@latest query "..." --with-diff`
13
+ - `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
14
+ - `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
8
15
 
9
- This package is published under the org scope for governance; the wrapper delegates here.
16
+ Requirements:
17
+ - Node `18+`
18
+ - macOS, Linux, or Windows
19
+ - `git` in `PATH` for `--with-diff`
20
+ - `MYTE_API_KEY=<project_api_key>` in env or `.env`
21
+ - repo folder names must match the project repo names configured in Myte, including casing on case-sensitive filesystems
10
22
 
23
+ Notes:
24
+ - `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
25
+ - `npm install myte` means the CLI is available locally; bare `myte ...` still requires a global install.
26
+ - `bootstrap` is a local file materialization path, not a hosted file download.
27
+ - `bootstrap` expects to run from a wrapper root that contains the project's configured repo folders.
28
+ - `bootstrap` writes `MyteCommandCenter/data/phases`, `epics`, `stories`, `missions`, `project.yml`, and `bootstrap-manifest.json`.
29
+ - `create-prd` is a deterministic PRD upload path, not an LLM generation command.
30
+ - `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
31
+ - `--with-diff` includes per-repo diagnostics in `print-context` payload:
32
+ - missing repo directories
33
+ - per-repo errors (for example fetch or command failures)
34
+ - clean/no-change repo summaries
35
+ - `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
36
+
37
+ Deterministic `create-prd` contract:
38
+ - Required: `MYTE_API_KEY`, a PRD markdown body, and a title.
39
+ - Title source: `myte-kanban.title`, the first markdown `# Heading`, or `--title`.
40
+ - Description source: `myte-kanban.description` or `--description`.
41
+ - The markdown body is stored verbatim as PRD content and is what the backend uses to build the PRD DOCX.
42
+ - Legacy `feedback_text` is still accepted for older payloads, but new callers should use `description`.
43
+ - Optional structured fields: `priority`, `status`, `tags`, `assigned_user_email`, `assigned_user_id`, `due_date`, `repo_name`, `repo_id`, `preview_url`, `source`.
44
+
45
+ Examples:
46
+ - `npx myte bootstrap`
47
+ - `npx myte bootstrap --dry-run --json`
48
+ - `npx myte create-prd ./drafts/auth-prd.md --description "Short card summary"`
49
+ - `npx myte create-prd ./drafts/auth-prd.md --print-context`
50
+
51
+ 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", "bootstrap", "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", "description", "feedback-text", "output-dir"],
67
67
  alias: {
68
68
  q: "query",
69
69
  d: "with-diff",
@@ -116,27 +116,84 @@ function printHelp() {
116
116
  "Usage:",
117
117
  " myte query \"<text>\" [--with-diff] [--context \"...\"]",
118
118
  " myte config [--json]",
119
+ " myte bootstrap [--output-dir ./MyteCommandCenter] [--json]",
119
120
  " myte chat",
121
+ " myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
122
+ " myte add-prd <file.md> [--json]",
123
+ " cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
124
+ "",
125
+ "Run forms:",
126
+ " npm install myte then npx myte query \"...\" --with-diff",
127
+ " npm install myte then npm exec myte -- query \"...\" --with-diff",
128
+ " npm i -g myte then myte query \"...\" --with-diff",
129
+ " npx myte@latest query \"What changed in logging?\" --with-diff",
120
130
  "",
121
131
  "Auth:",
122
132
  " - Set MYTE_API_KEY in a workspace .env (or env var)",
123
133
  "",
134
+ "bootstrap contract:",
135
+ " - Run from the wrapper root that contains the project's configured repo folders",
136
+ " - Writes MyteCommandCenter/data/phases, epics, stories, and missions locally",
137
+ " - Uses the project-scoped bootstrap snapshot from the Myte API",
138
+ "",
139
+ "create-prd contract:",
140
+ " - Required: valid MYTE_API_KEY, PRD markdown body, title",
141
+ " - Title source: myte-kanban.title, first # heading, or --title",
142
+ " - Description source: myte-kanban.description or --description",
143
+ " - PRD DOCX content: the markdown body is stored verbatim",
144
+ "",
124
145
  "Options:",
125
146
  " --with-diff Include deterministic git diffs (project-scoped)",
126
147
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
127
148
  " --timeout-ms <ms> Request timeout (default: 300000)",
128
149
  " --base-url <url> API base (default: https://api.myte.dev)",
150
+ " --output-dir <path> Bootstrap output directory (default: <wrapper-root>/MyteCommandCenter)",
151
+ " --stdin Read PRD content from stdin instead of a file path",
152
+ " --title <text> Override PRD title for raw markdown uploads",
153
+ " --description <text> Set feedback description/card summary for raw markdown uploads",
129
154
  " --print-context Print JSON payload and exit (no query call)",
130
155
  " --no-fetch Don't git fetch origin main/master before diff",
131
156
  "",
132
157
  "Examples:",
133
158
  " myte query \"What changed in logging?\" --with-diff",
134
- " myte query \"...\" --with-diff --diff-limit 120000",
159
+ " myte bootstrap",
160
+ " myte bootstrap --output-dir ./MyteCommandCenter",
161
+ " myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
162
+ " cat ./drafts/auth-prd.md | myte create-prd --stdin",
135
163
  " myte config",
136
164
  ].join("\n");
137
165
  console.log(text);
138
166
  }
139
167
 
168
+ function extractMarkdownTitle(text) {
169
+ const raw = String(text || "");
170
+ const lines = raw.split(/\r?\n/);
171
+ for (const rawLine of lines) {
172
+ const line = rawLine.trim();
173
+ if (line.startsWith("#")) {
174
+ const title = line.replace(/^#+\s*/, "").trim();
175
+ if (title) return title;
176
+ }
177
+ }
178
+ return "";
179
+ }
180
+
181
+ function isMyteKanbanTicket(text) {
182
+ return /^\s*```myte-kanban\s*\{[\s\S]*?\}\s*```\s*/.test(String(text || ""));
183
+ }
184
+
185
+ async function readStdinText() {
186
+ return new Promise((resolve, reject) => {
187
+ let data = "";
188
+ process.stdin.setEncoding("utf8");
189
+ process.stdin.on("data", (chunk) => {
190
+ data += chunk;
191
+ });
192
+ process.stdin.on("end", () => resolve(data));
193
+ process.stdin.on("error", reject);
194
+ });
195
+ }
196
+
140
197
  async function getFetch() {
141
198
  if (typeof fetch !== "undefined") return fetch;
142
199
  const mod = await import("node-fetch");
@@ -170,10 +227,47 @@ async function fetchJsonWithTimeout(fetchFn, url, options, timeoutMs) {
170
227
  }
171
228
  }
172
229
 
173
- function buildContext(args, diffText) {
230
+ function summarizeDiffDiagnosticsForContext(diagnostics) {
231
+ if (!diagnostics) return null;
232
+ const repos = Array.isArray(diagnostics.repo_summaries)
233
+ ? diagnostics.repo_summaries
234
+ : [];
235
+ return {
236
+ project_id: diagnostics.project_id || null,
237
+ mode: diagnostics.mode,
238
+ requested_repos: diagnostics.requested_repo_names || [],
239
+ found_repos: diagnostics.found_repos || [],
240
+ missing_repos: diagnostics.missing_repos || [],
241
+ collected_any: Boolean(diagnostics.collected_any),
242
+ truncation: diagnostics.truncated ? "truncated" : "full",
243
+ repos: repos.map((repo) => ({
244
+ name: repo.name,
245
+ status: repo.status || "ok",
246
+ head_branch: repo.head_branch || null,
247
+ base_ref: repo.base_ref || null,
248
+ has_changes: Boolean(repo.has_changes),
249
+ changed_blocks: repo.changed_blocks || {},
250
+ untracked_file_count: repo.untracked_file_count || 0,
251
+ error_count: Array.isArray(repo.errors) ? repo.errors.length : 0,
252
+ })),
253
+ warnings: diagnostics.warnings || [],
254
+ errors: diagnostics.errors || [],
255
+ };
256
+ }
257
+
258
+ function formatDiffDiagnosticText(diagnostics) {
259
+ const summary = summarizeDiffDiagnosticsForContext(diagnostics);
260
+ if (!summary) return "";
261
+ return `Git diff diagnostics:\n${JSON.stringify(summary, null, 2)}`;
262
+ }
263
+
264
+ function buildContext(args, diffText, diffDiagnostics) {
174
265
  const ctx = args.context ?? args.ctx ?? args.c;
175
266
  const extra = ctx === undefined ? [] : Array.isArray(ctx) ? ctx.map(String) : [String(ctx)];
176
267
  if (diffText) extra.push(`Current git diff snapshot (project-configured):\n${diffText}`);
268
+ if (diffDiagnostics) {
269
+ extra.push(formatDiffDiagnosticText(diffDiagnostics));
270
+ }
177
271
  return extra;
178
272
  }
179
273
 
@@ -203,7 +297,7 @@ function hasGitDir(repoPath) {
203
297
  return fs.existsSync(path.join(repoPath, ".git"));
204
298
  }
205
299
 
206
- function runGitDiff(repoPath, args, opts = {}) {
300
+ function runGitRaw(repoPath, args, opts = {}) {
207
301
  const res = spawnSync("git", args, {
208
302
  cwd: repoPath,
209
303
  encoding: "utf8",
@@ -214,15 +308,40 @@ function runGitDiff(repoPath, args, opts = {}) {
214
308
  if (res.error && res.error.code === "ENOENT") {
215
309
  const err = new Error("git executable not found in PATH. Install Git and ensure PATH includes git.");
216
310
  err.code = "ENOENT";
217
- throw err;
311
+ return { ok: false, status: null, stdout: "", stderr: "", error: err };
218
312
  }
219
313
  if (res.status !== 0 && res.status !== 1) {
220
314
  const msg = res.stderr || res.error?.message || "unknown git error";
221
315
  const err = new Error(`git ${args.join(" ")} failed (${res.status}): ${msg}`);
222
316
  err.code = res.status;
223
- throw err;
317
+ return { ok: false, status: res.status, stdout: "", stderr: msg, error: err };
318
+ }
319
+ return { ok: true, status: res.status, stdout: (res.stdout || "").trim(), stderr: res.stderr || "" };
320
+ }
321
+
322
+ function runGitDiff(repoPath, args, opts = {}) {
323
+ const result = runGitRaw(repoPath, args, opts);
324
+ if (!result.ok) {
325
+ throw result.error;
326
+ }
327
+ return result.stdout;
328
+ }
329
+
330
+ function runGitDiffResult(repoPath, args, opts = {}) {
331
+ return runGitRaw(repoPath, args, opts);
332
+ }
333
+
334
+ function toPatchWithPrefix(diffText, prefix) {
335
+ if (!diffText) return "";
336
+ return patchPaths(diffText, prefix);
337
+ }
338
+
339
+ function safeText(value) {
340
+ try {
341
+ return String(value || "");
342
+ } catch (err) {
343
+ return "";
224
344
  }
225
- return (res.stdout || "").trim();
226
345
  }
227
346
 
228
347
  function runGitOk(repoPath, args, opts = {}) {
@@ -230,7 +349,7 @@ function runGitOk(repoPath, args, opts = {}) {
230
349
  cwd: repoPath,
231
350
  encoding: "utf8",
232
351
  stdio: ["ignore", "pipe", "pipe"],
233
- maxBuffer: 16 * 1024 * 1024,
352
+ maxBuffer: 64 * 1024 * 1024,
234
353
  ...opts,
235
354
  });
236
355
  return res.status === 0;
@@ -244,6 +363,19 @@ function runGitTry(repoPath, args) {
244
363
  }
245
364
  }
246
365
 
366
+ function runGitTryWithResult(repoPath, args, opts = {}) {
367
+ const result = runGitDiffResult(repoPath, args, opts);
368
+ if (!result.ok) {
369
+ return {
370
+ ok: false,
371
+ output: "",
372
+ error: safeText(result.error?.message),
373
+ status: result.status,
374
+ };
375
+ }
376
+ return { ok: true, output: result.stdout, error: "", status: result.status };
377
+ }
378
+
247
379
  function runGitSafeDiff(repoPath, args, prefix) {
248
380
  const patchText = runGitTry(repoPath, args);
249
381
  if (!patchText) {
@@ -258,27 +390,27 @@ function patchPaths(diff, prefix = "") {
258
390
  .replace(
259
391
  /^diff --git ([ab]\/|\/dev\/null)(.+?) ([ab]\/|\/dev\/null)(.+?)$/gm,
260
392
  (_, aPre, a, bPre, b) =>
261
- `diff --git ${aPre === "/dev/null" ? "/dev/null" : prefix + a} ${
262
- bPre === "/dev/null" ? "/dev/null" : prefix + b
393
+ `diff --git ${aPre === "/dev/null" ? "/dev/null" : `${aPre}${prefix}${a}`} ${
394
+ bPre === "/dev/null" ? "/dev/null" : `${bPre}${prefix}${b}`
263
395
  }`
264
396
  )
265
397
  .replace(
266
398
  /^(---) ([ab]\/|\/dev\/null)(.+)$/gm,
267
- (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
399
+ (_, m, pre, p) => `${m} ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
268
400
  )
269
401
  .replace(
270
402
  /^(?:\+\+\+) ([ab]\/|\/dev\/null)(.+)$/gm,
271
- (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : prefix + p}`
403
+ (_, pre, p) => `+++ ${pre === "/dev/null" ? "/dev/null" : `${pre}${prefix}${p}`}`
272
404
  );
273
405
  }
274
406
 
275
407
  function filterDiff(diff) {
276
408
  if (!diff) return "";
277
409
  return diff
278
- .split(/^diff --git /gm)
410
+ .split(/(?=^diff --git )/gm)
279
411
  .filter((block) => {
280
412
  if (!block.trim()) return false;
281
- const m = block.match(/^([ab]\/\S+?) ([ab]\/\S+?)\n/);
413
+ const m = block.match(/^diff --git ([ab]\/\S+?) ([ab]\/\S+?)\n/);
282
414
  if (
283
415
  m &&
284
416
  (shouldIgnore(m[1].replace(/^[ab]\//, "")) || shouldIgnore(m[2].replace(/^[ab]\//, "")))
@@ -287,7 +419,6 @@ function filterDiff(diff) {
287
419
  }
288
420
  return true;
289
421
  })
290
- .map((blk, i) => (i === 0 ? blk : `diff --git ${blk}`))
291
422
  .join("");
292
423
  }
293
424
 
@@ -362,80 +493,202 @@ function resolveBaseRef(repoPath) {
362
493
  return null;
363
494
  }
364
495
 
365
- function collectGitDiff({ projectId, repoNames, maxChars, fetchRemote = true } = {}) {
496
+ function collectGitDiffWithDiagnostics({
497
+ projectId,
498
+ repoNames,
499
+ maxChars,
500
+ fetchRemote = true,
501
+ } = {}) {
502
+ const configuredRepos = Array.isArray(repoNames) ? repoNames.map(String).map((s) => s.trim()).filter(Boolean) : [];
503
+ const diagnostics = {
504
+ project_id: projectId || null,
505
+ requested_repo_names: configuredRepos,
506
+ fetch_remote: Boolean(fetchRemote),
507
+ mode: "none",
508
+ search_root: null,
509
+ found_repos: [],
510
+ missing_repos: [],
511
+ repo_summaries: [],
512
+ collected_any: false,
513
+ collected_text_length: 0,
514
+ truncated: false,
515
+ warnings: [],
516
+ errors: [],
517
+ };
518
+
366
519
  try {
367
- const resolved = resolveConfiguredRepos(repoNames);
520
+ const resolved = resolveConfiguredRepos(configuredRepos);
521
+ diagnostics.mode = resolved.mode || "none";
522
+ diagnostics.search_root = resolved.root || null;
523
+ diagnostics.found_repos = (resolved.repos || []).map((r) => r.name);
524
+ diagnostics.missing_repos = resolved.missing || [];
368
525
  const repos = resolved.repos || [];
369
526
  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);
527
+ diagnostics.errors.push("No configured repos found locally.");
528
+ diagnostics.reason = "no_repos_found";
529
+ return {
530
+ text: "",
531
+ diagnostics,
532
+ };
377
533
  }
378
534
 
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`;
535
+ const sections = [];
536
+ if (projectId) sections.push(`# Project: ${projectId}`);
537
+ sections.push(`# Mode: ${resolved.mode}`);
538
+ sections.push(`# Configured repos: ${configuredRepos.join(", ") || ""}`);
383
539
  if (resolved.missing && resolved.missing.length) {
384
- output += `# Missing locally (skipped): ${resolved.missing.join(", ")}\n`;
540
+ sections.push(`# Missing locally (skipped): ${resolved.missing.join(", ")}`);
385
541
  }
386
- output += "\n";
387
-
388
- for (const { name, dir, abs, prefix } of repos) {
389
- if (fetchRemote) fetchBaseBranches(abs);
542
+ sections.push("");
543
+
544
+ for (const repo of repos) {
545
+ const repoSummary = {
546
+ name: repo.name,
547
+ dir: repo.dir || ".",
548
+ root: repo.abs,
549
+ status: "ok",
550
+ head_branch: null,
551
+ base_ref: null,
552
+ has_changes: false,
553
+ changed_blocks: {
554
+ base_vs_head: false,
555
+ staged: false,
556
+ unstaged: false,
557
+ untracked_files: false,
558
+ },
559
+ untracked_file_count: 0,
560
+ errors: [],
561
+ };
562
+
563
+ const { name, dir, abs, prefix } = repo;
564
+ const fetchDiag = { attempted: false, ok: false, message: "" };
565
+ if (fetchRemote) {
566
+ fetchDiag.attempted = true;
567
+ fetchDiag.ok = fetchBaseBranches(abs);
568
+ if (!fetchDiag.ok) {
569
+ fetchDiag.message = "failed to refresh origin main/master";
570
+ repoSummary.errors.push(fetchDiag.message);
571
+ diagnostics.warnings.push(`Repo "${name}": ${fetchDiag.message}`);
572
+ }
573
+ }
390
574
 
391
575
  const headBranch = runGitTry(abs, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
392
576
  const baseRef = resolveBaseRef(abs);
577
+ repoSummary.head_branch = headBranch;
578
+ repoSummary.base_ref = baseRef || "base-unresolved";
579
+ repoSummary.fetch = fetchDiag;
580
+
581
+ let baseDiff = "";
582
+ if (baseRef) {
583
+ const baseDiffRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2", `${baseRef}...HEAD`], {
584
+ maxBuffer: 64 * 1024 * 1024,
585
+ });
586
+ baseDiff = filterDiff(toPatchWithPrefix(baseDiffRaw.output || "", prefix));
587
+ if (baseDiff) {
588
+ repoSummary.changed_blocks.base_vs_head = true;
589
+ repoSummary.has_changes = true;
590
+ } else if (baseDiffRaw.ok === false) {
591
+ repoSummary.errors.push(`base-diff: ${baseDiffRaw.error}`);
592
+ }
593
+ }
393
594
 
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);
595
+ const stagedRaw = runGitTryWithResult(abs, ["diff", "--cached", "--no-color", "-U2"]);
596
+ const staged = filterDiff(toPatchWithPrefix(stagedRaw.output || "", prefix));
597
+ if (staged) {
598
+ repoSummary.changed_blocks.staged = true;
599
+ repoSummary.has_changes = true;
600
+ } else if (stagedRaw.ok === false) {
601
+ repoSummary.errors.push(`staged-diff: ${stagedRaw.error}`);
602
+ }
399
603
 
400
- const baseDiff = filterDiff(baseDiffRaw);
401
- const staged = filterDiff(stagedRaw);
402
- const unstaged = filterDiff(unstagedRaw);
604
+ const unstagedRaw = runGitTryWithResult(abs, ["diff", "--no-color", "-U2"]);
605
+ const unstaged = filterDiff(toPatchWithPrefix(unstagedRaw.output || "", prefix));
606
+ if (unstaged) {
607
+ repoSummary.changed_blocks.unstaged = true;
608
+ repoSummary.has_changes = true;
609
+ } else if (unstagedRaw.ok === false) {
610
+ repoSummary.errors.push(`unstaged-diff: ${unstagedRaw.error}`);
611
+ }
403
612
 
404
- const untracked = runGitTry(abs, ["ls-files", "--others", "--exclude-standard"])
405
- .split("\n")
406
- .filter(Boolean)
407
- .filter((f) => !shouldIgnore(f));
613
+ const untrackedListResult = runGitTryWithResult(abs, ["ls-files", "--others", "--exclude-standard"]);
614
+ const untracked = untrackedListResult.ok
615
+ ? untrackedListResult.output
616
+ .split("\n")
617
+ .map((f) => f.trim())
618
+ .filter(Boolean)
619
+ .filter((f) => !shouldIgnore(f))
620
+ : [];
621
+ if (!untrackedListResult.ok) {
622
+ repoSummary.errors.push(`untracked-list: ${untrackedListResult.error}`);
623
+ }
408
624
 
409
625
  let untrackedDiffs = "";
410
626
  for (const file of untracked) {
411
627
  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";
628
+ try {
629
+ if (!fs.existsSync(absFile) || !fs.statSync(absFile).isFile()) continue;
630
+ } catch (err) {
631
+ repoSummary.errors.push(`untracked-stat(${file}): ${safeText(err?.message || err)}`);
632
+ continue;
633
+ }
634
+ const patchResult = runGitTryWithResult(abs, ["diff", "--no-index", "--no-color", "/dev/null", file]);
635
+ if (patchResult.ok && patchResult.output) {
636
+ untrackedDiffs += toPatchWithPrefix(patchResult.output, prefix) + "\n";
637
+ repoSummary.untracked_file_count += 1;
638
+ repoSummary.changed_blocks.untracked_files = true;
639
+ repoSummary.has_changes = true;
640
+ } else if (!patchResult.ok) {
641
+ repoSummary.errors.push(`untracked-diff(${file}): ${patchResult.error}`);
642
+ }
643
+ }
644
+
645
+ if (repoSummary.errors.length) {
646
+ repoSummary.status = "partial";
415
647
  }
416
648
 
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`;
649
+ let section = `### ${name} (${dir || "."})\n\n`;
650
+ section += `# ?? Base vs HEAD (${repoSummary.base_ref}...HEAD | head=${headBranch})\n`;
651
+ if (baseDiff) section += `${baseDiff}\n\n`;
652
+ if (staged) section += `# ?? Staged changes\n${staged}\n\n`;
653
+ if (unstaged) section += `# ?? Unstaged changes\n${unstaged}\n\n`;
654
+ if (untrackedDiffs) section += `# ?? Untracked files (full contents below)\n${untrackedDiffs}\n`;
423
655
 
424
- if (!baseDiff && !staged && !unstaged && !untrackedDiffs) {
425
- output += "_No local changes - working tree clean_\n\n";
656
+ if (!repoSummary.has_changes) {
657
+ section += "_No local changes - working tree clean_\n\n";
426
658
  }
659
+ sections.push(section);
660
+ diagnostics.repo_summaries.push(repoSummary);
661
+ if (repoSummary.has_changes) diagnostics.collected_any = true;
427
662
  }
428
663
 
664
+ let output = sections.join("\n");
665
+ diagnostics.collected_text_length = output.length;
666
+ if (!diagnostics.collected_any && !diagnostics.reason) {
667
+ diagnostics.reason = "no_changes_detected";
668
+ }
429
669
  if (Number.isFinite(maxChars) && maxChars > 0 && output.length > maxChars) {
430
- return `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
670
+ diagnostics.truncated = true;
671
+ output = `${output.slice(0, maxChars)}\n...[truncated to ${maxChars} chars]`;
431
672
  }
432
- return output.trim();
673
+ output = output.trim();
674
+ return {
675
+ text: output,
676
+ diagnostics,
677
+ };
433
678
  } catch (err) {
434
- console.warn("Failed to collect git diffs:", err?.message || err);
435
- return "";
679
+ diagnostics.errors.push(safeText(err?.message || err));
680
+ diagnostics.reason = "collection_error";
681
+ return {
682
+ text: "",
683
+ diagnostics,
684
+ };
436
685
  }
437
686
  }
438
687
 
688
+ function collectGitDiff(options = {}) {
689
+ return collectGitDiffWithDiagnostics(options).text;
690
+ }
691
+
439
692
  async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
440
693
  const fetchFn = await getFetch();
441
694
  const url = `${apiBase}/project-assistant/config`;
@@ -458,9 +711,31 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
458
711
  return body.data || {};
459
712
  }
460
713
 
461
- async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
714
+ async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
462
715
  const fetchFn = await getFetch();
463
- const url = `${apiBase}/project-assistant/query`;
716
+ const url = `${apiBase}/project-assistant/bootstrap`;
717
+ const { resp, body } = await fetchJsonWithTimeout(
718
+ fetchFn,
719
+ url,
720
+ {
721
+ method: "GET",
722
+ headers: { Authorization: `Bearer ${key}` },
723
+ },
724
+ timeoutMs
725
+ );
726
+
727
+ if (!resp.ok || body.status !== "success") {
728
+ const msg = body?.message || `Bootstrap request failed (${resp.status})`;
729
+ const err = new Error(msg);
730
+ err.status = resp.status;
731
+ throw err;
732
+ }
733
+ return body.data || {};
734
+ }
735
+
736
+ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
737
+ const fetchFn = await getFetch();
738
+ const url = `${apiBase}${endpoint}`;
464
739
  const { resp, body } = await fetchJsonWithTimeout(
465
740
  fetchFn,
466
741
  url,
@@ -484,6 +759,231 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs }) {
484
759
  return body.data || {};
485
760
  }
486
761
 
762
+ function ensureDir(dirPath) {
763
+ fs.mkdirSync(dirPath, { recursive: true });
764
+ }
765
+
766
+ function clearYamlDirectory(dirPath) {
767
+ if (!fs.existsSync(dirPath)) {
768
+ fs.mkdirSync(dirPath, { recursive: true });
769
+ return;
770
+ }
771
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
772
+ if (!entry.isFile()) continue;
773
+ if (!entry.name.toLowerCase().endsWith(".yml")) continue;
774
+ fs.rmSync(path.join(dirPath, entry.name), { force: true });
775
+ }
776
+ }
777
+
778
+ function stableItemId(item, keys, fallback) {
779
+ for (const key of keys) {
780
+ const value = String(item?.[key] || "").trim();
781
+ if (value) return value;
782
+ }
783
+ return fallback;
784
+ }
785
+
786
+ function stringifyYaml(value) {
787
+ // eslint-disable-next-line global-require
788
+ const YAML = require("yaml");
789
+ return YAML.stringify(value, { lineWidth: 0 });
790
+ }
791
+
792
+ function writeYamlFile(filePath, value) {
793
+ ensureDir(path.dirname(filePath));
794
+ fs.writeFileSync(filePath, stringifyYaml(value), "utf8");
795
+ }
796
+
797
+ function writeJsonFile(filePath, value) {
798
+ ensureDir(path.dirname(filePath));
799
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
800
+ }
801
+
802
+ function resolveBootstrapWorkspace(repoNames) {
803
+ const resolved = resolveConfiguredRepos(repoNames);
804
+ if (!resolved.root || !Array.isArray(resolved.repos) || !resolved.repos.length) {
805
+ const names = Array.isArray(repoNames) ? repoNames.join(", ") : "";
806
+ throw new Error(
807
+ `No configured project repos were found from the current workspace. Expected child folders matching: ${names || "(none)"}`
808
+ );
809
+ }
810
+ return resolved;
811
+ }
812
+
813
+ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
814
+ const targetRoot = outputDir
815
+ ? path.resolve(process.cwd(), String(outputDir))
816
+ : path.join(wrapperRoot, "MyteCommandCenter");
817
+ const dataRoot = path.join(targetRoot, "data");
818
+ const phasesDir = path.join(dataRoot, "phases");
819
+ const epicsDir = path.join(dataRoot, "epics");
820
+ const storiesDir = path.join(dataRoot, "stories");
821
+ const missionsDir = path.join(dataRoot, "missions");
822
+
823
+ ensureDir(dataRoot);
824
+ clearYamlDirectory(phasesDir);
825
+ clearYamlDirectory(epicsDir);
826
+ clearYamlDirectory(storiesDir);
827
+ clearYamlDirectory(missionsDir);
828
+
829
+ const phases = Array.isArray(snapshot.phases) ? snapshot.phases : [];
830
+ const epics = Array.isArray(snapshot.epics) ? snapshot.epics : [];
831
+ const stories = Array.isArray(snapshot.stories) ? snapshot.stories : [];
832
+ const missions = Array.isArray(snapshot.missions) ? snapshot.missions : [];
833
+
834
+ phases.forEach((phase, index) => {
835
+ const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
836
+ writeYamlFile(path.join(phasesDir, `${phaseId}.yml`), phase);
837
+ });
838
+ epics.forEach((epic, index) => {
839
+ const epicId = stableItemId(epic, ["epic_id", "id"], `E${String(index + 1).padStart(3, "0")}`);
840
+ writeYamlFile(path.join(epicsDir, `${epicId}.yml`), epic);
841
+ });
842
+ stories.forEach((story, index) => {
843
+ const storyId = stableItemId(story, ["story_id", "id"], `S${String(index + 1).padStart(3, "0")}`);
844
+ writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
845
+ });
846
+ missions.forEach((mission, index) => {
847
+ const missionId = stableItemId(mission, ["mission_id", "id", "_id"], `M${String(index + 1).padStart(3, "0")}`);
848
+ writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
849
+ });
850
+
851
+ if (snapshot.project && typeof snapshot.project === "object") {
852
+ writeYamlFile(path.join(dataRoot, "project.yml"), snapshot.project);
853
+ }
854
+
855
+ const manifest = {
856
+ schema_version: snapshot.schema_version || 1,
857
+ generated_at: snapshot.generated_at || null,
858
+ snapshot_hash: snapshot.snapshot_hash || null,
859
+ project: snapshot.project || null,
860
+ repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
861
+ counts: {
862
+ phases: phases.length,
863
+ epics: epics.length,
864
+ stories: stories.length,
865
+ missions: missions.length,
866
+ },
867
+ };
868
+ writeJsonFile(path.join(dataRoot, "bootstrap-manifest.json"), manifest);
869
+
870
+ return {
871
+ targetRoot,
872
+ dataRoot,
873
+ manifest,
874
+ };
875
+ }
876
+
877
+ async function runCreatePrd(args) {
878
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
879
+ if (!key) {
880
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
881
+ process.exit(1);
882
+ }
883
+ const printContext = Boolean(args["print-context"] || args.printContext || args["dry-run"] || args.dryRun);
884
+
885
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
886
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
887
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
888
+
889
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
890
+ const apiBase = normalizeApiBase(baseRaw);
891
+
892
+ const providedPath = args.file || args.path || (Array.isArray(args._) ? args._[0] : "");
893
+ const useStdin = Boolean(args.stdin || (!process.stdin.isTTY && !providedPath));
894
+
895
+ let sourceText = "";
896
+ let filePath = "";
897
+ if (useStdin) {
898
+ sourceText = String(await readStdinText() || "");
899
+ } else {
900
+ filePath = String(providedPath || "").trim();
901
+ if (!filePath) {
902
+ console.error("Missing PRD file path.");
903
+ printHelp();
904
+ process.exit(1);
905
+ }
906
+ const absPath = path.resolve(filePath);
907
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
908
+ console.error(`PRD file not found: ${absPath}`);
909
+ process.exit(1);
910
+ }
911
+ sourceText = fs.readFileSync(absPath, "utf8");
912
+ filePath = absPath;
913
+ }
914
+
915
+ const trimmedSource = String(sourceText || "").trim();
916
+ if (!trimmedSource) {
917
+ console.error("PRD content is empty.");
918
+ process.exit(1);
919
+ }
920
+
921
+ const inferredTitle = String(args.title || extractMarkdownTitle(trimmedSource) || (!useStdin && filePath ? path.parse(filePath).name : "")).trim();
922
+ const description = String(args.description || args["feedback-text"] || args.feedbackText || "").trim();
923
+ const payload = isMyteKanbanTicket(trimmedSource)
924
+ ? {
925
+ ticket_markdown: trimmedSource,
926
+ }
927
+ : {
928
+ prd_markdown: trimmedSource,
929
+ title: inferredTitle,
930
+ };
931
+
932
+ if (!payload.ticket_markdown && description) {
933
+ payload.description = description;
934
+ }
935
+
936
+ if (!payload.ticket_markdown && !payload.title) {
937
+ console.error("A title is required when uploading raw markdown without a myte-kanban metadata block. Use --title or add a top-level # heading.");
938
+ process.exit(1);
939
+ }
940
+
941
+ if (printContext) {
942
+ console.log(JSON.stringify(payload, null, 2));
943
+ process.exit(0);
944
+ }
945
+
946
+ let data;
947
+ try {
948
+ data = await callAssistantQuery({
949
+ apiBase,
950
+ key,
951
+ payload,
952
+ timeoutMs,
953
+ endpoint: "/project-assistant/create-prd",
954
+ });
955
+ } catch (err) {
956
+ if (err?.name === "AbortError") {
957
+ console.error(`Request timed out after ${timeoutMs}ms`);
958
+ } else {
959
+ console.error("PRD upload failed:", err?.message || err);
960
+ }
961
+ process.exit(1);
962
+ }
963
+
964
+ if (args.json) {
965
+ console.log(
966
+ JSON.stringify(
967
+ {
968
+ feedback_id: data.feedback_id || null,
969
+ project_id: data.project_id || null,
970
+ title: data.title || null,
971
+ status: data.status || null,
972
+ deterministic: data.deterministic === true,
973
+ },
974
+ null,
975
+ 2
976
+ )
977
+ );
978
+ return;
979
+ }
980
+
981
+ if (data.feedback_id) console.log(`Feedback ID: ${data.feedback_id}`);
982
+ if (data.project_id) console.log(`Project ID: ${data.project_id}`);
983
+ if (data.title) console.log(`Title: ${data.title}`);
984
+ if (data.status) console.log(`Status: ${data.status}`);
985
+ }
986
+
487
987
  async function runConfig(args) {
488
988
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
489
989
  if (!key) {
@@ -532,6 +1032,100 @@ async function runConfig(args) {
532
1032
  }
533
1033
  }
534
1034
 
1035
+ async function runBootstrap(args) {
1036
+ const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
1037
+ if (!key) {
1038
+ console.error("Missing MYTE_API_KEY (project key) in environment/.env");
1039
+ process.exit(1);
1040
+ }
1041
+
1042
+ const timeoutRaw = args["timeout-ms"] || args.timeoutMs || args.timeout_ms;
1043
+ const timeoutParsed = timeoutRaw !== undefined ? Number(timeoutRaw) : 300_000;
1044
+ const timeoutMs = Number.isFinite(timeoutParsed) ? timeoutParsed : 300_000;
1045
+
1046
+ const baseRaw = args["base-url"] || args.baseUrl || args.base_url || process.env.MYTE_API_BASE || DEFAULT_API_BASE;
1047
+ const apiBase = normalizeApiBase(baseRaw);
1048
+
1049
+ let snapshot;
1050
+ try {
1051
+ snapshot = await fetchBootstrapSnapshot({ apiBase, key, timeoutMs });
1052
+ } catch (err) {
1053
+ console.error("Failed to fetch bootstrap snapshot:", err?.message || err);
1054
+ process.exit(1);
1055
+ }
1056
+
1057
+ if (args["print-context"] || args.printContext) {
1058
+ console.log(JSON.stringify(snapshot, null, 2));
1059
+ return;
1060
+ }
1061
+
1062
+ let resolved;
1063
+ try {
1064
+ resolved = resolveBootstrapWorkspace(snapshot.repo_names || []);
1065
+ } catch (err) {
1066
+ console.error(err?.message || err);
1067
+ process.exit(1);
1068
+ }
1069
+
1070
+ const wrapperRoot = resolved.root;
1071
+ const outputDir = args["output-dir"] || args.outputDir || args.output_dir;
1072
+ const dryRun = Boolean(args["dry-run"] || args.dryRun);
1073
+ const summary = {
1074
+ api_base: apiBase,
1075
+ project_id: snapshot?.project?.id || null,
1076
+ wrapper_root: wrapperRoot,
1077
+ output_root: outputDir ? path.resolve(process.cwd(), String(outputDir)) : path.join(wrapperRoot, "MyteCommandCenter"),
1078
+ repo_names: Array.isArray(snapshot.repo_names) ? snapshot.repo_names : [],
1079
+ local: {
1080
+ mode: resolved.mode,
1081
+ found: (resolved.repos || []).map((repo) => repo.name),
1082
+ missing: resolved.missing || [],
1083
+ },
1084
+ counts: {
1085
+ phases: Array.isArray(snapshot.phases) ? snapshot.phases.length : 0,
1086
+ epics: Array.isArray(snapshot.epics) ? snapshot.epics.length : 0,
1087
+ stories: Array.isArray(snapshot.stories) ? snapshot.stories.length : 0,
1088
+ missions: Array.isArray(snapshot.missions) ? snapshot.missions.length : 0,
1089
+ },
1090
+ snapshot_hash: snapshot.snapshot_hash || null,
1091
+ generated_at: snapshot.generated_at || null,
1092
+ dry_run: dryRun,
1093
+ };
1094
+
1095
+ if (dryRun) {
1096
+ if (args.json) {
1097
+ console.log(JSON.stringify(summary, null, 2));
1098
+ } else {
1099
+ console.log(`Project: ${summary.project_id || "(unknown)"}`);
1100
+ console.log(`Wrapper root: ${summary.wrapper_root}`);
1101
+ console.log(`Output root: ${summary.output_root}`);
1102
+ console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
1103
+ console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
1104
+ if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
1105
+ console.log(`Counts: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
1106
+ console.log("Dry run only - no files written.");
1107
+ }
1108
+ return;
1109
+ }
1110
+
1111
+ const writeResult = writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir });
1112
+ summary.data_root = writeResult.dataRoot;
1113
+
1114
+ if (args.json) {
1115
+ console.log(JSON.stringify(summary, null, 2));
1116
+ return;
1117
+ }
1118
+
1119
+ console.log(`Project: ${summary.project_id || "(unknown)"}`);
1120
+ console.log(`Wrapper root: ${summary.wrapper_root}`);
1121
+ console.log(`Output root: ${summary.output_root}`);
1122
+ console.log(`Configured repos: ${summary.repo_names.join(", ") || "(none)"}`);
1123
+ console.log(`Found locally: ${summary.local.found.join(", ") || "(none)"}`);
1124
+ if (summary.local.missing.length) console.log(`Missing locally: ${summary.local.missing.join(", ")}`);
1125
+ console.log(`Wrote: phases=${summary.counts.phases}, epics=${summary.counts.epics}, stories=${summary.counts.stories}, missions=${summary.counts.missions}`);
1126
+ console.log(`Snapshot: ${summary.snapshot_hash || "n/a"}`);
1127
+ }
1128
+
535
1129
  async function runQuery(args) {
536
1130
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
537
1131
  if (!key) {
@@ -563,6 +1157,7 @@ async function runQuery(args) {
563
1157
  const apiBase = normalizeApiBase(baseRaw);
564
1158
 
565
1159
  let diffText = "";
1160
+ let diffDiagnostics = null;
566
1161
  if (includeDiff) {
567
1162
  let cfg = null;
568
1163
  try {
@@ -575,14 +1170,20 @@ async function runQuery(args) {
575
1170
 
576
1171
  if (cfg) {
577
1172
  const repoNames = Array.isArray(cfg.repo_names) ? cfg.repo_names : [];
578
- diffText = collectGitDiff({
1173
+ const diffResult = collectGitDiffWithDiagnostics({
579
1174
  projectId: cfg.project_id,
580
1175
  repoNames,
581
1176
  maxChars: diffLimit,
582
1177
  fetchRemote,
583
1178
  });
1179
+ diffText = diffResult.text;
1180
+ diffDiagnostics = diffResult.diagnostics;
1181
+ if (diffDiagnostics?.errors?.length) {
1182
+ for (const warning of diffDiagnostics.errors) console.warn(`Warning: ${warning}`);
1183
+ }
584
1184
  } else {
585
1185
  diffText = "";
1186
+ diffDiagnostics = null;
586
1187
  }
587
1188
  if (!diffText) {
588
1189
  console.error("Warning: no diff context collected. Continuing without --with-diff context.");
@@ -591,8 +1192,9 @@ async function runQuery(args) {
591
1192
 
592
1193
  const payload = {
593
1194
  query,
594
- additional_context: buildContext(args, diffText),
1195
+ additional_context: buildContext(args, diffText, diffDiagnostics),
595
1196
  };
1197
+ if (diffDiagnostics) payload.diff_diagnostics = diffDiagnostics;
596
1198
 
597
1199
  if (printContext) {
598
1200
  console.log(JSON.stringify(payload, null, 2));
@@ -657,11 +1259,21 @@ async function main() {
657
1259
  return;
658
1260
  }
659
1261
 
1262
+ if (command === "bootstrap") {
1263
+ await runBootstrap(args);
1264
+ return;
1265
+ }
1266
+
660
1267
  if (command === "chat") {
661
1268
  await runChat(args);
662
1269
  return;
663
1270
  }
664
1271
 
1272
+ if (command === "create-prd" || command === "add-prd" || command === "prd") {
1273
+ await runCreatePrd(args);
1274
+ return;
1275
+ }
1276
+
665
1277
  // query/ask default
666
1278
  await runQuery(args);
667
1279
  }
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.5",
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"
@@ -14,6 +22,7 @@
14
22
  "dependencies": {
15
23
  "dotenv": "^16.5.0",
16
24
  "minimist": "^1.2.8",
17
- "node-fetch": "^3.3.2"
25
+ "node-fetch": "^3.3.2",
26
+ "yaml": "^2.8.1"
18
27
  }
19
28
  }