@nexrall/code-core 1.4.45 → 1.4.47

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.
@@ -86,6 +86,53 @@ export declare function parsePluginSource(spec: string): PluginSource;
86
86
  * containing no MCP servers.
87
87
  */
88
88
  export declare function inspectPluginDir(dir: string): PluginInspection;
89
+ export interface PluginComponentSummary {
90
+ name: string;
91
+ description: string;
92
+ }
93
+ export interface PluginMcpServerSummary {
94
+ name: string;
95
+ /** Transport, best-effort from the raw config shape (mirrors McpServerConfig in mcp/client.ts). */
96
+ type: 'stdio' | 'http' | 'sse';
97
+ /** Short human-readable summary of how this server connects (command, or URL host). */
98
+ summary: string;
99
+ }
100
+ export interface PluginComponentDetail {
101
+ commands: PluginComponentSummary[];
102
+ skills: PluginComponentSummary[];
103
+ agents: PluginComponentSummary[];
104
+ mcpServers: PluginMcpServerSummary[];
105
+ hasHooks: boolean;
106
+ }
107
+ /**
108
+ * Full breakdown of what a plugin bundles — one skill/command/connector per
109
+ * row, each with the human-readable text a user needs to decide whether to
110
+ * use/connect it. Distinct from inspectPluginDir (which only reports COUNTS
111
+ * and booleans for the install-time safety warning): this is the read model
112
+ * for the "click a plugin to see what's inside" detail screen, most directly
113
+ * needed because a Connectors tab has to show each MCP server by NAME so a
114
+ * user can connect them one at a time, the same way Claude Cowork's plugin
115
+ * detail view does.
116
+ */
117
+ export declare function pluginComponentDetail(dir: string): PluginComponentDetail;
118
+ export interface PluginItemDetail extends PluginComponentSummary {
119
+ /** Full raw file contents, including frontmatter — what a "Raw" toggle / Copy button should use. */
120
+ raw: string;
121
+ /** Frontmatter-stripped body — what a "Preview" toggle should render as markdown. */
122
+ body: string;
123
+ /** Absolute path to the source file, for "Edit" (open in the user's default editor/file manager). */
124
+ filePath: string;
125
+ }
126
+ /**
127
+ * Full content of ONE skill/command/agent inside a plugin — the read model
128
+ * for "click a skill row to see its content", mirroring Claude Cowork's own
129
+ * per-skill screen (rendered preview, raw markdown toggle, copy button).
130
+ * Deliberately separate from pluginComponentDetail's summaries (name +
131
+ * description only): loading every item's full body up front for a plugin
132
+ * with many skills would be wasteful, so this is fetched lazily, one row at
133
+ * a time, only when the user actually opens it.
134
+ */
135
+ export declare function pluginItemDetail(dir: string, kind: 'commands' | 'agents' | 'skills', name: string): PluginItemDetail | null;
89
136
  export interface InstallOptions {
90
137
  /** 'global' (default) → ~/.nexrall/plugins; 'project' → <workDir>/.nexrall/plugins */
91
138
  scope?: 'project' | 'global';
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"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"}
@@ -38,6 +38,8 @@ exports.getRegistryPlugin = getRegistryPlugin;
38
38
  exports.reportInstall = reportInstall;
39
39
  exports.parsePluginSource = parsePluginSource;
40
40
  exports.inspectPluginDir = inspectPluginDir;
41
+ exports.pluginComponentDetail = pluginComponentDetail;
42
+ exports.pluginItemDetail = pluginItemDetail;
41
43
  exports.installPlugin = installPlugin;
42
44
  exports.readReceipt = readReceipt;
43
45
  exports.removePlugin = removePlugin;
@@ -49,6 +51,7 @@ const child_process_1 = require("child_process");
49
51
  const client_1 = require("../api/client");
50
52
  const index_1 = require("./index");
51
53
  const sources_1 = require("./sources");
54
+ const frontmatter_1 = require("../util/frontmatter");
52
55
  const RECEIPT_FILE = '_install.json';
53
56
  /** Search the registry (empty query = list all, official first). */
54
57
  async function searchRegistry(query, limit = 50) {
@@ -169,6 +172,176 @@ function inspectPluginDir(dir) {
169
172
  meta,
170
173
  };
171
174
  }
175
+ /** Read a flat `commands/*.md` or `agents/*.md` directory, returning name + frontmatter description. */
176
+ function listMdWithDescription(dir) {
177
+ let files;
178
+ try {
179
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
180
+ }
181
+ catch {
182
+ return [];
183
+ }
184
+ const out = [];
185
+ for (const file of files) {
186
+ const name = file.replace(/\.md$/, '');
187
+ try {
188
+ const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
189
+ const { meta } = (0, frontmatter_1.parseFrontmatter)(raw);
190
+ out.push({ name, description: meta.description || '' });
191
+ }
192
+ catch {
193
+ out.push({ name, description: '' }); // malformed file — still list it, just without a description
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+ /** Read `skills/<name>/SKILL.md` subdirectories, returning name + frontmatter description. */
199
+ function listSkillsWithDescription(dir) {
200
+ let entries;
201
+ try {
202
+ entries = fs.readdirSync(dir, { withFileTypes: true });
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ const out = [];
208
+ for (const entry of entries) {
209
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
210
+ continue;
211
+ const skillFile = path.join(dir, entry.name, 'SKILL.md');
212
+ try {
213
+ if (!fs.statSync(skillFile).isFile())
214
+ continue;
215
+ const raw = fs.readFileSync(skillFile, 'utf-8');
216
+ const { meta } = (0, frontmatter_1.parseFrontmatter)(raw);
217
+ out.push({ name: meta.name || entry.name, description: meta.description || '' });
218
+ }
219
+ catch {
220
+ /* not a valid skill directory — skip */
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ /** Best-effort transport + one-line summary for an mcp.json server entry, for display only (never executed). */
226
+ function summariseMcpServer(name, cfg) {
227
+ const c = (cfg && typeof cfg === 'object' ? cfg : {});
228
+ const url = typeof c.url === 'string' ? c.url : undefined;
229
+ const command = typeof c.command === 'string' ? c.command : undefined;
230
+ const declaredType = c.type === 'http' || c.type === 'sse' || c.type === 'stdio' ? c.type : undefined;
231
+ const type = declaredType ?? (url ? 'http' : 'stdio');
232
+ let summary = 'Unknown connector configuration';
233
+ if (url) {
234
+ try {
235
+ summary = new URL(url).host;
236
+ }
237
+ catch {
238
+ summary = url;
239
+ }
240
+ }
241
+ else if (command) {
242
+ const args = Array.isArray(c.args) ? c.args.filter((a) => typeof a === 'string') : [];
243
+ summary = [command, ...args].join(' ');
244
+ }
245
+ return { name, type, summary };
246
+ }
247
+ /**
248
+ * Full breakdown of what a plugin bundles — one skill/command/connector per
249
+ * row, each with the human-readable text a user needs to decide whether to
250
+ * use/connect it. Distinct from inspectPluginDir (which only reports COUNTS
251
+ * and booleans for the install-time safety warning): this is the read model
252
+ * for the "click a plugin to see what's inside" detail screen, most directly
253
+ * needed because a Connectors tab has to show each MCP server by NAME so a
254
+ * user can connect them one at a time, the same way Claude Cowork's plugin
255
+ * detail view does.
256
+ */
257
+ function pluginComponentDetail(dir) {
258
+ const mcpFile = (0, index_1.resolvePluginFile)(dir, 'mcp');
259
+ let mcpServers = [];
260
+ if (mcpFile) {
261
+ try {
262
+ const j = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
263
+ const servers = (j.mcpServers ?? j);
264
+ mcpServers = Object.entries(servers).map(([name, cfg]) => summariseMcpServer(name, cfg));
265
+ }
266
+ catch {
267
+ /* malformed mcp.json — report no servers rather than throwing */
268
+ }
269
+ }
270
+ return {
271
+ commands: listMdWithDescription(path.join(dir, 'commands')),
272
+ skills: listSkillsWithDescription(path.join(dir, 'skills')),
273
+ agents: listMdWithDescription(path.join(dir, 'agents')),
274
+ mcpServers,
275
+ hasHooks: (0, index_1.resolvePluginFile)(dir, 'hooks') !== null,
276
+ };
277
+ }
278
+ /** Match a flat `commands/*.md` or `agents/*.md` file by its display name (never builds a path from `name` — only compares against real directory entries, so a caller cannot path-traverse via the name argument). */
279
+ function readPluginItemFromFlatDir(dir, name) {
280
+ let files;
281
+ try {
282
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ for (const file of files) {
288
+ if (file.replace(/\.md$/, '') !== name)
289
+ continue;
290
+ const filePath = path.join(dir, file);
291
+ try {
292
+ const raw = fs.readFileSync(filePath, 'utf-8');
293
+ const { meta, body } = (0, frontmatter_1.parseFrontmatter)(raw);
294
+ return { name, description: meta.description || '', raw, body, filePath };
295
+ }
296
+ catch {
297
+ return null;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+ /** Match a `skills/<dir>/SKILL.md` by its effective name (meta.name override, or the directory name). */
303
+ function readPluginSkill(skillsDir, name) {
304
+ let entries;
305
+ try {
306
+ entries = fs.readdirSync(skillsDir, { withFileTypes: true });
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ for (const entry of entries) {
312
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
313
+ continue;
314
+ const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
315
+ try {
316
+ if (!fs.statSync(skillFile).isFile())
317
+ continue;
318
+ const raw = fs.readFileSync(skillFile, 'utf-8');
319
+ const { meta, body } = (0, frontmatter_1.parseFrontmatter)(raw);
320
+ const skillName = meta.name || entry.name;
321
+ if (skillName !== name)
322
+ continue;
323
+ return { name: skillName, description: meta.description || '', raw, body, filePath: skillFile };
324
+ }
325
+ catch {
326
+ /* unreadable/malformed — keep looking, don't fail the whole scan */
327
+ }
328
+ }
329
+ return null;
330
+ }
331
+ /**
332
+ * Full content of ONE skill/command/agent inside a plugin — the read model
333
+ * for "click a skill row to see its content", mirroring Claude Cowork's own
334
+ * per-skill screen (rendered preview, raw markdown toggle, copy button).
335
+ * Deliberately separate from pluginComponentDetail's summaries (name +
336
+ * description only): loading every item's full body up front for a plugin
337
+ * with many skills would be wasteful, so this is fetched lazily, one row at
338
+ * a time, only when the user actually opens it.
339
+ */
340
+ function pluginItemDetail(dir, kind, name) {
341
+ if (kind === 'skills')
342
+ return readPluginSkill(path.join(dir, 'skills'), name);
343
+ return readPluginItemFromFlatDir(path.join(dir, kind), name);
344
+ }
172
345
  /** Is this directory a plausible plugin root? */
173
346
  function looksLikePlugin(dir) {
174
347
  const i = inspectPluginDir(dir);
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAg3EtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CA6BrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA8kFtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CA6BrB"}
@@ -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,22 @@
1
+ /**
2
+ * Shared YAML-ish frontmatter parser for the markdown-based config formats
3
+ * this project uses (slash commands, skills, agent definitions). All three
4
+ * previously carried their own byte-for-byte identical copy of this function
5
+ * — extracted here so a future fix (e.g. multi-line values, list syntax)
6
+ * only has to happen once and cannot silently drift between them.
7
+ *
8
+ * ---
9
+ * key: value
10
+ * another-key: "quoted value"
11
+ * ---
12
+ * body text...
13
+ *
14
+ * Deliberately minimal: single-line `key: value` pairs only, no nested
15
+ * structures or YAML lists — matches what commands/loader.ts, agent/skills.ts
16
+ * and agent/agentTypes.ts have always accepted.
17
+ */
18
+ export declare function parseFrontmatter(raw: string): {
19
+ meta: Record<string, string>;
20
+ body: string;
21
+ };
22
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAS5F"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseFrontmatter = parseFrontmatter;
4
+ /**
5
+ * Shared YAML-ish frontmatter parser for the markdown-based config formats
6
+ * this project uses (slash commands, skills, agent definitions). All three
7
+ * previously carried their own byte-for-byte identical copy of this function
8
+ * — extracted here so a future fix (e.g. multi-line values, list syntax)
9
+ * only has to happen once and cannot silently drift between them.
10
+ *
11
+ * ---
12
+ * key: value
13
+ * another-key: "quoted value"
14
+ * ---
15
+ * body text...
16
+ *
17
+ * Deliberately minimal: single-line `key: value` pairs only, no nested
18
+ * structures or YAML lists — matches what commands/loader.ts, agent/skills.ts
19
+ * and agent/agentTypes.ts have always accepted.
20
+ */
21
+ function parseFrontmatter(raw) {
22
+ const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
23
+ if (!m)
24
+ return { meta: {}, body: raw.trim() };
25
+ const meta = {};
26
+ for (const line of m[1].split(/\r?\n/)) {
27
+ const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
28
+ if (kv)
29
+ meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
30
+ }
31
+ return { meta, body: (m[2] ?? '').trim() };
32
+ }
33
+ //# sourceMappingURL=frontmatter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.45",
3
+ "version": "1.4.47",
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",