@nexrall/code-core 1.4.46 → 1.4.48

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 (38) hide show
  1. package/dist/agent/agentTypes.d.ts.map +1 -1
  2. package/dist/agent/agentTypes.js +12 -15
  3. package/dist/agent/loop.d.ts.map +1 -1
  4. package/dist/agent/loop.js +14 -1
  5. package/dist/agent/memory.d.ts +38 -10
  6. package/dist/agent/memory.d.ts.map +1 -1
  7. package/dist/agent/memory.js +223 -67
  8. package/dist/agent/skills.d.ts +34 -4
  9. package/dist/agent/skills.d.ts.map +1 -1
  10. package/dist/agent/skills.js +111 -38
  11. package/dist/agent/trust.d.ts +7 -0
  12. package/dist/agent/trust.d.ts.map +1 -0
  13. package/dist/agent/trust.js +163 -0
  14. package/dist/api/client.d.ts +30 -0
  15. package/dist/api/client.d.ts.map +1 -1
  16. package/dist/api/client.js +52 -0
  17. package/dist/checkpoint/manager.d.ts.map +1 -1
  18. package/dist/checkpoint/manager.js +6 -0
  19. package/dist/commands/loader.d.ts.map +1 -1
  20. package/dist/commands/loader.js +2 -13
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/permissions/modePolicy.d.ts.map +1 -1
  25. package/dist/permissions/modePolicy.js +1 -0
  26. package/dist/permissions/rules.js +1 -1
  27. package/dist/plugins/index.d.ts +31 -4
  28. package/dist/plugins/index.d.ts.map +1 -1
  29. package/dist/plugins/index.js +72 -5
  30. package/dist/plugins/installer.d.ts +47 -0
  31. package/dist/plugins/installer.d.ts.map +1 -1
  32. package/dist/plugins/installer.js +173 -0
  33. package/dist/tools/executor.d.ts.map +1 -1
  34. package/dist/tools/executor.js +196 -5
  35. package/dist/util/frontmatter.d.ts +79 -0
  36. package/dist/util/frontmatter.d.ts.map +1 -0
  37. package/dist/util/frontmatter.js +172 -0
  38. package/package.json +5 -2
@@ -442,6 +442,187 @@ async function writeFile(input, workDir) {
442
442
  return { error: err.message };
443
443
  }
444
444
  }
445
+ // ─── Office document tools (write_docx / write_xlsx / write_pptx) ─────────────
446
+ //
447
+ // Mirrors how Claude Cowork actually does this (per Anthropic's own docs):
448
+ // generate the real OOXML binary via code, not a chat "artifact" preview.
449
+ // Word/Excel/PowerPoint files ARE zip archives of XML under the hood — these
450
+ // tools just drive the `docx`/`exceljs`/`pptxgenjs` libraries that build that
451
+ // XML correctly, so a real .docx/.xlsx/.pptx lands on disk that Word/Excel/
452
+ // PowerPoint (or LibreOffice) opens normally. The model is given a simplified
453
+ // JSON content shape (paragraphs / sheets+rows / slides) rather than being
454
+ // asked to write library calls itself — smaller, more reliable tool calls,
455
+ // and it keeps these tools usable by models with no library-specific training.
456
+ //
457
+ // atomicWrite() (temp+rename) is NOT reused here — these libraries write
458
+ // binary buffers via their own `.save()`/`.writeFile()` APIs rather than a
459
+ // string this module already has in memory, so each tool does its own
460
+ // write-to-temp-then-rename to get the same crash-safety property.
461
+ function atomicWriteBuffer(resolved, data) {
462
+ const dir = path.dirname(resolved);
463
+ fs.mkdirSync(dir, { recursive: true });
464
+ const tmp = path.join(dir, `.${path.basename(resolved)}.nexrall-tmp-${process.pid}-${Date.now()}`);
465
+ fs.writeFileSync(tmp, data);
466
+ fs.renameSync(tmp, resolved);
467
+ }
468
+ async function writeDocx(input, workDir) {
469
+ const filePath = typeof input.path === 'string' ? input.path : '';
470
+ if (!filePath)
471
+ return { error: 'Missing required parameter: path' };
472
+ const title = typeof input.title === 'string' ? input.title : undefined;
473
+ const paragraphsRaw = input.paragraphs;
474
+ if (!Array.isArray(paragraphsRaw) || paragraphsRaw.length === 0) {
475
+ return { error: 'Missing or empty required parameter: paragraphs (array of { text, heading?, bold?, italic?, bullet?, align? })' };
476
+ }
477
+ try {
478
+ // Loaded lazily so the (fairly large) docx/exceljs/pptxgenjs bundles only
479
+ // ever get pulled into memory for a session that actually uses them —
480
+ // most agent sessions never touch Office files at all.
481
+ const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, } = await Promise.resolve().then(() => __importStar(require('docx')));
482
+ const headingMap = {
483
+ 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2,
484
+ 3: HeadingLevel.HEADING_3, 4: HeadingLevel.HEADING_4,
485
+ };
486
+ const alignMap = {
487
+ left: AlignmentType.LEFT, center: AlignmentType.CENTER,
488
+ right: AlignmentType.RIGHT, justify: AlignmentType.JUSTIFIED,
489
+ };
490
+ const children = [];
491
+ if (title) {
492
+ children.push(new Paragraph({ text: title, heading: HeadingLevel.TITLE }));
493
+ }
494
+ for (const raw of paragraphsRaw) {
495
+ if (!raw || typeof raw.text !== 'string')
496
+ continue;
497
+ children.push(new Paragraph({
498
+ heading: raw.heading ? headingMap[raw.heading] : undefined,
499
+ bullet: raw.bullet ? { level: 0 } : undefined,
500
+ alignment: raw.align ? alignMap[raw.align] : undefined,
501
+ children: [new TextRun({ text: raw.text, bold: raw.bold, italics: raw.italic })],
502
+ }));
503
+ }
504
+ const doc = new Document({ sections: [{ children }] });
505
+ const buffer = await Packer.toBuffer(doc);
506
+ const resolved = resolvePath(filePath, workDir);
507
+ const isNew = !fs.existsSync(resolved);
508
+ atomicWriteBuffer(resolved, buffer);
509
+ return { output: `${isNew ? 'Created' : 'Overwrote'} ${resolved} (${paragraphsRaw.length} paragraph(s), ${buffer.length} bytes)` };
510
+ }
511
+ catch (err) {
512
+ return { error: `Failed to write .docx: ${err.message}` };
513
+ }
514
+ }
515
+ async function writeXlsx(input, workDir) {
516
+ const filePath = typeof input.path === 'string' ? input.path : '';
517
+ if (!filePath)
518
+ return { error: 'Missing required parameter: path' };
519
+ const sheetsRaw = input.sheets;
520
+ if (!Array.isArray(sheetsRaw) || sheetsRaw.length === 0) {
521
+ return { error: 'Missing or empty required parameter: sheets (array of { name?, columns?, rows })' };
522
+ }
523
+ // mode: 'edit' loads the EXISTING workbook first (so other sheets, styles,
524
+ // and formulas already in the file survive) instead of always starting
525
+ // from a blank workbook — this is what makes "sửa file Excel có sẵn" work
526
+ // rather than only ever creating brand-new files.
527
+ const mode = input.mode === 'edit' ? 'edit' : 'create';
528
+ try {
529
+ const ExcelJS = await Promise.resolve().then(() => __importStar(require('exceljs')));
530
+ const resolved = resolvePath(filePath, workDir);
531
+ const isNew = !fs.existsSync(resolved);
532
+ const workbook = new ExcelJS.Workbook();
533
+ if (mode === 'edit' && !isNew) {
534
+ try {
535
+ await workbook.xlsx.readFile(resolved);
536
+ }
537
+ catch (err) {
538
+ return { error: `mode: 'edit' but could not read existing workbook at ${resolved}: ${err.message}` };
539
+ }
540
+ }
541
+ else if (mode === 'edit' && isNew) {
542
+ return { error: `mode: 'edit' but ${resolved} does not exist yet — use mode: 'create' (or omit mode) for a new file.` };
543
+ }
544
+ let sheetsTouched = 0;
545
+ for (const raw of sheetsRaw) {
546
+ const name = raw?.name || `Sheet${sheetsTouched + 1}`;
547
+ let sheet = workbook.getWorksheet(name);
548
+ if (sheet && mode === 'edit') {
549
+ // Editing an existing sheet in place: remove and recreate it so
550
+ // re-running the same tool call REPLACES its data instead of
551
+ // appending duplicate rows below whatever was already there —
552
+ // matches write_file's "full replace" semantics for a sheet's data,
553
+ // while sibling sheets and workbook-level state readFile() already
554
+ // loaded are untouched. (sheet.spliceRows(1, rowCount) looks like the
555
+ // obvious way to clear a sheet in place, but empirically does NOT
556
+ // remove existing rows in exceljs 4.4.0 — verified with a minimal
557
+ // repro before choosing this remove+recreate approach instead.)
558
+ //
559
+ // Trade-off: the recreated sheet is appended at the end of the
560
+ // workbook's tab order rather than kept in its original position —
561
+ // acceptable since sheet DATA correctness matters far more than tab
562
+ // order for an agent-generated edit, and exceljs has no public API
563
+ // to reinsert a worksheet at an arbitrary index.
564
+ workbook.removeWorksheet(sheet.id);
565
+ sheet = workbook.addWorksheet(name);
566
+ }
567
+ else if (!sheet) {
568
+ sheet = workbook.addWorksheet(name);
569
+ }
570
+ if (Array.isArray(raw?.columns) && raw.columns.length) {
571
+ sheet.columns = raw.columns.map((c) => ({ header: c.header, key: c.key, width: c.width }));
572
+ }
573
+ if (Array.isArray(raw?.rows)) {
574
+ for (const row of raw.rows)
575
+ sheet.addRow(row);
576
+ }
577
+ sheetsTouched++;
578
+ }
579
+ const buffer = await workbook.xlsx.writeBuffer();
580
+ atomicWriteBuffer(resolved, Buffer.from(buffer));
581
+ return {
582
+ output: `${isNew ? 'Created' : mode === 'edit' ? 'Updated' : 'Overwrote'} ${resolved} (${sheetsTouched} sheet(s), ${buffer.byteLength} bytes)`,
583
+ };
584
+ }
585
+ catch (err) {
586
+ return { error: `Failed to write .xlsx: ${err.message}` };
587
+ }
588
+ }
589
+ async function writePptx(input, workDir) {
590
+ const filePath = typeof input.path === 'string' ? input.path : '';
591
+ if (!filePath)
592
+ return { error: 'Missing required parameter: path' };
593
+ const slidesRaw = input.slides;
594
+ if (!Array.isArray(slidesRaw) || slidesRaw.length === 0) {
595
+ return { error: 'Missing or empty required parameter: slides (array of { title?, bullets?, notes? })' };
596
+ }
597
+ try {
598
+ const PptxGenJSModule = await Promise.resolve().then(() => __importStar(require('pptxgenjs')));
599
+ const PptxGenJS = PptxGenJSModule.default;
600
+ const pres = new PptxGenJS();
601
+ for (const raw of slidesRaw) {
602
+ const slide = pres.addSlide();
603
+ if (raw?.title) {
604
+ slide.addText(raw.title, { x: 0.5, y: 0.3, w: '90%', h: 1, fontSize: 28, bold: true });
605
+ }
606
+ if (Array.isArray(raw?.bullets) && raw.bullets.length) {
607
+ slide.addText(raw.bullets.map((b) => ({ text: b, options: { bullet: true, breakLine: true } })), { x: 0.5, y: 1.5, w: '90%', h: '70%', fontSize: 18 });
608
+ }
609
+ if (raw?.notes)
610
+ slide.addNotes(raw.notes);
611
+ }
612
+ const resolved = resolvePath(filePath, workDir);
613
+ const isNew = !fs.existsSync(resolved);
614
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
615
+ // pptxgenjs writes to disk itself (no in-memory buffer API for Node the
616
+ // way docx/exceljs offer) — 'nodebuffer' gets us the buffer instead so we
617
+ // can still go through the same atomic temp+rename write as the other two.
618
+ const buffer = (await pres.write({ outputType: 'nodebuffer' }));
619
+ atomicWriteBuffer(resolved, buffer);
620
+ return { output: `${isNew ? 'Created' : 'Overwrote'} ${resolved} (${slidesRaw.length} slide(s), ${buffer.length} bytes)` };
621
+ }
622
+ catch (err) {
623
+ return { error: `Failed to write .pptx: ${err.message}` };
624
+ }
625
+ }
445
626
  async function listDirectory(input, workDir) {
446
627
  const dirPath = typeof input.path === 'string' ? input.path : '.';
447
628
  try {
@@ -2360,20 +2541,26 @@ async function memoryWrite(input, workDir) {
2360
2541
  if (!content)
2361
2542
  return { error: 'content is required' };
2362
2543
  const scope = memoryScopeOf(input);
2363
- const r = await (0, memory_1.writeMemory)(content, scope, workDir);
2544
+ const supersedes = typeof input.supersedes === 'string' ? input.supersedes.trim() : undefined;
2545
+ const source = typeof input.source === 'string' ? input.source.trim() : undefined;
2546
+ const r = await (0, memory_1.writeMemory)(content, scope, workDir, { supersedes, source });
2364
2547
  if (!r.ok)
2365
2548
  return { error: 'failed to save memory' };
2366
- return { output: r.already ? `Memory already recorded (skipped duplicate): ${content}` : `Memory saved (${scope}): ${content}` };
2549
+ if (r.already)
2550
+ return { output: `Memory already recorded (skipped duplicate): ${content}` };
2551
+ const supersedeNote = r.superseded ? ' (older fact moved to Superseded)' : '';
2552
+ return { output: `Memory saved (${scope}): ${content}${supersedeNote}` };
2367
2553
  }
2368
2554
  async function memoryRead(input, workDir) {
2369
2555
  try {
2556
+ const includeArchived = input?.include_archived === true;
2370
2557
  // No explicit scope requested → merge both (what the model actually needs
2371
2558
  // when re-orienting mid-conversation); an explicit scope narrows to just that file.
2372
2559
  if (input && (input.scope === 'project' || input.scope === 'global')) {
2373
- const mem = (0, memory_1.readMemory)(input.scope, workDir);
2560
+ const mem = (0, memory_1.readMemory)(input.scope, workDir, { includeArchived });
2374
2561
  return { output: mem || 'No memories saved yet.' };
2375
2562
  }
2376
- const mem = (0, memory_1.readAllMemory)(workDir);
2563
+ const mem = (0, memory_1.readAllMemory)(workDir, { includeArchived });
2377
2564
  return { output: mem || 'No memories saved yet.' };
2378
2565
  }
2379
2566
  catch (err) {
@@ -2445,6 +2632,9 @@ async function getHover(input, workDir) {
2445
2632
  const TOOL_MAP = {
2446
2633
  read_file: readFile,
2447
2634
  write_file: writeFile,
2635
+ write_docx: writeDocx,
2636
+ write_xlsx: writeXlsx,
2637
+ write_pptx: writePptx,
2448
2638
  list_directory: listDirectory,
2449
2639
  bash: bash,
2450
2640
  search_files: searchFiles,
@@ -2484,7 +2674,8 @@ const TOOL_MAP = {
2484
2674
  // their own dedicated branch above (which passes abortSignal too) — kept out
2485
2675
  // of this set so there's exactly one dispatch path per tool, not two.
2486
2676
  const WORKDIR_TOOLS = new Set([
2487
- 'read_file', 'write_file', 'list_directory', 'search_files', 'glob',
2677
+ 'read_file', 'write_file', 'write_docx', 'write_xlsx', 'write_pptx',
2678
+ 'list_directory', 'search_files', 'glob',
2488
2679
  'create_directory', 'edit_file', 'multi_edit', 'copy_file', 'move_file',
2489
2680
  'delete_file', 'notebook_read', 'notebook_edit',
2490
2681
  'get_symbols', 'get_workspace_symbols',
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared YAML-ish frontmatter parser for the markdown-based config formats
3
+ * this project uses (slash commands, skills, agent definitions, plugin
4
+ * manifests). Extracted here so a fix only has to happen once and cannot
5
+ * silently drift between the formats that consume it.
6
+ *
7
+ * ---
8
+ * key: value
9
+ * another-key: "quoted value"
10
+ * description: >-
11
+ * a long value that folds onto
12
+ * the following indented lines
13
+ * metadata:
14
+ * author: example-org
15
+ * version: "1.0"
16
+ * ---
17
+ * body text...
18
+ *
19
+ * Deliberately still not a full YAML parser (no lists, no multi-level
20
+ * nesting, no anchors) — but it now handles the three shapes real-world
21
+ * SKILL.md files (including the `agentskills.io` spec's own examples) use:
22
+ *
23
+ * 1. Plain multi-line values: a `key: value` line followed by indented
24
+ * lines with no `sub: value` shape of their own are folded onto the
25
+ * value with a single space, the same way a human reads wrapped prose.
26
+ * 2. Block scalars: `key: >`, `>-`, `|`, `|-` followed by indented lines.
27
+ * `>` folds lines into a single space-joined string (YAML "folded"
28
+ * style); `|` keeps them literal, newline-joined ("literal" style).
29
+ * The `-` chomping indicator is accepted for compatibility but every
30
+ * value returned here is trimmed regardless, since these are metadata
31
+ * strings, not file content whose trailing newline matters.
32
+ * 3. One-level nested maps: `key:` with NOTHING after the colon, followed
33
+ * by indented `sub: value` lines, becomes a nested map — returned
34
+ * separately in `nested[key]`, NEVER merged into the flat `meta` object.
35
+ *
36
+ * That last rule is load-bearing, not cosmetic. The previous version of
37
+ * this parser treated every trimmed line as a flat top-level key regardless
38
+ * of indentation, so a block like
39
+ *
40
+ * tools: read_file, search_files, glob
41
+ * metadata:
42
+ * tools: read_file, search_files, glob, bash, write_file, delete_file
43
+ *
44
+ * silently let the indented `tools:` inside `metadata:` OVERWRITE the real,
45
+ * innocent-looking top-level `tools:` a reviewer would actually read —
46
+ * because both lines matched the same regex and the second write won. That
47
+ * is a privilege-escalation bug in an agent-definition file, not a display
48
+ * quirk: `agentTypes.ts`'s tool allowlist reads `meta.tools` and would have
49
+ * granted `bash`/`write_file`/`delete_file` from a definition that visibly
50
+ * only requests three read-only tools. Every indented line now belongs to
51
+ * ITS OWN key's continuation/nested-map, never to the flat top-level
52
+ * namespace, which makes this class of collision structurally impossible
53
+ * rather than merely untested.
54
+ */
55
+ export interface ParsedFrontmatter {
56
+ /** Flat top-level `key: value` pairs. Indented/nested content never leaks in here. */
57
+ meta: Record<string, string>;
58
+ /** Markdown body after the closing `---`. */
59
+ body: string;
60
+ /**
61
+ * One-level nested maps, keyed by their parent field name — e.g. a
62
+ * `metadata:` block with indented `author: ...` / `version: ...` lines
63
+ * becomes `nested.metadata = { author: '...', version: '...' }`.
64
+ * Absent (not an empty object) when no field had a nested map, so
65
+ * callers that don't care can ignore this without an extra check.
66
+ */
67
+ nested?: Record<string, Record<string, string>>;
68
+ /**
69
+ * True if a valid `---`-delimited frontmatter block was found at all. False
70
+ * means the whole file is `body` and `meta`/`nested` are empty — callers
71
+ * that treat "no parseable frontmatter" as a distinct, fail-closed case
72
+ * (e.g. agent definitions default to read-only rather than unrestricted
73
+ * when this is false) should check this instead of `Object.keys(meta).length`,
74
+ * which can't tell "no frontmatter" apart from "frontmatter with no fields".
75
+ */
76
+ ok: boolean;
77
+ }
78
+ export declare function parseFrontmatter(raw: string): ParsedFrontmatter;
79
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,EAAE,EAAE,OAAO,CAAC;CACb;AAcD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAqG/D"}
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /**
3
+ * Shared YAML-ish frontmatter parser for the markdown-based config formats
4
+ * this project uses (slash commands, skills, agent definitions, plugin
5
+ * manifests). Extracted here so a fix only has to happen once and cannot
6
+ * silently drift between the formats that consume it.
7
+ *
8
+ * ---
9
+ * key: value
10
+ * another-key: "quoted value"
11
+ * description: >-
12
+ * a long value that folds onto
13
+ * the following indented lines
14
+ * metadata:
15
+ * author: example-org
16
+ * version: "1.0"
17
+ * ---
18
+ * body text...
19
+ *
20
+ * Deliberately still not a full YAML parser (no lists, no multi-level
21
+ * nesting, no anchors) — but it now handles the three shapes real-world
22
+ * SKILL.md files (including the `agentskills.io` spec's own examples) use:
23
+ *
24
+ * 1. Plain multi-line values: a `key: value` line followed by indented
25
+ * lines with no `sub: value` shape of their own are folded onto the
26
+ * value with a single space, the same way a human reads wrapped prose.
27
+ * 2. Block scalars: `key: >`, `>-`, `|`, `|-` followed by indented lines.
28
+ * `>` folds lines into a single space-joined string (YAML "folded"
29
+ * style); `|` keeps them literal, newline-joined ("literal" style).
30
+ * The `-` chomping indicator is accepted for compatibility but every
31
+ * value returned here is trimmed regardless, since these are metadata
32
+ * strings, not file content whose trailing newline matters.
33
+ * 3. One-level nested maps: `key:` with NOTHING after the colon, followed
34
+ * by indented `sub: value` lines, becomes a nested map — returned
35
+ * separately in `nested[key]`, NEVER merged into the flat `meta` object.
36
+ *
37
+ * That last rule is load-bearing, not cosmetic. The previous version of
38
+ * this parser treated every trimmed line as a flat top-level key regardless
39
+ * of indentation, so a block like
40
+ *
41
+ * tools: read_file, search_files, glob
42
+ * metadata:
43
+ * tools: read_file, search_files, glob, bash, write_file, delete_file
44
+ *
45
+ * silently let the indented `tools:` inside `metadata:` OVERWRITE the real,
46
+ * innocent-looking top-level `tools:` a reviewer would actually read —
47
+ * because both lines matched the same regex and the second write won. That
48
+ * is a privilege-escalation bug in an agent-definition file, not a display
49
+ * quirk: `agentTypes.ts`'s tool allowlist reads `meta.tools` and would have
50
+ * granted `bash`/`write_file`/`delete_file` from a definition that visibly
51
+ * only requests three read-only tools. Every indented line now belongs to
52
+ * ITS OWN key's continuation/nested-map, never to the flat top-level
53
+ * namespace, which makes this class of collision structurally impossible
54
+ * rather than merely untested.
55
+ */
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.parseFrontmatter = parseFrontmatter;
58
+ const TOP_LEVEL_KEY = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/;
59
+ const BLOCK_SCALAR = /^[|>][+-]?$/;
60
+ function leadingIndent(line) {
61
+ const m = /^[ \t]*/.exec(line);
62
+ return m ? m[0].length : 0;
63
+ }
64
+ function unquote(v) {
65
+ return v.trim().replace(/^["']|["']$/g, '');
66
+ }
67
+ function parseFrontmatter(raw) {
68
+ const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
69
+ if (!m)
70
+ return { meta: {}, body: raw.trim(), ok: false };
71
+ const lines = m[1].split(/\r?\n/);
72
+ const meta = {};
73
+ let nested;
74
+ let i = 0;
75
+ while (i < lines.length) {
76
+ const line = lines[i];
77
+ // Blank lines and stray indented lines with no top-level key before them
78
+ // (malformed input) are simply skipped — lenient parsing, same spirit as
79
+ // the rest of this file: warn elsewhere, don't throw here.
80
+ if (!line.trim() || leadingIndent(line) > 0) {
81
+ i++;
82
+ continue;
83
+ }
84
+ const kv = TOP_LEVEL_KEY.exec(line.trim());
85
+ if (!kv) {
86
+ i++;
87
+ continue;
88
+ }
89
+ const key = kv[1].toLowerCase();
90
+ const value = kv[2].trim();
91
+ // Collect every immediately-following indented line as this key's
92
+ // continuation block, whatever shape it turns out to be.
93
+ const continuation = [];
94
+ let j = i + 1;
95
+ while (j < lines.length && (leadingIndent(lines[j]) > 0 || !lines[j].trim())) {
96
+ continuation.push(lines[j]);
97
+ j++;
98
+ }
99
+ // Trim trailing blank continuation lines (keeps indent-detection below
100
+ // simple and doesn't affect folded/literal output).
101
+ while (continuation.length && !continuation[continuation.length - 1].trim())
102
+ continuation.pop();
103
+ if (BLOCK_SCALAR.test(value)) {
104
+ // `key: >`, `>-`, `|`, `|-` — content is EVERY continuation line.
105
+ // `>` = YAML "folded" style (join into one space-separated line);
106
+ // `|` = YAML "literal" style (keep the line breaks as-is).
107
+ const folded = value.startsWith('>');
108
+ const base = continuation.length ? leadingIndent(continuation.find((l) => l.trim()) ?? continuation[0]) : 0;
109
+ const dedented = continuation.map((l) => (l.trim() ? l.slice(Math.min(base, leadingIndent(l))) : ''));
110
+ meta[key] = folded
111
+ ? dedented.join(' ').replace(/\s+/g, ' ').trim() // folded (>) style
112
+ : dedented.join('\n').trim(); // literal (|) style
113
+ }
114
+ else if (value === '') {
115
+ // `key:` with nothing after it. Two possibilities:
116
+ // - continuation lines look like `sub: value` → this is a nested map,
117
+ // isolated in `nested[key]` and NEVER merged into flat `meta`.
118
+ // - continuation lines are plain prose → fold them as the value.
119
+ const looksNested = continuation.length > 0 && continuation.every((l) => !l.trim() || TOP_LEVEL_KEY.test(l.trim()));
120
+ if (looksNested && continuation.some((l) => l.trim())) {
121
+ const map = {};
122
+ for (const l of continuation) {
123
+ const sub = TOP_LEVEL_KEY.exec(l.trim());
124
+ if (sub)
125
+ map[sub[1].toLowerCase()] = unquote(sub[2]);
126
+ }
127
+ nested = nested ?? {};
128
+ nested[key] = map;
129
+ // Deliberately no `meta[key] = ...` here — an empty-header field with
130
+ // a nested map has no flat scalar value of its own.
131
+ }
132
+ else if (continuation.length) {
133
+ meta[key] = [value, ...continuation.map((l) => l.trim())].filter(Boolean).join(' ').trim();
134
+ }
135
+ else {
136
+ meta[key] = '';
137
+ }
138
+ }
139
+ else {
140
+ // Normal `key: value`. Fold any plain-prose continuation lines onto it
141
+ // (the common case: a long `description:` wrapped across lines) —
142
+ // but only if none of them look like their own `sub: value` pair,
143
+ // which would instead mean the author meant something structured and
144
+ // folding it into one string would corrupt it silently.
145
+ const looksNested = continuation.length > 0 && continuation.every((l) => !l.trim() || TOP_LEVEL_KEY.test(l.trim()));
146
+ if (continuation.length && !looksNested) {
147
+ meta[key] = unquote([value, ...continuation.map((l) => l.trim())].join(' '));
148
+ }
149
+ else if (continuation.length && looksNested) {
150
+ // `key: value` immediately followed by what looks like a nested map —
151
+ // ambiguous/malformed YAML. Keep the scalar value (least surprising:
152
+ // it's what a naive line-by-line reader sees first) and surface the
153
+ // rest as a nested map too, rather than silently dropping either.
154
+ meta[key] = unquote(value);
155
+ const map = {};
156
+ for (const l of continuation) {
157
+ const sub = TOP_LEVEL_KEY.exec(l.trim());
158
+ if (sub)
159
+ map[sub[1].toLowerCase()] = unquote(sub[2]);
160
+ }
161
+ nested = nested ?? {};
162
+ nested[key] = map;
163
+ }
164
+ else {
165
+ meta[key] = unquote(value);
166
+ }
167
+ }
168
+ i = j;
169
+ }
170
+ return { meta, body: (m[2] ?? '').trim(), nested, ok: true };
171
+ }
172
+ //# sourceMappingURL=frontmatter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.46",
3
+ "version": "1.4.48",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
@@ -95,7 +95,10 @@
95
95
  },
96
96
  "dependencies": {
97
97
  "eventsource-parser": "^1.1.2",
98
- "node-fetch": "^3.3.2"
98
+ "node-fetch": "^3.3.2",
99
+ "docx": "^9.7.1",
100
+ "exceljs": "^4.4.0",
101
+ "pptxgenjs": "^4.0.1"
99
102
  },
100
103
  "devDependencies": {
101
104
  "@types/node": "^26.0.1",