@alavida/agentpack 0.1.2 → 0.1.4
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/README.md +35 -1
- package/bin/intent.js +20 -0
- package/package.json +15 -5
- package/skills/agentpack-cli/SKILL.md +9 -1
- package/skills/authoring-skillgraphs-from-knowledge/SKILL.md +148 -0
- package/skills/authoring-skillgraphs-from-knowledge/references/authored-metadata.md +6 -0
- package/skills/developing-and-testing-skills/SKILL.md +127 -0
- package/skills/developing-and-testing-skills/references/local-workbench.md +7 -0
- package/skills/getting-started-skillgraphs/SKILL.md +115 -0
- package/skills/getting-started-skillgraphs/references/command-routing.md +7 -0
- package/skills/identifying-skill-opportunities/SKILL.md +119 -0
- package/skills/identifying-skill-opportunities/references/capability-boundaries.md +6 -0
- package/skills/maintaining-skillgraph-freshness/SKILL.md +110 -0
- package/skills/repairing-broken-skill-or-plugin-state/SKILL.md +116 -0
- package/skills/repairing-broken-skill-or-plugin-state/references/diagnostic-flows.md +6 -0
- package/skills/shipping-production-plugins-and-packages/SKILL.md +123 -0
- package/skills/shipping-production-plugins-and-packages/references/plugin-delivery.md +6 -0
- package/skills/sync-state.json +83 -0
- package/src/application/skills/build-skill-workbench-model.js +194 -0
- package/src/application/skills/run-skill-workbench-action.js +23 -0
- package/src/application/skills/start-skill-dev-workbench.js +192 -0
- package/src/cli.js +1 -1
- package/src/commands/skills.js +34 -10
- package/src/dashboard/App.jsx +343 -0
- package/src/dashboard/components/Breadcrumbs.jsx +45 -0
- package/src/dashboard/components/ControlStrip.jsx +153 -0
- package/src/dashboard/components/InspectorPanel.jsx +203 -0
- package/src/dashboard/components/SkillGraph.jsx +567 -0
- package/src/dashboard/components/Tooltip.jsx +111 -0
- package/src/dashboard/dist/dashboard.js +26692 -0
- package/src/dashboard/index.html +81 -0
- package/src/dashboard/lib/api.js +19 -0
- package/src/dashboard/lib/router.js +15 -0
- package/src/dashboard/main.jsx +4 -0
- package/src/domain/plugins/load-plugin-definition.js +163 -0
- package/src/domain/plugins/plugin-diagnostic-error.js +18 -0
- package/src/domain/plugins/plugin-requirements.js +15 -0
- package/src/domain/skills/skill-graph.js +1 -0
- package/src/infrastructure/fs/dev-session-repository.js +25 -0
- package/src/infrastructure/runtime/materialize-skills.js +18 -1
- package/src/infrastructure/runtime/open-browser.js +20 -0
- package/src/infrastructure/runtime/skill-dev-workbench-server.js +96 -0
- package/src/infrastructure/runtime/watch-skill-workbench.js +68 -0
- package/src/lib/plugins.js +19 -28
- package/src/lib/skills.js +245 -16
- package/src/utils/errors.js +33 -1
package/src/lib/skills.js
CHANGED
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
readNodeStatus,
|
|
10
10
|
} from '../domain/skills/skill-graph.js';
|
|
11
11
|
import { readInstallState } from '../infrastructure/fs/install-state-repository.js';
|
|
12
|
+
import { readDevSession, writeDevSession, removeDevSession } from '../infrastructure/fs/dev-session-repository.js';
|
|
12
13
|
import {
|
|
13
14
|
ensureSkillLink,
|
|
14
15
|
rebuildInstallState,
|
|
15
16
|
removePathIfExists,
|
|
16
17
|
removeSkillLinks,
|
|
18
|
+
removeSkillLinksByPaths,
|
|
17
19
|
removeSkillLinksByNames,
|
|
18
20
|
} from '../infrastructure/runtime/materialize-skills.js';
|
|
19
21
|
import {
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
readBuildState,
|
|
30
32
|
writeBuildState,
|
|
31
33
|
} from '../domain/skills/skill-provenance.js';
|
|
34
|
+
import { startSkillDevWorkbench } from '../application/skills/start-skill-dev-workbench.js';
|
|
32
35
|
import { AgentpackError, EXIT_CODES, NetworkError, NotFoundError, ValidationError } from '../utils/errors.js';
|
|
33
36
|
|
|
34
37
|
const GITHUB_PACKAGES_REGISTRY = 'https://npm.pkg.github.com';
|
|
@@ -170,6 +173,100 @@ function readPackageJson(packageDir) {
|
|
|
170
173
|
};
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
function isProcessAlive(pid) {
|
|
177
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error.code === 'EPERM') return true;
|
|
183
|
+
if (error.code === 'ESRCH') return false;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildDevSessionNextSteps(command) {
|
|
189
|
+
return [{
|
|
190
|
+
action: 'run_command',
|
|
191
|
+
command,
|
|
192
|
+
reason: 'Use the dev session cleanup flow to remove recorded linked skills for this repo',
|
|
193
|
+
}];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toDevSessionRecord(repoRoot, target, result, existing = null) {
|
|
197
|
+
const now = new Date().toISOString();
|
|
198
|
+
const rootSkill = result.linkedSkills.find((entry) => entry.name === result.name) || result.linkedSkills[0] || null;
|
|
199
|
+
return {
|
|
200
|
+
version: 1,
|
|
201
|
+
session_id: existing?.session_id || `dev-${now.replaceAll(':', '-').replaceAll('.', '-')}`,
|
|
202
|
+
status: 'active',
|
|
203
|
+
pid: process.pid,
|
|
204
|
+
repo_root: repoRoot,
|
|
205
|
+
target,
|
|
206
|
+
root_skill: rootSkill
|
|
207
|
+
? {
|
|
208
|
+
name: rootSkill.name,
|
|
209
|
+
package_name: rootSkill.packageName,
|
|
210
|
+
path: rootSkill.path,
|
|
211
|
+
}
|
|
212
|
+
: null,
|
|
213
|
+
linked_skills: result.linkedSkills.map((entry) => ({
|
|
214
|
+
name: entry.name,
|
|
215
|
+
package_name: entry.packageName,
|
|
216
|
+
path: entry.path,
|
|
217
|
+
})),
|
|
218
|
+
links: result.links,
|
|
219
|
+
started_at: existing?.started_at || now,
|
|
220
|
+
updated_at: now,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cleanupRecordedDevSession(repoRoot, session, status = 'stale') {
|
|
225
|
+
if (!session) {
|
|
226
|
+
return {
|
|
227
|
+
cleaned: false,
|
|
228
|
+
removed: [],
|
|
229
|
+
session: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
writeDevSession(repoRoot, {
|
|
234
|
+
...session,
|
|
235
|
+
status,
|
|
236
|
+
updated_at: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
const removed = removeSkillLinksByPaths(repoRoot, session.links || [], normalizeDisplayPath);
|
|
239
|
+
removeDevSession(repoRoot);
|
|
240
|
+
return {
|
|
241
|
+
cleaned: removed.length > 0 || Boolean(session),
|
|
242
|
+
removed,
|
|
243
|
+
session,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function reconcileDevSession(repoRoot) {
|
|
248
|
+
const session = readDevSession(repoRoot);
|
|
249
|
+
if (!session) return null;
|
|
250
|
+
|
|
251
|
+
if (session.status === 'active' && isProcessAlive(session.pid)) {
|
|
252
|
+
throw new AgentpackError('A skills dev session is already active in this repo', {
|
|
253
|
+
code: 'skills_dev_session_active',
|
|
254
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
255
|
+
nextSteps: [
|
|
256
|
+
...buildDevSessionNextSteps('agentpack skills dev cleanup'),
|
|
257
|
+
...buildDevSessionNextSteps('agentpack skills dev cleanup --force'),
|
|
258
|
+
],
|
|
259
|
+
details: {
|
|
260
|
+
rootSkill: session.root_skill?.name || null,
|
|
261
|
+
pid: session.pid,
|
|
262
|
+
startedAt: session.started_at || null,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return cleanupRecordedDevSession(repoRoot, session, 'stale');
|
|
268
|
+
}
|
|
269
|
+
|
|
173
270
|
export function syncSkillDependencies(skillDir) {
|
|
174
271
|
const skillFile = join(skillDir, 'SKILL.md');
|
|
175
272
|
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
@@ -263,25 +360,42 @@ export function devSkill(target, {
|
|
|
263
360
|
export function startSkillDev(target, {
|
|
264
361
|
cwd = process.cwd(),
|
|
265
362
|
sync = true,
|
|
363
|
+
dashboard = true,
|
|
266
364
|
onStart = () => {},
|
|
267
365
|
onRebuild = () => {},
|
|
268
366
|
} = {}) {
|
|
269
|
-
const
|
|
270
|
-
const { skillDir } = resolveLocalPackagedSkillDir(
|
|
367
|
+
const outerRepoRoot = findRepoRoot(cwd);
|
|
368
|
+
const { skillDir } = resolveLocalPackagedSkillDir(outerRepoRoot, target);
|
|
369
|
+
const repoRoot = findRepoRoot(skillDir);
|
|
370
|
+
reconcileDevSession(repoRoot);
|
|
271
371
|
let closed = false;
|
|
272
372
|
let timer = null;
|
|
273
373
|
let currentNames = [];
|
|
274
374
|
let watcher = null;
|
|
375
|
+
let workbench = null;
|
|
376
|
+
let initialResult = null;
|
|
377
|
+
let sessionRecord = null;
|
|
275
378
|
|
|
276
379
|
const cleanup = () => {
|
|
277
380
|
if (closed) return { name: currentNames[0] || null, unlinked: false, removed: [] };
|
|
278
381
|
closed = true;
|
|
279
382
|
clearTimeout(timer);
|
|
280
383
|
if (watcher) watcher.close();
|
|
281
|
-
|
|
384
|
+
if (workbench) workbench.close();
|
|
385
|
+
if (sessionRecord) {
|
|
386
|
+
writeDevSession(repoRoot, {
|
|
387
|
+
...sessionRecord,
|
|
388
|
+
status: 'cleaning',
|
|
389
|
+
updated_at: new Date().toISOString(),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const removed = sessionRecord
|
|
393
|
+
? removeSkillLinksByPaths(repoRoot, sessionRecord.links || [], normalizeDisplayPath)
|
|
394
|
+
: removeSkillLinksByNames(repoRoot, currentNames, normalizeDisplayPath);
|
|
395
|
+
removeDevSession(repoRoot);
|
|
282
396
|
detachProcessCleanup();
|
|
283
397
|
return {
|
|
284
|
-
name: currentNames[0] || null,
|
|
398
|
+
name: sessionRecord?.root_skill?.name || currentNames[0] || null,
|
|
285
399
|
unlinked: removed.length > 0,
|
|
286
400
|
removed,
|
|
287
401
|
};
|
|
@@ -289,9 +403,14 @@ export function startSkillDev(target, {
|
|
|
289
403
|
|
|
290
404
|
const processCleanupHandlers = new Map();
|
|
291
405
|
const attachProcessCleanup = () => {
|
|
292
|
-
|
|
406
|
+
const exitHandler = () => cleanup();
|
|
407
|
+
processCleanupHandlers.set('exit', exitHandler);
|
|
408
|
+
process.once('exit', exitHandler);
|
|
409
|
+
|
|
410
|
+
for (const eventName of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
293
411
|
const handler = () => {
|
|
294
412
|
cleanup();
|
|
413
|
+
process.exit(0);
|
|
295
414
|
};
|
|
296
415
|
processCleanupHandlers.set(eventName, handler);
|
|
297
416
|
process.once(eventName, handler);
|
|
@@ -305,19 +424,58 @@ export function startSkillDev(target, {
|
|
|
305
424
|
processCleanupHandlers.clear();
|
|
306
425
|
};
|
|
307
426
|
|
|
308
|
-
const
|
|
309
|
-
|
|
427
|
+
const enrichResult = (result) => ({
|
|
428
|
+
...result,
|
|
429
|
+
workbench: workbench
|
|
430
|
+
? {
|
|
431
|
+
enabled: true,
|
|
432
|
+
url: workbench.url,
|
|
433
|
+
port: workbench.port,
|
|
434
|
+
}
|
|
435
|
+
: {
|
|
436
|
+
enabled: false,
|
|
437
|
+
url: null,
|
|
438
|
+
port: null,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const applyDevResult = (result) => {
|
|
310
443
|
const nextNames = result.linkedSkills.map((entry) => entry.name);
|
|
311
444
|
const staleNames = currentNames.filter((name) => !nextNames.includes(name));
|
|
312
445
|
if (staleNames.length > 0) {
|
|
313
446
|
removeSkillLinksByNames(repoRoot, staleNames, normalizeDisplayPath);
|
|
314
447
|
}
|
|
315
448
|
currentNames = nextNames;
|
|
449
|
+
sessionRecord = toDevSessionRecord(repoRoot, target, result, sessionRecord);
|
|
450
|
+
writeDevSession(repoRoot, sessionRecord);
|
|
316
451
|
return result;
|
|
317
452
|
};
|
|
318
453
|
|
|
319
|
-
const
|
|
320
|
-
|
|
454
|
+
const startOrRefreshWorkbench = async () => {
|
|
455
|
+
if (dashboard && !workbench) {
|
|
456
|
+
workbench = await startSkillDevWorkbench({
|
|
457
|
+
repoRoot,
|
|
458
|
+
skillDir,
|
|
459
|
+
open: true,
|
|
460
|
+
disableBrowser: process.env.AGENTPACK_DISABLE_BROWSER === '1',
|
|
461
|
+
});
|
|
462
|
+
} else if (workbench) {
|
|
463
|
+
workbench.refresh();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
initialResult = enrichResult(applyDevResult(devSkill(target, { cwd, sync })));
|
|
468
|
+
const ready = Promise.resolve(startOrRefreshWorkbench())
|
|
469
|
+
.then(() => {
|
|
470
|
+
const result = enrichResult(initialResult);
|
|
471
|
+
initialResult = result;
|
|
472
|
+
onStart(result);
|
|
473
|
+
return result;
|
|
474
|
+
})
|
|
475
|
+
.catch((error) => {
|
|
476
|
+
cleanup();
|
|
477
|
+
throw error;
|
|
478
|
+
});
|
|
321
479
|
|
|
322
480
|
attachProcessCleanup();
|
|
323
481
|
|
|
@@ -325,25 +483,59 @@ export function startSkillDev(target, {
|
|
|
325
483
|
if (closed) return;
|
|
326
484
|
clearTimeout(timer);
|
|
327
485
|
timer = setTimeout(() => {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
486
|
+
Promise.resolve()
|
|
487
|
+
.then(() => applyDevResult(devSkill(target, { cwd, sync })))
|
|
488
|
+
.then(async (result) => {
|
|
489
|
+
await startOrRefreshWorkbench();
|
|
490
|
+
return enrichResult(result);
|
|
491
|
+
})
|
|
492
|
+
.then(onRebuild)
|
|
493
|
+
.catch((error) => {
|
|
494
|
+
onRebuild({ error });
|
|
495
|
+
});
|
|
334
496
|
}, 100);
|
|
335
497
|
});
|
|
336
498
|
|
|
337
499
|
return {
|
|
338
500
|
initialResult,
|
|
501
|
+
ready,
|
|
339
502
|
close() {
|
|
340
503
|
return cleanup();
|
|
341
504
|
},
|
|
342
505
|
};
|
|
343
506
|
}
|
|
344
507
|
|
|
345
|
-
export function unlinkSkill(name, { cwd = process.cwd() } = {}) {
|
|
508
|
+
export function unlinkSkill(name, { cwd = process.cwd(), recursive = false } = {}) {
|
|
346
509
|
const repoRoot = findRepoRoot(cwd);
|
|
510
|
+
const session = readDevSession(repoRoot);
|
|
511
|
+
|
|
512
|
+
if (recursive) {
|
|
513
|
+
if (!session || session.root_skill?.name !== name) {
|
|
514
|
+
throw new AgentpackError('Recursive unlink requires the active dev-session root skill', {
|
|
515
|
+
code: 'linked_skill_recursive_unlink_requires_root',
|
|
516
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
517
|
+
nextSteps: session?.root_skill?.name
|
|
518
|
+
? [{
|
|
519
|
+
action: 'run_command',
|
|
520
|
+
command: `agentpack skills unlink ${session.root_skill.name} --recursive`,
|
|
521
|
+
reason: 'Recursive unlink in v1 only works for the recorded dev-session root skill',
|
|
522
|
+
}]
|
|
523
|
+
: buildDevSessionNextSteps('agentpack skills dev cleanup --force'),
|
|
524
|
+
details: {
|
|
525
|
+
rootSkill: session?.root_skill?.name || null,
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const removed = removeSkillLinksByPaths(repoRoot, session.links || [], normalizeDisplayPath);
|
|
531
|
+
removeDevSession(repoRoot);
|
|
532
|
+
return {
|
|
533
|
+
name,
|
|
534
|
+
unlinked: removed.length > 0,
|
|
535
|
+
removed,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
347
539
|
const existing = [
|
|
348
540
|
join(repoRoot, '.claude', 'skills', name),
|
|
349
541
|
join(repoRoot, '.agents', 'skills', name),
|
|
@@ -365,6 +557,43 @@ export function unlinkSkill(name, { cwd = process.cwd() } = {}) {
|
|
|
365
557
|
};
|
|
366
558
|
}
|
|
367
559
|
|
|
560
|
+
export function cleanupSkillDevSession({ cwd = process.cwd(), force = false } = {}) {
|
|
561
|
+
const repoRoot = findRepoRoot(cwd);
|
|
562
|
+
const session = readDevSession(repoRoot);
|
|
563
|
+
if (!session) {
|
|
564
|
+
return {
|
|
565
|
+
cleaned: false,
|
|
566
|
+
active: false,
|
|
567
|
+
removed: [],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!force && session.status === 'active' && isProcessAlive(session.pid)) {
|
|
572
|
+
throw new AgentpackError('A skills dev session is still active in this repo', {
|
|
573
|
+
code: 'skills_dev_session_active',
|
|
574
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
575
|
+
nextSteps: [
|
|
576
|
+
...buildDevSessionNextSteps('agentpack skills dev cleanup'),
|
|
577
|
+
...buildDevSessionNextSteps('agentpack skills dev cleanup --force'),
|
|
578
|
+
],
|
|
579
|
+
details: {
|
|
580
|
+
rootSkill: session.root_skill?.name || null,
|
|
581
|
+
pid: session.pid,
|
|
582
|
+
startedAt: session.started_at || null,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const result = cleanupRecordedDevSession(repoRoot, session, 'stale');
|
|
588
|
+
return {
|
|
589
|
+
cleaned: true,
|
|
590
|
+
active: false,
|
|
591
|
+
forced: force,
|
|
592
|
+
name: session.root_skill?.name || null,
|
|
593
|
+
removed: result.removed,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
368
597
|
function buildValidateNextSteps(packageMetadata, valid) {
|
|
369
598
|
if (!valid || !packageMetadata.packageName) return [];
|
|
370
599
|
|
package/src/utils/errors.js
CHANGED
|
@@ -14,18 +14,31 @@ export const EXIT_CODES = {
|
|
|
14
14
|
* Carries a machine-readable code and mapped exit code.
|
|
15
15
|
*/
|
|
16
16
|
export class AgentpackError extends Error {
|
|
17
|
-
constructor(message, {
|
|
17
|
+
constructor(message, {
|
|
18
|
+
code,
|
|
19
|
+
exitCode = EXIT_CODES.GENERAL,
|
|
20
|
+
suggestion,
|
|
21
|
+
path,
|
|
22
|
+
nextSteps,
|
|
23
|
+
details,
|
|
24
|
+
} = {}) {
|
|
18
25
|
super(message);
|
|
19
26
|
this.name = 'AgentpackError';
|
|
20
27
|
this.code = code || 'general_error';
|
|
21
28
|
this.exitCode = exitCode;
|
|
22
29
|
this.suggestion = suggestion;
|
|
30
|
+
this.path = path;
|
|
31
|
+
this.nextSteps = nextSteps || [];
|
|
32
|
+
this.details = details || {};
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
toJSON() {
|
|
26
36
|
return {
|
|
27
37
|
error: this.code,
|
|
28
38
|
message: this.message,
|
|
39
|
+
...(this.path && { path: this.path }),
|
|
40
|
+
...(this.nextSteps.length > 0 && { nextSteps: this.nextSteps }),
|
|
41
|
+
...(Object.keys(this.details).length > 0 && { details: this.details }),
|
|
29
42
|
...(this.suggestion && { suggestion: this.suggestion }),
|
|
30
43
|
};
|
|
31
44
|
}
|
|
@@ -58,6 +71,25 @@ export class NotFoundError extends AgentpackError {
|
|
|
58
71
|
export function formatError(err) {
|
|
59
72
|
if (err instanceof AgentpackError) {
|
|
60
73
|
let msg = `Error: ${err.message}`;
|
|
74
|
+
if (err.path) {
|
|
75
|
+
msg += `\nPath: ${err.path}`;
|
|
76
|
+
}
|
|
77
|
+
if (err.nextSteps?.length) {
|
|
78
|
+
for (const step of err.nextSteps) {
|
|
79
|
+
const actionLabel = step.action === 'create_file'
|
|
80
|
+
? `Create ${step.path}`
|
|
81
|
+
: step.action === 'edit_file'
|
|
82
|
+
? `Edit ${step.path}`
|
|
83
|
+
: step.reason;
|
|
84
|
+
msg += `\nNext: ${actionLabel}`;
|
|
85
|
+
if (step.reason && step.reason !== actionLabel) {
|
|
86
|
+
msg += `\nWhy: ${step.reason}`;
|
|
87
|
+
}
|
|
88
|
+
if (step.example) {
|
|
89
|
+
msg += `\nExample:\n${JSON.stringify(step.example, null, 2)}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
61
93
|
if (err.suggestion) {
|
|
62
94
|
msg += `\n\nSuggestion: ${err.suggestion}`;
|
|
63
95
|
}
|