@nexrall/code-core 1.4.46 → 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.
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +6 -2
- 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.map +1 -1
- package/dist/agent/skills.js +3 -14
- package/dist/agent/trust.d.ts +7 -0
- package/dist/agent/trust.d.ts.map +1 -0
- package/dist/agent/trust.js +151 -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 +22 -0
- package/dist/util/frontmatter.d.ts.map +1 -0
- package/dist/util/frontmatter.js +33 -0
- package/package.json +5 -2
|
@@ -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":"
|
|
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;
|
|
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"}
|
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,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.
|
|
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",
|