@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.
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +12 -15
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +14 -1
- package/dist/agent/memory.d.ts +38 -10
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +223 -67
- package/dist/agent/skills.d.ts +34 -4
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +111 -38
- package/dist/agent/trust.d.ts +7 -0
- package/dist/agent/trust.d.ts.map +1 -0
- package/dist/agent/trust.js +163 -0
- package/dist/api/client.d.ts +30 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +52 -0
- package/dist/checkpoint/manager.d.ts.map +1 -1
- package/dist/checkpoint/manager.js +6 -0
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +2 -13
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +1 -0
- package/dist/permissions/rules.js +1 -1
- package/dist/plugins/index.d.ts +31 -4
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +72 -5
- package/dist/plugins/installer.d.ts +47 -0
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +173 -0
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +196 -5
- package/dist/util/frontmatter.d.ts +79 -0
- package/dist/util/frontmatter.d.ts.map +1 -0
- package/dist/util/frontmatter.js +172 -0
- package/package.json +5 -2
package/dist/tools/executor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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', '
|
|
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.
|
|
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",
|