@neobiotechlabs/neobiotech-dev-agent 0.1.35 → 0.1.37

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.
@@ -10,7 +10,7 @@
10
10
  "source": "npm",
11
11
  "package": "@neobiotechlabs/neobiotech-dev-agent",
12
12
  "registry": "https://registry.npmjs.org",
13
- "version": "0.1.35"
13
+ "version": "0.1.37"
14
14
  },
15
15
  "description": "의료기기 SW 규제 개발 자동화 (Jira·Confluence·Risk·CVSS·Xray·IEC 62304/62366·MDR·문서 렌더)",
16
16
  "category": "medical-device",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neobiotech-dev-agent",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "displayName": "NeoBiotech Dev Agent (MedDev)",
5
5
  "description": "의료기기 SW 규제 개발 자동화 — Jira·Confluence·Risk·CVSS·Xray·IEC 62304/62366·MDR·문서 렌더",
6
6
  "author": {
package/dist/index.js CHANGED
@@ -159735,6 +159735,31 @@ true\r
159735
159735
  clearTimeout(timer);
159736
159736
  }
159737
159737
  }
159738
+ /**
159739
+ * 페이지 첨부 목록 조회. Confluence REST는 페이지 자체 attachments 도 expand 가능하지만
159740
+ * 명시적 list도 지원 — 여기선 GET child/attachment?limit=N.
159741
+ */
159742
+ async listAttachments(pageId, limit = 100) {
159743
+ const resp = await this.request(
159744
+ `${this.baseUrl}/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment?limit=${limit}`,
159745
+ { label: "listAttachments" }
159746
+ );
159747
+ const data = resp;
159748
+ const items = Array.isArray(data?.results) ? data.results : [];
159749
+ return items.map((it2) => ({ id: String(it2.id), title: String(it2.title ?? "") }));
159750
+ }
159751
+ /**
159752
+ * 첨부 삭제. POST .../child/attachment/{id}/remove 와 동일 — REST API.
159753
+ * 위 endpoint URL의 page_id는 첫 pageId(이 attachment가 속한 페이지)지만, GET 응답의 id 경로의
159754
+ * second segment가 attachment id. Confluence Cloud는 id로 직접 delete 가능:
159755
+ * DELETE /wiki/rest/api/content/{pageId}/child/attachment/{attachmentId}
159756
+ */
159757
+ async deleteAttachment(pageId, attachmentId) {
159758
+ await this.request(
159759
+ `${this.baseUrl}/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment/${encodeURIComponent(attachmentId)}`,
159760
+ { method: "DELETE", label: "deleteAttachment" }
159761
+ );
159762
+ }
159738
159763
  };
159739
159764
  function escapeMimeFilename2(name) {
159740
159765
  return name.replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
@@ -160094,7 +160119,8 @@ var registerJiraCrudTools = (ctx2) => {
160094
160119
  // src/tools/confluence.ts
160095
160120
  init_markdown();
160096
160121
  import { readFileSync } from "fs";
160097
- import { resolve as resolve2 } from "path";
160122
+ import { resolve as resolve2, dirname as dirname2 } from "path";
160123
+ import { fileURLToPath as fileURLToPath2 } from "url";
160098
160124
 
160099
160125
  // src/utils/plantuml-renderer.ts
160100
160126
  import { spawn } from "child_process";
@@ -160197,10 +160223,12 @@ async function renderPlantUml(source, opts = {}) {
160197
160223
  // src/tools/confluence.ts
160198
160224
  function readPluginVersion() {
160199
160225
  try {
160200
- const pkg = JSON.parse(readFileSync(resolve2(process.cwd(), "package.json"), "utf8"));
160201
- return pkg.version || "unknown";
160202
- } catch {
160203
- return "unknown-read-failed";
160226
+ const here = dirname2(fileURLToPath2(import.meta.url));
160227
+ const pkgPath = resolve2(here, "package.json");
160228
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
160229
+ return pkg.version || "unknown-no-version";
160230
+ } catch (err) {
160231
+ return "unknown-read-failed: " + (err?.message ?? String(err));
160204
160232
  }
160205
160233
  }
160206
160234
  var PLUGIN_VERSION = readPluginVersion();
@@ -160264,9 +160292,10 @@ var registerConfluenceTools = (ctx2) => {
160264
160292
  parent_page_id: external_exports.string().optional().describe("Parent page ID \u2014 create mode only, nests the new page under this parent. Ignored in update mode."),
160265
160293
  page_id: external_exports.string().optional().describe("Existing page ID. When provided, the page is UPDATED (current version fetched then incremented). When omitted, a NEW page is created."),
160266
160294
  render_diagrams: external_exports.boolean().optional().default(true).describe("Render ```plantuml fences to PNG/SVG and embed as attachments. Default true. Falls back to code block on render failure."),
160295
+ clean_stale_attachments: external_exports.boolean().optional().default(false).describe("update \uBAA8\uB4DC\uC77C \uB54C \uAC8C\uC7AC \uC9C1\uC804 \uD398\uC774\uC9C0\uC5D0\uC11C diagram-* prefix \uCCA8\uBD80 \uC77C\uAD04 \uC0AD\uC81C. \uB204\uC801 \uBC29\uC9C0\uC6A9. Default false (\uC548\uC804). \uCCAB \uAC8C\uC7AC(create)\uC5D4 \uD6A8\uACFC \uC5C6\uC74C."),
160267
160296
  diagram_format: external_exports.enum(["png", "svg"]).optional().default("png").describe("Image format for rendered diagrams. Default png."),
160268
160297
  diagram_dpi: external_exports.number().optional().default(96).describe("Rendering DPI (PNG only, ignored for SVG). Default 96.")
160269
- }, async ({ space_key, title, markdown, markdown_file, parent_page_id, page_id, render_diagrams = true, diagram_format = "png", diagram_dpi = 96 }) => {
160298
+ }, async ({ space_key, title, markdown, markdown_file, parent_page_id, page_id, render_diagrams = true, clean_stale_attachments = false, diagram_format = "png", diagram_dpi = 96 }) => {
160270
160299
  if (!markdown && !markdown_file) {
160271
160300
  throw new Error("Either markdown or markdown_file must be provided");
160272
160301
  }
@@ -160326,20 +160355,49 @@ var registerConfluenceTools = (ctx2) => {
160326
160355
  currentPageId = result?.id;
160327
160356
  currentVersion = result?.version?.number ?? 1;
160328
160357
  }
160358
+ if (mode === "update" && clean_stale_attachments) {
160359
+ renderTrace.push("cleanup entry");
160360
+ try {
160361
+ const existing = await confluence2.listAttachments(currentPageId);
160362
+ const targets = existing.filter((a) => /^diagram-/.test(a.title));
160363
+ let deleted = 0;
160364
+ for (const att of targets) {
160365
+ try {
160366
+ await confluence2.deleteAttachment(currentPageId, att.id);
160367
+ deleted++;
160368
+ } catch (err) {
160369
+ renderTrace.push(`cleanup delete FAIL ${att.title}: ${err?.message ?? err}`);
160370
+ }
160371
+ }
160372
+ renderTrace.push(`cleanup done: deleted=${deleted}/${targets.length}`);
160373
+ } catch (err) {
160374
+ renderTrace.push(`cleanup FAIL: ${err?.message ?? err}`);
160375
+ }
160376
+ }
160329
160377
  let finalStorage = storage;
160378
+ renderTrace.push(`round2 entry: renderedDiagrams.size=${renderedDiagrams.size}`);
160330
160379
  if (render_diagrams && renderedDiagrams.size > 0) {
160380
+ let attIter = 0;
160381
+ const attachmentNames = /* @__PURE__ */ new Map();
160331
160382
  for (const [id, diag] of renderedDiagrams) {
160332
- const filename = `diagram-${id}.${diag.format}`;
160383
+ attIter++;
160384
+ const filename = `diagram-${id}-${Date.now()}.${diag.format}`;
160333
160385
  try {
160334
160386
  await confluence2.addAttachment(currentPageId, filename, diag.buffer, diag.contentType);
160387
+ attachmentNames.set(id, filename);
160388
+ renderTrace.push(`attach ${attIter}/${renderedDiagrams.size}: ${filename} OK`);
160335
160389
  } catch (err) {
160390
+ renderTrace.push(`attach ${attIter}/${renderedDiagrams.size}: ${filename} FAIL \u2014 ${err?.message ?? err}`);
160336
160391
  console.error(`[confluence_publish_doc] attachment upload failed for ${id}: ${err?.message ?? err}`);
160337
160392
  renderedDiagrams.delete(id);
160338
160393
  diagramStats.rendered--;
160339
160394
  }
160340
160395
  }
160341
- finalStorage = replacePumlMarkers(storage, renderedDiagrams, pumlRegistry);
160396
+ renderTrace.push(`round2 pre-replace: renderedDiagrams.size=${renderedDiagrams.size}`);
160397
+ finalStorage = replacePumlMarkers(storage, renderedDiagrams, pumlRegistry, attachmentNames);
160398
+ renderTrace.push(`round2 post-replace: finalStorage.length=${finalStorage.length}, contains-image=${finalStorage.includes("<ac:image")}`);
160342
160399
  result = await confluence2.updatePage(currentPageId, title, finalStorage, currentVersion);
160400
+ renderTrace.push(`round2 update: v${result?.version?.number}, body_chars=${finalStorage.length}`);
160343
160401
  versionsUsed = 2;
160344
160402
  }
160345
160403
  const pageIdOut = result?.id ?? currentPageId;
@@ -160371,10 +160429,10 @@ var registerConfluenceTools = (ctx2) => {
160371
160429
  };
160372
160430
  });
160373
160431
  };
160374
- function replacePumlMarkers(storage, rendered, pumlRegistry) {
160432
+ function replacePumlMarkers(storage, rendered, pumlRegistry, attachmentNames) {
160375
160433
  let out = storage;
160376
160434
  for (const [id, diag] of rendered) {
160377
- const filename = `diagram-${id}.${diag.format}`;
160435
+ const filename = attachmentNames?.get(id) ?? `diagram-${id}.${diag.format}`;
160378
160436
  const imageMacro = `<ac:image ac:alt="PlantUML Diagram ${id}"><ri:attachment ri:filename="${filename}"/></ac:image>`;
160379
160437
  out = out.replace(
160380
160438
  new RegExp(`<p data-puml-id="${id}"></p>`),
@@ -162114,7 +162172,7 @@ var registerInitTools = ({ server: server2 }) => {
162114
162172
 
162115
162173
  // src/tools/doc-render.ts
162116
162174
  import { readFileSync as readFileSync3 } from "fs";
162117
- import { fileURLToPath as fileURLToPath2 } from "url";
162175
+ import { fileURLToPath as fileURLToPath3 } from "url";
162118
162176
  import path from "path";
162119
162177
 
162120
162178
  // src/utils/doc-render.ts
@@ -162179,7 +162237,7 @@ var TEMPLATE_VERSION = "17d0c4e";
162179
162237
 
162180
162238
  // src/tools/doc-render.ts
162181
162239
  init_adf();
162182
- var __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
162240
+ var __dirname2 = path.dirname(fileURLToPath3(import.meta.url));
162183
162241
  var TEMPLATE_DIR = path.resolve(__dirname2, "../vendor/dev-docs-templates/templates");
162184
162242
  var registerDocRenderTools = (ctx2) => {
162185
162243
  const { server: server2, jira: jira2 } = ctx2;
@@ -184326,13 +184384,13 @@ init_marked_esm();
184326
184384
 
184327
184385
  // src/utils/pdf-fonts.ts
184328
184386
  import { existsSync as existsSync3 } from "fs";
184329
- import { join as join4, dirname as dirname2 } from "path";
184330
- import { fileURLToPath as fileURLToPath3 } from "url";
184387
+ import { join as join4, dirname as dirname3 } from "path";
184388
+ import { fileURLToPath as fileURLToPath4 } from "url";
184331
184389
  var BUNDLED_FONT_FAMILY = "BodyFont";
184332
184390
  var BUNDLED_FONT_FAMILY_BOLD = "BodyFont-Bold";
184333
184391
  var DEFAULT_FAMILY = "BodyFont";
184334
184392
  function bundledFontPaths() {
184335
- const here = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath3(import.meta.url));
184393
+ const here = typeof __dirname !== "undefined" ? __dirname : dirname3(fileURLToPath4(import.meta.url));
184336
184394
  const rootFromSrc = join4(here, "..", "..");
184337
184395
  const candidates = [join4(here, "fonts"), join4(rootFromSrc, "dist", "fonts")];
184338
184396
  const fontsDir = candidates.find(
@@ -185503,9 +185561,9 @@ var registerExpertTools = (ctx2) => {
185503
185561
 
185504
185562
  // src/tools/review-criteria.ts
185505
185563
  import { readFileSync as readFileSync5 } from "fs";
185506
- import { fileURLToPath as fileURLToPath4 } from "url";
185564
+ import { fileURLToPath as fileURLToPath5 } from "url";
185507
185565
  import path2 from "path";
185508
- var __dirname3 = path2.dirname(fileURLToPath4(import.meta.url));
185566
+ var __dirname3 = path2.dirname(fileURLToPath5(import.meta.url));
185509
185567
  var CRITERIA_PATH = path2.resolve(__dirname3, "../data/review-criteria.md");
185510
185568
  var registerReviewCriteriaTools = (ctx2) => {
185511
185569
  ctx2.server.tool(
@@ -185552,7 +185610,7 @@ var xrayClientSecret = process.env.XRAY_CLIENT_SECRET || "";
185552
185610
  var xray = xrayClientId && xrayClientSecret ? new XrayClient(xrayClientId, xrayClientSecret, jiraUrl, jiraEmail, jiraToken) : null;
185553
185611
  var server = new McpServer({
185554
185612
  name: "jira-confluence",
185555
- version: "0.1.35"
185613
+ version: "0.1.37"
185556
185614
  });
185557
185615
  var ctx = { server, jira, confluence, risk, xray };
185558
185616
  registerJiraCrudTools(ctx);