mrvn-cli 0.2.8 → 0.2.10

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.
@@ -39,6 +39,26 @@ import * as fs from "fs";
39
39
  import * as path from "path";
40
40
  import * as os from "os";
41
41
  import * as YAML from "yaml";
42
+ function userConfigDir() {
43
+ return path.join(os.homedir(), ".config", "marvin");
44
+ }
45
+ function userConfigPath() {
46
+ return path.join(userConfigDir(), "config.yaml");
47
+ }
48
+ function loadUserConfig() {
49
+ const configPath = userConfigPath();
50
+ if (!fs.existsSync(configPath)) {
51
+ return {};
52
+ }
53
+ try {
54
+ const raw = fs.readFileSync(configPath, "utf-8");
55
+ return YAML.parse(raw) ?? {};
56
+ } catch (err) {
57
+ throw new ConfigError(
58
+ `Failed to parse user config at ${configPath}: ${err}`
59
+ );
60
+ }
61
+ }
42
62
  function loadProjectConfig(marvinDir) {
43
63
  const configPath = path.join(marvinDir, "config.yaml");
44
64
  if (!fs.existsSync(configPath)) {
@@ -146,6 +166,11 @@ var DocumentStore = class {
146
166
  const raw = fs3.readFileSync(filePath, "utf-8");
147
167
  const doc = parseDocument(raw, filePath);
148
168
  if (doc.frontmatter.id) {
169
+ if (this.index.has(doc.frontmatter.id)) {
170
+ console.warn(
171
+ `[marvin] Duplicate ID "${doc.frontmatter.id}" in ${file2} \u2014 conflicts with existing entry. Run ID repair to fix.`
172
+ );
173
+ }
149
174
  this.index.set(doc.frontmatter.id, doc.frontmatter);
150
175
  }
151
176
  }
@@ -269,14 +294,19 @@ var DocumentStore = class {
269
294
  const dirName = this.typeDirs[type];
270
295
  const dir = path3.join(this.docsDir, dirName);
271
296
  if (!fs3.existsSync(dir)) return `${prefix}-001`;
297
+ const idPattern = new RegExp(`^${prefix}-(\\d+)$`);
272
298
  const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md"));
273
299
  let maxNum = 0;
274
300
  for (const file2 of files) {
275
- const match = file2.match(new RegExp(`^${prefix}-(\\d+)\\.md$`));
301
+ const filePath = path3.join(dir, file2);
302
+ const raw = fs3.readFileSync(filePath, "utf-8");
303
+ const doc = parseDocument(raw, filePath);
304
+ const match = doc.frontmatter.id?.match(idPattern);
276
305
  if (match) {
277
306
  maxNum = Math.max(maxNum, parseInt(match[1], 10));
278
307
  }
279
308
  }
309
+ maxNum = Math.max(maxNum, files.length);
280
310
  return `${prefix}-${String(maxNum + 1).padStart(3, "0")}`;
281
311
  }
282
312
  counts() {
@@ -17322,12 +17352,12 @@ var JiraClient = class {
17322
17352
  );
17323
17353
  }
17324
17354
  };
17325
- function createJiraClient() {
17326
- const host = process.env.JIRA_HOST;
17327
- const email3 = process.env.JIRA_EMAIL;
17328
- const apiToken = process.env.JIRA_API_TOKEN;
17355
+ function createJiraClient(jiraUserConfig) {
17356
+ const host = jiraUserConfig?.host ?? process.env.JIRA_HOST;
17357
+ const email3 = jiraUserConfig?.email ?? process.env.JIRA_EMAIL;
17358
+ const apiToken = jiraUserConfig?.apiToken ?? process.env.JIRA_API_TOKEN;
17329
17359
  if (!host || !email3 || !apiToken) return null;
17330
- return new JiraClient({ host, email: email3, apiToken });
17360
+ return { client: new JiraClient({ host, email: email3, apiToken }), host };
17331
17361
  }
17332
17362
 
17333
17363
  // src/skills/builtin/jira/tools.ts
@@ -17337,7 +17367,7 @@ function jiraNotConfiguredError() {
17337
17367
  content: [
17338
17368
  {
17339
17369
  type: "text",
17340
- text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
17370
+ text: 'Jira is not configured. Run "marvin config jira" or set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.'
17341
17371
  }
17342
17372
  ],
17343
17373
  isError: true
@@ -17369,6 +17399,7 @@ function findByJiraKey(store, jiraKey) {
17369
17399
  return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
17370
17400
  }
17371
17401
  function createJiraTools(store) {
17402
+ const jiraUserConfig = loadUserConfig().jira;
17372
17403
  return [
17373
17404
  // --- Local read tools ---
17374
17405
  tool19(
@@ -17439,15 +17470,14 @@ function createJiraTools(store) {
17439
17470
  key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
17440
17471
  },
17441
17472
  async (args) => {
17442
- const client = createJiraClient();
17443
- if (!client) return jiraNotConfiguredError();
17444
- const issue2 = await client.getIssue(args.key);
17445
- const host = process.env.JIRA_HOST;
17473
+ const jira = createJiraClient(jiraUserConfig);
17474
+ if (!jira) return jiraNotConfiguredError();
17475
+ const issue2 = await jira.client.getIssue(args.key);
17446
17476
  const existing = findByJiraKey(store, args.key);
17447
17477
  if (existing) {
17448
17478
  const fm2 = jiraIssueToFrontmatter(
17449
17479
  issue2,
17450
- host,
17480
+ jira.host,
17451
17481
  existing.frontmatter.linkedArtifacts
17452
17482
  );
17453
17483
  const doc2 = store.update(
@@ -17464,7 +17494,7 @@ function createJiraTools(store) {
17464
17494
  ]
17465
17495
  };
17466
17496
  }
17467
- const fm = jiraIssueToFrontmatter(issue2, host);
17497
+ const fm = jiraIssueToFrontmatter(issue2, jira.host);
17468
17498
  const doc = store.create(
17469
17499
  JIRA_TYPE,
17470
17500
  fm,
@@ -17488,10 +17518,9 @@ function createJiraTools(store) {
17488
17518
  maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
17489
17519
  },
17490
17520
  async (args) => {
17491
- const client = createJiraClient();
17492
- if (!client) return jiraNotConfiguredError();
17493
- const result = await client.searchIssues(args.jql, args.maxResults);
17494
- const host = process.env.JIRA_HOST;
17521
+ const jira = createJiraClient(jiraUserConfig);
17522
+ if (!jira) return jiraNotConfiguredError();
17523
+ const result = await jira.client.searchIssues(args.jql, args.maxResults);
17495
17524
  const created = [];
17496
17525
  const updated = [];
17497
17526
  for (const issue2 of result.issues) {
@@ -17499,7 +17528,7 @@ function createJiraTools(store) {
17499
17528
  if (existing) {
17500
17529
  const fm = jiraIssueToFrontmatter(
17501
17530
  issue2,
17502
- host,
17531
+ jira.host,
17503
17532
  existing.frontmatter.linkedArtifacts
17504
17533
  );
17505
17534
  store.update(
@@ -17509,7 +17538,7 @@ function createJiraTools(store) {
17509
17538
  );
17510
17539
  updated.push(`${existing.frontmatter.id} (${issue2.key})`);
17511
17540
  } else {
17512
- const fm = jiraIssueToFrontmatter(issue2, host);
17541
+ const fm = jiraIssueToFrontmatter(issue2, jira.host);
17513
17542
  const doc = store.create(
17514
17543
  JIRA_TYPE,
17515
17544
  fm,
@@ -17538,8 +17567,8 @@ function createJiraTools(store) {
17538
17567
  issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
17539
17568
  },
17540
17569
  async (args) => {
17541
- const client = createJiraClient();
17542
- if (!client) return jiraNotConfiguredError();
17570
+ const jira = createJiraClient(jiraUserConfig);
17571
+ if (!jira) return jiraNotConfiguredError();
17543
17572
  const artifact = store.get(args.artifactId);
17544
17573
  if (!artifact) {
17545
17574
  return {
@@ -17556,20 +17585,19 @@ function createJiraTools(store) {
17556
17585
  `Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
17557
17586
  `Status: ${artifact.frontmatter.status}`
17558
17587
  ].join("\n");
17559
- const jiraResult = await client.createIssue({
17588
+ const jiraResult = await jira.client.createIssue({
17560
17589
  project: { key: args.projectKey },
17561
17590
  summary: artifact.frontmatter.title,
17562
17591
  description,
17563
17592
  issuetype: { name: args.issueType ?? "Task" }
17564
17593
  });
17565
- const host = process.env.JIRA_HOST;
17566
17594
  const jiDoc = store.create(
17567
17595
  JIRA_TYPE,
17568
17596
  {
17569
17597
  title: artifact.frontmatter.title,
17570
17598
  status: "open",
17571
17599
  jiraKey: jiraResult.key,
17572
- jiraUrl: `https://${host}/browse/${jiraResult.key}`,
17600
+ jiraUrl: `https://${jira.host}/browse/${jiraResult.key}`,
17573
17601
  issueType: args.issueType ?? "Task",
17574
17602
  priority: "Medium",
17575
17603
  assignee: "",
@@ -17598,8 +17626,8 @@ function createJiraTools(store) {
17598
17626
  id: external_exports.string().describe("Local JI-xxx ID")
17599
17627
  },
17600
17628
  async (args) => {
17601
- const client = createJiraClient();
17602
- if (!client) return jiraNotConfiguredError();
17629
+ const jira = createJiraClient(jiraUserConfig);
17630
+ if (!jira) return jiraNotConfiguredError();
17603
17631
  const doc = store.get(args.id);
17604
17632
  if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
17605
17633
  return {
@@ -17610,15 +17638,14 @@ function createJiraTools(store) {
17610
17638
  };
17611
17639
  }
17612
17640
  const jiraKey = doc.frontmatter.jiraKey;
17613
- await client.updateIssue(jiraKey, {
17641
+ await jira.client.updateIssue(jiraKey, {
17614
17642
  summary: doc.frontmatter.title,
17615
17643
  description: doc.content || void 0
17616
17644
  });
17617
- const issue2 = await client.getIssue(jiraKey);
17618
- const host = process.env.JIRA_HOST;
17645
+ const issue2 = await jira.client.getIssue(jiraKey);
17619
17646
  const fm = jiraIssueToFrontmatter(
17620
17647
  issue2,
17621
- host,
17648
+ jira.host,
17622
17649
  doc.frontmatter.linkedArtifacts
17623
17650
  );
17624
17651
  store.update(args.id, fm, issue2.fields.description ?? "");