@mytegroupinc/myte-core 0.0.4 → 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 +20 -0
  2. package/cli.js +263 -6
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -3,9 +3,12 @@
3
3
  Internal implementation package for the `myte` CLI.
4
4
 
5
5
  Most users should install the unscoped wrapper instead:
6
+ - `npm install myte` then `npx myte bootstrap`
6
7
  - `npm install myte` then `npx myte query "..." --with-diff`
7
8
  - `npm install myte` then `npm exec myte -- query "..." --with-diff`
9
+ - `npm i -g myte` then `myte bootstrap`
8
10
  - `npm i -g myte` then `myte query "..." --with-diff`
11
+ - `npx myte@latest bootstrap`
9
12
  - `npx myte@latest query "..." --with-diff`
10
13
  - `npm install myte` then `npx myte create-prd ./drafts/auth-prd.md`
11
14
  - `cat ./drafts/auth-prd.md | npx myte create-prd --stdin`
@@ -20,6 +23,9 @@ Requirements:
20
23
  Notes:
21
24
  - `npm install myte` installs the wrapper locally; use `npx myte` or `npm exec myte -- ...` to run it.
22
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`.
23
29
  - `create-prd` is a deterministic PRD upload path, not an LLM generation command.
24
30
  - `--with-diff` only searches repo folders whose names match the project repo names configured in Myte.
25
31
  - `--with-diff` includes per-repo diagnostics in `print-context` payload:
@@ -28,4 +34,18 @@ Notes:
28
34
  - clean/no-change repo summaries
29
35
  - `--with-diff` query payload includes `diff_diagnostics` so backend/UI can report exactly why context may be missing.
30
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
+
31
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", "create-prd", "add-prd", "prd", "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;
@@ -63,7 +63,7 @@ function parseArgs(argv) {
63
63
  // eslint-disable-next-line global-require
64
64
  return require("minimist")(argv, {
65
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"],
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,10 +116,11 @@ 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",
120
- " myte create-prd <file.md> [--json]",
121
+ " myte create-prd <file.md> [--json] [--title \"...\"] [--description \"...\"]",
121
122
  " myte add-prd <file.md> [--json]",
122
- " cat file.md | myte create-prd --stdin [--title \"...\"]",
123
+ " cat file.md | myte create-prd --stdin [--title \"...\"] [--description \"...\"]",
123
124
  "",
124
125
  "Run forms:",
125
126
  " npm install myte then npx myte query \"...\" --with-diff",
@@ -130,19 +131,34 @@ function printHelp() {
130
131
  "Auth:",
131
132
  " - Set MYTE_API_KEY in a workspace .env (or env var)",
132
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
+ "",
133
145
  "Options:",
134
146
  " --with-diff Include deterministic git diffs (project-scoped)",
135
147
  " --diff-limit <chars> Truncate diff context to N chars (default: 200000)",
136
148
  " --timeout-ms <ms> Request timeout (default: 300000)",
137
149
  " --base-url <url> API base (default: https://api.myte.dev)",
150
+ " --output-dir <path> Bootstrap output directory (default: <wrapper-root>/MyteCommandCenter)",
138
151
  " --stdin Read PRD content from stdin instead of a file path",
139
- " --title <text> Required only when stdin/raw markdown has no H1 title",
152
+ " --title <text> Override PRD title for raw markdown uploads",
153
+ " --description <text> Set feedback description/card summary for raw markdown uploads",
140
154
  " --print-context Print JSON payload and exit (no query call)",
141
155
  " --no-fetch Don't git fetch origin main/master before diff",
142
156
  "",
143
157
  "Examples:",
144
158
  " myte query \"What changed in logging?\" --with-diff",
145
- " myte create-prd ./drafts/auth-prd.md",
159
+ " myte bootstrap",
160
+ " myte bootstrap --output-dir ./MyteCommandCenter",
161
+ " myte create-prd ./drafts/auth-prd.md --description \"Short card summary\"",
146
162
  " cat ./drafts/auth-prd.md | myte create-prd --stdin",
147
163
  " myte config",
148
164
  ].join("\n");
@@ -695,6 +711,28 @@ async function fetchProjectConfig({ apiBase, key, timeoutMs }) {
695
711
  return body.data || {};
696
712
  }
697
713
 
714
+ async function fetchBootstrapSnapshot({ apiBase, key, timeoutMs }) {
715
+ const fetchFn = await getFetch();
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
+
698
736
  async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
699
737
  const fetchFn = await getFetch();
700
738
  const url = `${apiBase}${endpoint}`;
@@ -721,6 +759,121 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
721
759
  return body.data || {};
722
760
  }
723
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
+
724
877
  async function runCreatePrd(args) {
725
878
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
726
879
  if (!key) {
@@ -766,6 +919,7 @@ async function runCreatePrd(args) {
766
919
  }
767
920
 
768
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();
769
923
  const payload = isMyteKanbanTicket(trimmedSource)
770
924
  ? {
771
925
  ticket_markdown: trimmedSource,
@@ -775,6 +929,10 @@ async function runCreatePrd(args) {
775
929
  title: inferredTitle,
776
930
  };
777
931
 
932
+ if (!payload.ticket_markdown && description) {
933
+ payload.description = description;
934
+ }
935
+
778
936
  if (!payload.ticket_markdown && !payload.title) {
779
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.");
780
938
  process.exit(1);
@@ -874,6 +1032,100 @@ async function runConfig(args) {
874
1032
  }
875
1033
  }
876
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
+
877
1129
  async function runQuery(args) {
878
1130
  const key = (process.env.MYTE_API_KEY || process.env.MYTE_PROJECT_API_KEY || "").trim();
879
1131
  if (!key) {
@@ -1007,6 +1259,11 @@ async function main() {
1007
1259
  return;
1008
1260
  }
1009
1261
 
1262
+ if (command === "bootstrap") {
1263
+ await runBootstrap(args);
1264
+ return;
1265
+ }
1266
+
1010
1267
  if (command === "chat") {
1011
1268
  await runChat(args);
1012
1269
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.4",
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",
@@ -22,6 +22,7 @@
22
22
  "dependencies": {
23
23
  "dotenv": "^16.5.0",
24
24
  "minimist": "^1.2.8",
25
- "node-fetch": "^3.3.2"
25
+ "node-fetch": "^3.3.2",
26
+ "yaml": "^2.8.1"
26
27
  }
27
28
  }