@skill-map/cli 0.47.0 → 0.48.0
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 +3 -3
- package/dist/cli/tutorial/sm-master/SKILL.md +3 -2
- package/dist/cli/tutorial/sm-tutorial/SKILL.md +2 -2
- package/dist/cli.js +441 -108
- package/dist/index.js +7 -5
- package/dist/kernel/index.d.ts +23 -28
- package/dist/kernel/index.js +7 -5
- package/dist/ui/{chunk-UV3QRBRR.js → chunk-2GTH7ZLV.js} +1 -1
- package/dist/ui/chunk-5K5WASS7.js +315 -0
- package/dist/ui/{chunk-WPUUCIS3.js → chunk-AVGEDQNI.js} +1 -1
- package/dist/ui/chunk-AWNZZYAU.js +1 -0
- package/dist/ui/{chunk-7K36273M.js → chunk-BKU4RCQK.js} +1 -1
- package/dist/ui/{chunk-RT7E4S5B.js → chunk-FSJSSLYD.js} +1 -1
- package/dist/ui/{chunk-CRWK2NFZ.js → chunk-IDZ7ZQXM.js} +1 -1
- package/dist/ui/chunk-K2LHWJKO.js +2190 -0
- package/dist/ui/chunk-L56QU7EF.js +2 -0
- package/dist/ui/chunk-NPK64R5H.js +123 -0
- package/dist/ui/chunk-OBYZDEVO.js +1 -0
- package/dist/ui/{chunk-UIUGLD7F.js → chunk-Q747VBQL.js} +3 -3
- package/dist/ui/{chunk-3HLMBEDX.js → chunk-QNFHGPFR.js} +1 -1
- package/dist/ui/chunk-Y6VK27P4.js +1 -0
- package/dist/ui/{chunk-CO2ZOUSD.js → chunk-YQ7ZKAFY.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui/{main-55GYZX6C.js → main-KXXADOQV.js} +3 -3
- package/dist/ui/{styles-HI4A6IWA.css → styles-HWRPHKTJ.css} +1 -1
- package/package.json +5 -3
- package/dist/ui/chunk-3AKR33GE.js +0 -1
- package/dist/ui/chunk-EPBUSS3I.js +0 -2
- package/dist/ui/chunk-K365TVPA.js +0 -1
- package/dist/ui/chunk-PO2VZMOB.js +0 -123
- package/dist/ui/chunk-VNA3TMIO.js +0 -1
- package/dist/ui/chunk-XWU3YFSM.js +0 -315
- package/dist/ui/chunk-YOF6HQCQ.js +0 -2190
- package/dist/ui/chunk-ZZJ7XWDX.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/entry.ts
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a68e2b18-f1ae-5a4d-8cd7-72ddbf9688ab")}catch(e){}}();
|
|
4
4
|
import { existsSync as existsSync33 } from "fs";
|
|
5
5
|
import { Builtins, Cli as Cli2 } from "clipanion";
|
|
6
6
|
|
|
@@ -246,7 +246,7 @@ function bucketByKind(kind, instance, bag) {
|
|
|
246
246
|
// package.json
|
|
247
247
|
var package_default = {
|
|
248
248
|
name: "@skill-map/cli",
|
|
249
|
-
version: "0.
|
|
249
|
+
version: "0.48.0",
|
|
250
250
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
251
251
|
license: "MIT",
|
|
252
252
|
type: "module",
|
|
@@ -299,9 +299,11 @@ var package_default = {
|
|
|
299
299
|
"lint:fix": "eslint . --fix",
|
|
300
300
|
"build-built-ins": "node ../scripts/generate-built-ins.js",
|
|
301
301
|
"built-ins:check": "node ../scripts/generate-built-ins.js --check",
|
|
302
|
+
"view-catalog": "node ../scripts/generate-view-catalog.js",
|
|
303
|
+
"view-catalog:check": "node ../scripts/generate-view-catalog.js --check",
|
|
302
304
|
prebuild: "pnpm build-built-ins",
|
|
303
305
|
validate: "pnpm validate:compile && pnpm validate:test",
|
|
304
|
-
"validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm built-ins:check",
|
|
306
|
+
"validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm built-ins:check && pnpm view-catalog:check",
|
|
305
307
|
"validate:test": "pnpm test:ci",
|
|
306
308
|
pretest: "tsup",
|
|
307
309
|
"pretest:coverage": "tsup",
|
|
@@ -1950,10 +1952,11 @@ var annotationStaleAnalyzer = {
|
|
|
1950
1952
|
slot: "card.footer.right",
|
|
1951
1953
|
icon: "pi-clock",
|
|
1952
1954
|
emitWhenEmpty: true,
|
|
1953
|
-
//
|
|
1954
|
-
//
|
|
1955
|
-
//
|
|
1956
|
-
|
|
1955
|
+
// Sits right after the stability badge and before the severity
|
|
1956
|
+
// counters: stability is the node's declared lifecycle state,
|
|
1957
|
+
// drift is "this node disagrees with its sidecar", then the
|
|
1958
|
+
// warn / error counts anchor the right edge.
|
|
1959
|
+
priority: 20
|
|
1957
1960
|
}
|
|
1958
1961
|
},
|
|
1959
1962
|
evaluate(ctx) {
|
|
@@ -2589,22 +2592,23 @@ var nodeStabilityAnalyzer = {
|
|
|
2589
2592
|
description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
|
|
2590
2593
|
mode: "deterministic",
|
|
2591
2594
|
ui: {
|
|
2592
|
-
//
|
|
2593
|
-
//
|
|
2594
|
-
//
|
|
2595
|
+
// First in the footer-right cluster: stability is the node's
|
|
2596
|
+
// declared lifecycle state, so it leads, followed by the drift
|
|
2597
|
+
// chip and then the severity counters. It's a state badge, not a
|
|
2598
|
+
// count, so it stays left of the numeric zone.
|
|
2595
2599
|
experimental: {
|
|
2596
2600
|
slot: "card.footer.right",
|
|
2597
2601
|
icon: "fa-solid fa-flask",
|
|
2598
2602
|
label: "experimental",
|
|
2599
2603
|
emitWhenEmpty: false,
|
|
2600
|
-
priority:
|
|
2604
|
+
priority: 10
|
|
2601
2605
|
},
|
|
2602
2606
|
deprecated: {
|
|
2603
2607
|
slot: "card.footer.right",
|
|
2604
2608
|
icon: "pi-ban",
|
|
2605
2609
|
label: "deprecated",
|
|
2606
2610
|
emitWhenEmpty: false,
|
|
2607
|
-
priority:
|
|
2611
|
+
priority: 10
|
|
2608
2612
|
}
|
|
2609
2613
|
},
|
|
2610
2614
|
evaluate(ctx) {
|
|
@@ -3009,7 +3013,7 @@ import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
|
3009
3013
|
import { createRequire as createRequire2 } from "module";
|
|
3010
3014
|
import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
|
|
3011
3015
|
|
|
3012
|
-
// kernel/types/view-catalog.ts
|
|
3016
|
+
// kernel/types/view-catalog.generated.ts
|
|
3013
3017
|
var ALL_SLOT_NAMES = [
|
|
3014
3018
|
"card.title.right",
|
|
3015
3019
|
"card.subtitle.left",
|
|
@@ -19999,6 +20003,12 @@ var PLUGINS_TEXTS = {
|
|
|
19999
20003
|
*/
|
|
20000
20004
|
createInvalidId: "{{glyph}} Plugin id must be kebab-case lowercase (got: {{id}}).\n {{hint}}\n",
|
|
20001
20005
|
createInvalidIdHint: "Use a-z, 0-9, and hyphens between segments (e.g. `my-plugin`, `kw-counter`).",
|
|
20006
|
+
/**
|
|
20007
|
+
* §3.1b two-line block. Rejected when the `<kind>` positional is not one
|
|
20008
|
+
* of the closed extension-kind catalog; the hint lists the valid kinds.
|
|
20009
|
+
*/
|
|
20010
|
+
createInvalidKind: "{{glyph}} Unknown extension kind (got: {{kind}}).\n {{hint}}\n",
|
|
20011
|
+
createInvalidKindHint: "Use one of: {{kinds}}.",
|
|
20002
20012
|
/**
|
|
20003
20013
|
* §3.1b two-line block. Target directory exists and `--force` was not
|
|
20004
20014
|
* passed; the hint surfaces the override flag.
|
|
@@ -20006,10 +20016,10 @@ var PLUGINS_TEXTS = {
|
|
|
20006
20016
|
createRefuseOverwrite: "{{glyph}} Refusing to overwrite {{targetDir}}.\n {{hint}}\n",
|
|
20007
20017
|
createRefuseOverwriteHint: "Pass --force to overwrite the existing directory.",
|
|
20008
20018
|
/**
|
|
20009
|
-
* Success block printed after scaffolding.
|
|
20010
|
-
* across every line.
|
|
20019
|
+
* Success block printed after scaffolding. Kind-agnostic (the main stub
|
|
20020
|
+
* path is interpolated). Follows the no-em-dash rule across every line.
|
|
20011
20021
|
*/
|
|
20012
|
-
createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{
|
|
20022
|
+
createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{mainFile}}\n - Run sm plugins doctor to confirm it loads\n - sm plugins slots list: browse slots and input-types\n",
|
|
20013
20023
|
// --- slots list verb -------------------------------------------------
|
|
20014
20024
|
/** Section header for the view-slots catalogue. */
|
|
20015
20025
|
slotsListHeaderViewSlots: " View slots ({{count}})\n",
|
|
@@ -21291,72 +21301,137 @@ function resolveBareToggle(id, catalogue) {
|
|
|
21291
21301
|
|
|
21292
21302
|
// cli/commands/plugins/create.ts
|
|
21293
21303
|
import { existsSync as existsSync25, mkdirSync as mkdirSync5, writeFileSync } from "fs";
|
|
21294
|
-
import { join as join18, resolve as resolve33 } from "path";
|
|
21304
|
+
import { dirname as dirname17, join as join18, resolve as resolve33 } from "path";
|
|
21295
21305
|
import { Command as Command26, Option as Option25 } from "clipanion";
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
|
|
21300
|
-
|
|
21301
|
-
|
|
21302
|
-
|
|
21303
|
-
|
|
21304
|
-
|
|
21305
|
-
|
|
21306
|
-
|
|
21307
|
-
|
|
21308
|
-
|
|
21309
|
-
|
|
21310
|
-
|
|
21311
|
-
|
|
21312
|
-
|
|
21313
|
-
|
|
21314
|
-
|
|
21315
|
-
|
|
21316
|
-
|
|
21317
|
-
|
|
21306
|
+
|
|
21307
|
+
// cli/commands/plugins/scaffold/action.ts
|
|
21308
|
+
function indexStub(extId) {
|
|
21309
|
+
return `/**
|
|
21310
|
+
* Generated by \`sm plugins create\`. Edit the invoke() body.
|
|
21311
|
+
*
|
|
21312
|
+
* Loader contract: default export object literal; \`kind\` (action) and id
|
|
21313
|
+
* (\`${extId}\`) come from the folder layout (structure-as-truth), so
|
|
21314
|
+
* neither is declared here. Every Action carries a sibling
|
|
21315
|
+
* \`report.schema.json\`; a deterministic Action must NOT carry a
|
|
21316
|
+
* \`prompt.md\` (that is the probabilistic surface).
|
|
21317
|
+
*
|
|
21318
|
+
* \`invoke(input, ctx)\` returns \`{ report, writes? }\`. The report must
|
|
21319
|
+
* match this folder's \`report.schema.json\` (which extends the shared
|
|
21320
|
+
* report-base: \`confidence\` + \`safety\`). \`writes\` is an optional
|
|
21321
|
+
* array of side-effect descriptors the kernel materialises after the
|
|
21322
|
+
* call returns (e.g. a sidecar write); keep \`invoke()\` itself pure.
|
|
21323
|
+
*
|
|
21324
|
+
* See: spec/plugin-author-guide.md, spec/schemas/extensions/action.schema.json
|
|
21325
|
+
*/
|
|
21326
|
+
export default {
|
|
21327
|
+
version: '0.1.0',
|
|
21328
|
+
description: 'Demo deterministic action; returns a fixed report.',
|
|
21329
|
+
mode: 'deterministic',
|
|
21330
|
+
|
|
21331
|
+
invoke(input, ctx) {
|
|
21332
|
+
return {
|
|
21333
|
+
report: {
|
|
21334
|
+
ok: true,
|
|
21335
|
+
confidence: 1,
|
|
21336
|
+
safety: { injectionDetected: false, contentQuality: 'unknown' },
|
|
21337
|
+
},
|
|
21338
|
+
};
|
|
21339
|
+
},
|
|
21340
|
+
};
|
|
21341
|
+
`;
|
|
21342
|
+
}
|
|
21343
|
+
function reportSchema(pluginId, extId) {
|
|
21344
|
+
const schema = {
|
|
21345
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
21346
|
+
$id: `urn:skill-map:${pluginId}/${extId}/report`,
|
|
21347
|
+
title: "DemoReport",
|
|
21348
|
+
description: "Report returned by the demo action. Deterministic: confidence is fixed at 1 and no injection is detected.",
|
|
21349
|
+
type: "object",
|
|
21350
|
+
required: ["confidence", "safety"],
|
|
21351
|
+
additionalProperties: false,
|
|
21352
|
+
properties: {
|
|
21353
|
+
ok: { type: "boolean" },
|
|
21354
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
21355
|
+
safety: {
|
|
21356
|
+
type: "object",
|
|
21357
|
+
required: ["injectionDetected", "contentQuality"],
|
|
21358
|
+
additionalProperties: false,
|
|
21359
|
+
properties: {
|
|
21360
|
+
injectionDetected: { type: "boolean" },
|
|
21361
|
+
contentQuality: {
|
|
21362
|
+
type: "string",
|
|
21363
|
+
enum: ["high", "medium", "low", "unknown"]
|
|
21364
|
+
}
|
|
21365
|
+
}
|
|
21366
|
+
}
|
|
21318
21367
|
}
|
|
21319
|
-
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
|
|
21324
|
-
|
|
21325
|
-
|
|
21326
|
-
|
|
21327
|
-
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21368
|
+
};
|
|
21369
|
+
return JSON.stringify(schema, null, 2) + "\n";
|
|
21370
|
+
}
|
|
21371
|
+
var actionGenerator = {
|
|
21372
|
+
folder: "actions",
|
|
21373
|
+
files(pluginId) {
|
|
21374
|
+
const ext = `${pluginId}-action`;
|
|
21375
|
+
return [
|
|
21376
|
+
{ relPath: `actions/${ext}/index.js`, contents: indexStub(ext) },
|
|
21377
|
+
{ relPath: `actions/${ext}/report.schema.json`, contents: reportSchema(pluginId, ext) }
|
|
21378
|
+
];
|
|
21379
|
+
}
|
|
21380
|
+
};
|
|
21381
|
+
|
|
21382
|
+
// cli/commands/plugins/scaffold/analyzer.ts
|
|
21383
|
+
function stub(extId) {
|
|
21384
|
+
return `/**
|
|
21385
|
+
* Generated by \`sm plugins create\`. Edit the evaluate() body.
|
|
21386
|
+
*
|
|
21387
|
+
* Loader contract: default export object literal; \`kind\` (analyzer) and
|
|
21388
|
+
* id (\`${extId}\`) come from the folder layout (structure-as-truth), so
|
|
21389
|
+
* neither is declared here. Re-declaring them is \`invalid-manifest\`.
|
|
21390
|
+
*
|
|
21391
|
+
* \`evaluate(ctx)\` runs once per scan and returns \`Issue[]\`. The kernel
|
|
21392
|
+
* stamps \`analyzerId\` from this extension's id and validates each issue
|
|
21393
|
+
* (\`severity\` must be 'error' | 'warn' | 'info'; off-spec issues drop
|
|
21394
|
+
* with an \`extension.error\` diagnostic). \`ctx.nodes\` / \`ctx.links\`
|
|
21395
|
+
* are the scanned graph; \`ctx.accumulatedIssues\` is the live issue
|
|
21396
|
+
* accumulator (read-only) for late-phase aggregate analyzers.
|
|
21397
|
+
*
|
|
21398
|
+
* See: spec/plugin-author-guide.md, spec/schemas/extensions/analyzer.schema.json
|
|
21399
|
+
*/
|
|
21400
|
+
export default {
|
|
21401
|
+
version: '0.1.0',
|
|
21402
|
+
description: 'Flags nodes that have no inbound or outbound links.',
|
|
21403
|
+
mode: 'deterministic',
|
|
21404
|
+
|
|
21405
|
+
evaluate(ctx) {
|
|
21406
|
+
const issues = [];
|
|
21407
|
+
for (const node of ctx.nodes) {
|
|
21408
|
+
if (node.linksInCount === 0 && node.linksOutCount === 0) {
|
|
21409
|
+
issues.push({
|
|
21410
|
+
severity: 'info',
|
|
21411
|
+
nodeIds: [node.path],
|
|
21412
|
+
message: 'Node has no links in or out (isolated).',
|
|
21413
|
+
});
|
|
21414
|
+
}
|
|
21331
21415
|
}
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
21338
|
-
|
|
21339
|
-
|
|
21340
|
-
}
|
|
21341
|
-
|
|
21342
|
-
join18(targetDir, "plugin.json"),
|
|
21343
|
-
JSON.stringify(manifest, null, 2) + "\n"
|
|
21344
|
-
);
|
|
21345
|
-
writeFileSync(
|
|
21346
|
-
join18(targetDir, "extractors", extractorName, "index.js"),
|
|
21347
|
-
scaffolderExtractorStub(extractorName)
|
|
21348
|
-
);
|
|
21349
|
-
writeFileSync(join18(targetDir, "README.md"), scaffolderReadme(this.pluginId));
|
|
21350
|
-
this.printer.data(
|
|
21351
|
-
tx(PLUGINS_TEXTS.createSuccess, {
|
|
21352
|
-
targetDir: sanitizeForTerminal(targetDir),
|
|
21353
|
-
pluginId: this.pluginId
|
|
21354
|
-
})
|
|
21355
|
-
);
|
|
21356
|
-
return ExitCode.Ok;
|
|
21416
|
+
return issues;
|
|
21417
|
+
},
|
|
21418
|
+
};
|
|
21419
|
+
`;
|
|
21420
|
+
}
|
|
21421
|
+
var analyzerGenerator = {
|
|
21422
|
+
folder: "analyzers",
|
|
21423
|
+
files(pluginId) {
|
|
21424
|
+
const ext = `${pluginId}-analyzer`;
|
|
21425
|
+
return [{ relPath: `analyzers/${ext}/index.js`, contents: stub(ext) }];
|
|
21357
21426
|
}
|
|
21358
21427
|
};
|
|
21359
|
-
|
|
21428
|
+
|
|
21429
|
+
// cli/commands/plugins/scaffold/defaults.ts
|
|
21430
|
+
var DEFAULT_SLOT = "card.footer.left";
|
|
21431
|
+
var DEFAULT_INPUT_TYPE = "string-list";
|
|
21432
|
+
|
|
21433
|
+
// cli/commands/plugins/scaffold/extractor.ts
|
|
21434
|
+
function stub2(extId) {
|
|
21360
21435
|
return `/**
|
|
21361
21436
|
* Generated by \`sm plugins create\`. Edit the extract() body.
|
|
21362
21437
|
*
|
|
@@ -21366,19 +21441,19 @@ function scaffolderExtractorStub(extractorId) {
|
|
|
21366
21441
|
* a \`load-error\`.
|
|
21367
21442
|
*
|
|
21368
21443
|
* Folder convention: this file lives at
|
|
21369
|
-
* \`extractors/${
|
|
21370
|
-
*
|
|
21371
|
-
* (\`extractor\`) from the parent folder and the id (\`${
|
|
21372
|
-
*
|
|
21373
|
-
*
|
|
21444
|
+
* \`extractors/${extId}/index.js\`. The folder layout is the source of
|
|
21445
|
+
* truth (structure-as-truth): the loader derives \`kind\`
|
|
21446
|
+
* (\`extractor\`) from the parent folder and the id (\`${extId}\`) from
|
|
21447
|
+
* the leaf folder, and injects \`pluginId\` from the plugin, so none of
|
|
21448
|
+
* them are declared here. Re-declaring \`kind\` / \`id\` is rejected as
|
|
21374
21449
|
* \`invalid-manifest\`.
|
|
21375
21450
|
*
|
|
21376
21451
|
* Declared view contributions (\`ui\`):
|
|
21377
|
-
* - 'count' \u2192 slot \`
|
|
21378
|
-
*
|
|
21452
|
+
* - 'count' \u2192 slot \`${DEFAULT_SLOT}\` (renders as a chip in the node
|
|
21453
|
+
* card footer)
|
|
21379
21454
|
*
|
|
21380
21455
|
* Declared settings (\`settings\`):
|
|
21381
|
-
* - 'keywords' (
|
|
21456
|
+
* - 'keywords' (${DEFAULT_INPUT_TYPE}) \u2192 exposed as ctx.settings.keywords
|
|
21382
21457
|
*
|
|
21383
21458
|
* See: spec/plugin-author-guide.md \xA7View contributions
|
|
21384
21459
|
* spec/view-slots.md
|
|
@@ -21390,7 +21465,7 @@ export default {
|
|
|
21390
21465
|
|
|
21391
21466
|
settings: {
|
|
21392
21467
|
keywords: {
|
|
21393
|
-
type: '
|
|
21468
|
+
type: '${DEFAULT_INPUT_TYPE}',
|
|
21394
21469
|
label: 'Keywords to track',
|
|
21395
21470
|
description: 'Words counted across each scanned node body.',
|
|
21396
21471
|
default: ['TODO', 'FIXME'],
|
|
@@ -21400,7 +21475,7 @@ export default {
|
|
|
21400
21475
|
|
|
21401
21476
|
ui: {
|
|
21402
21477
|
count: {
|
|
21403
|
-
slot: '
|
|
21478
|
+
slot: '${DEFAULT_SLOT}',
|
|
21404
21479
|
icon: '\u{1F50D}',
|
|
21405
21480
|
label: 'kw',
|
|
21406
21481
|
emitWhenEmpty: false,
|
|
@@ -21421,26 +21496,284 @@ export default {
|
|
|
21421
21496
|
};
|
|
21422
21497
|
`;
|
|
21423
21498
|
}
|
|
21424
|
-
|
|
21499
|
+
var extractorGenerator = {
|
|
21500
|
+
folder: "extractors",
|
|
21501
|
+
files(pluginId) {
|
|
21502
|
+
const ext = `${pluginId}-extractor`;
|
|
21503
|
+
return [{ relPath: `extractors/${ext}/index.js`, contents: stub2(ext) }];
|
|
21504
|
+
}
|
|
21505
|
+
};
|
|
21506
|
+
|
|
21507
|
+
// cli/commands/plugins/scaffold/formatter.ts
|
|
21508
|
+
function stub3(extId) {
|
|
21509
|
+
return `/**
|
|
21510
|
+
* Generated by \`sm plugins create\`. Edit the format() body.
|
|
21511
|
+
*
|
|
21512
|
+
* Loader contract: default export object literal; \`kind\` (formatter) and
|
|
21513
|
+
* id (\`${extId}\`) come from the folder layout (structure-as-truth). The
|
|
21514
|
+
* format id used by \`sm graph --format ${extId}\` is the folder name; do
|
|
21515
|
+
* NOT declare \`formatId\` (derived, re-declaring it is invalid-manifest).
|
|
21516
|
+
*
|
|
21517
|
+
* \`format(ctx)\` returns a string. \`ctx.scanResult\` is the full
|
|
21518
|
+
* persisted scan (the canonical input); \`ctx.nodes\` / \`ctx.links\` /
|
|
21519
|
+
* \`ctx.issues\` are the same data split out for convenience. Output has
|
|
21520
|
+
* no trailing newline guarantee; the calling verb adds one if it needs.
|
|
21521
|
+
*
|
|
21522
|
+
* See: spec/plugin-author-guide.md, spec/schemas/extensions/formatter.schema.json
|
|
21523
|
+
*/
|
|
21524
|
+
export default {
|
|
21525
|
+
version: '0.1.0',
|
|
21526
|
+
description: 'Renders the scan as a one-line node/link/issue tally.',
|
|
21527
|
+
contentType: 'text/plain',
|
|
21528
|
+
|
|
21529
|
+
format(ctx) {
|
|
21530
|
+
const nodes = ctx.nodes || [];
|
|
21531
|
+
const links = ctx.links || [];
|
|
21532
|
+
const issues = ctx.issues || [];
|
|
21533
|
+
return \`nodes=\${nodes.length} links=\${links.length} issues=\${issues.length}\`;
|
|
21534
|
+
},
|
|
21535
|
+
};
|
|
21536
|
+
`;
|
|
21537
|
+
}
|
|
21538
|
+
var formatterGenerator = {
|
|
21539
|
+
folder: "formatters",
|
|
21540
|
+
files(pluginId) {
|
|
21541
|
+
const ext = `${pluginId}-formatter`;
|
|
21542
|
+
return [{ relPath: `formatters/${ext}/index.js`, contents: stub3(ext) }];
|
|
21543
|
+
}
|
|
21544
|
+
};
|
|
21545
|
+
|
|
21546
|
+
// cli/commands/plugins/scaffold/hook.ts
|
|
21547
|
+
function stub4(extId) {
|
|
21548
|
+
return `/**
|
|
21549
|
+
* Generated by \`sm plugins create\`. Edit the on() body.
|
|
21550
|
+
*
|
|
21551
|
+
* Loader contract: default export object literal; \`kind\` (hook) and id
|
|
21552
|
+
* (\`${extId}\`) come from the folder layout (structure-as-truth). Hooks
|
|
21553
|
+
* are deterministic-only: to run LLM work, enqueue a probabilistic Action
|
|
21554
|
+
* via \`ctx.queue('<plugin>/<action>', payload)\` instead.
|
|
21555
|
+
*
|
|
21556
|
+
* \`triggers\` is the curated set this hook subscribes to (one of: boot,
|
|
21557
|
+
* scan.started, scan.completed, extractor.completed, analyzer.completed,
|
|
21558
|
+
* action.completed, job.spawning, job.completed, job.failed, shutdown).
|
|
21559
|
+
* \`on(ctx)\` reads the event off \`ctx.event\` (\`ctx.event.data\` is the
|
|
21560
|
+
* payload); the dispatcher catches errors so a throw never breaks the verb.
|
|
21561
|
+
*
|
|
21562
|
+
* See: spec/plugin-author-guide.md, spec/schemas/extensions/hook.schema.json
|
|
21563
|
+
*/
|
|
21564
|
+
export default {
|
|
21565
|
+
version: '0.1.0',
|
|
21566
|
+
description: 'Demo hook; reacts once on boot.',
|
|
21567
|
+
triggers: ['boot'],
|
|
21568
|
+
|
|
21569
|
+
async on(ctx) {
|
|
21570
|
+
const data = (ctx.event && ctx.event.data) || {};
|
|
21571
|
+
// No-op demo. A real hook reacts here, e.g. inspect \`data\`, write a
|
|
21572
|
+
// log line, or enqueue an action with ctx.queue(...).
|
|
21573
|
+
void data;
|
|
21574
|
+
},
|
|
21575
|
+
};
|
|
21576
|
+
`;
|
|
21577
|
+
}
|
|
21578
|
+
var hookGenerator = {
|
|
21579
|
+
folder: "hooks",
|
|
21580
|
+
files(pluginId) {
|
|
21581
|
+
const ext = `${pluginId}-hook`;
|
|
21582
|
+
return [{ relPath: `hooks/${ext}/index.js`, contents: stub4(ext) }];
|
|
21583
|
+
}
|
|
21584
|
+
};
|
|
21585
|
+
|
|
21586
|
+
// cli/commands/plugins/scaffold/provider.ts
|
|
21587
|
+
function titleCase(id) {
|
|
21588
|
+
return id.split("-").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
|
|
21589
|
+
}
|
|
21590
|
+
function stub5(pluginId, extId) {
|
|
21591
|
+
const label = titleCase(pluginId);
|
|
21592
|
+
return `/**
|
|
21593
|
+
* Generated by \`sm plugins create\`. Skeleton provider: edit classify()
|
|
21594
|
+
* and add a kind under \`kinds/\` (see the TODO below).
|
|
21595
|
+
*
|
|
21596
|
+
* Loader contract: default export object literal; \`kind\` (provider) and
|
|
21597
|
+
* id (\`${extId}\`) come from the folder layout (structure-as-truth). Do
|
|
21598
|
+
* NOT declare a \`kinds\` map here, it is rejected as invalid-manifest;
|
|
21599
|
+
* kinds are discovered from \`kinds/<kindName>/\` folders at the PLUGIN
|
|
21600
|
+
* root.
|
|
21601
|
+
*
|
|
21602
|
+
* \`presentation\` (label + hex color) is required: the UI reads it for
|
|
21603
|
+
* the lens dropdown, the topbar chip, and the per-node provider chip.
|
|
21604
|
+
* \`read\` configures file discovery (defaults to \`.md\` +
|
|
21605
|
+
* frontmatter-yaml when omitted). \`classify(path, frontmatter)\` returns
|
|
21606
|
+
* the node kind for files this provider owns, or null to let the file
|
|
21607
|
+
* fall through to the markdown fallback.
|
|
21608
|
+
*
|
|
21609
|
+
* TODO: this skeleton classifies nothing (returns null). To emit nodes:
|
|
21610
|
+
* 1. Pick a kind name, e.g. 'note'.
|
|
21611
|
+
* 2. Create \`kinds/note/schema.json\` (frontmatter JSON Schema,
|
|
21612
|
+
* allOf + $ref to frontmatter/base.schema.json).
|
|
21613
|
+
* 3. Create \`kinds/note/kind.json\` ({ "ui": { "label": "...",
|
|
21614
|
+
* "color": "#......" } }).
|
|
21615
|
+
* 4. Return 'note' from classify() for the files you own.
|
|
21616
|
+
*
|
|
21617
|
+
* See: spec/plugin-author-guide.md, spec/schemas/extensions/provider.schema.json
|
|
21618
|
+
*/
|
|
21619
|
+
export default {
|
|
21620
|
+
version: '0.1.0',
|
|
21621
|
+
description: 'Skeleton provider; classify() returns null until a kind is added.',
|
|
21622
|
+
|
|
21623
|
+
presentation: {
|
|
21624
|
+
label: '${label}',
|
|
21625
|
+
color: '#6b7280',
|
|
21626
|
+
},
|
|
21627
|
+
|
|
21628
|
+
read: { extensions: ['.md'], parser: 'frontmatter-yaml' },
|
|
21629
|
+
|
|
21630
|
+
classify(path, frontmatter) {
|
|
21631
|
+
// TODO: return a kind name (see header) for files this provider owns.
|
|
21632
|
+
void path;
|
|
21633
|
+
void frontmatter;
|
|
21634
|
+
return null;
|
|
21635
|
+
},
|
|
21636
|
+
};
|
|
21637
|
+
`;
|
|
21638
|
+
}
|
|
21639
|
+
var providerGenerator = {
|
|
21640
|
+
folder: "providers",
|
|
21641
|
+
files(pluginId) {
|
|
21642
|
+
const ext = `${pluginId}-provider`;
|
|
21643
|
+
return [{ relPath: `providers/${ext}/index.js`, contents: stub5(pluginId, ext) }];
|
|
21644
|
+
}
|
|
21645
|
+
};
|
|
21646
|
+
|
|
21647
|
+
// cli/commands/plugins/scaffold/readme.ts
|
|
21648
|
+
function scaffolderReadme(pluginId, kind, mainFileRel) {
|
|
21425
21649
|
return `# ${pluginId}
|
|
21426
21650
|
|
|
21427
|
-
Generated by \`sm plugins create\`. Edit
|
|
21651
|
+
Generated by \`sm plugins create ${kind} ${pluginId}\`. Edit \`${mainFileRel}\` to taste.
|
|
21428
21652
|
|
|
21429
21653
|
## Verbs
|
|
21430
21654
|
|
|
21431
21655
|
- \`sm plugins show ${pluginId}\`: manifest + load status
|
|
21432
21656
|
- \`sm plugins doctor\`: full plugin diagnostic
|
|
21433
|
-
- \`sm scan\`: re-emit contributions
|
|
21657
|
+
- \`sm scan\`: re-emit contributions / re-run analysis
|
|
21434
21658
|
|
|
21435
21659
|
## Resources
|
|
21436
21660
|
|
|
21437
|
-
- \`spec/plugin-author-guide.md
|
|
21438
|
-
- \`spec/view-slots.md\`: the closed catalog of slots
|
|
21439
|
-
- \`spec/input-types.md\`: the closed catalog of input-types
|
|
21440
|
-
- \`sm plugins slots list\`: browse the catalog from the CLI
|
|
21661
|
+
- \`spec/plugin-author-guide.md\`: authoring guide for every kind
|
|
21662
|
+
- \`spec/view-slots.md\`: the closed catalog of view slots
|
|
21663
|
+
- \`spec/input-types.md\`: the closed catalog of setting input-types
|
|
21664
|
+
- \`sm plugins slots list\`: browse the slot / input-type catalog from the CLI
|
|
21441
21665
|
`;
|
|
21442
21666
|
}
|
|
21443
21667
|
|
|
21668
|
+
// cli/commands/plugins/scaffold/index.ts
|
|
21669
|
+
var GENERATORS = {
|
|
21670
|
+
provider: providerGenerator,
|
|
21671
|
+
extractor: extractorGenerator,
|
|
21672
|
+
analyzer: analyzerGenerator,
|
|
21673
|
+
action: actionGenerator,
|
|
21674
|
+
formatter: formatterGenerator,
|
|
21675
|
+
hook: hookGenerator
|
|
21676
|
+
};
|
|
21677
|
+
function pluginManifest(specVersion) {
|
|
21678
|
+
return {
|
|
21679
|
+
version: "0.1.0",
|
|
21680
|
+
specCompat: `^${specVersion}`,
|
|
21681
|
+
catalogCompat: "^1.0.0",
|
|
21682
|
+
description: "Generated by `sm plugins create`. Edit to taste."
|
|
21683
|
+
};
|
|
21684
|
+
}
|
|
21685
|
+
function generateScaffold(kind, pluginId, specVersion) {
|
|
21686
|
+
const extFiles = GENERATORS[kind].files(pluginId);
|
|
21687
|
+
const main = extFiles[0];
|
|
21688
|
+
if (!main) {
|
|
21689
|
+
throw new Error(`scaffold generator for kind '${kind}' returned no files`);
|
|
21690
|
+
}
|
|
21691
|
+
return [
|
|
21692
|
+
{
|
|
21693
|
+
relPath: "plugin.json",
|
|
21694
|
+
contents: JSON.stringify(pluginManifest(specVersion), null, 2) + "\n"
|
|
21695
|
+
},
|
|
21696
|
+
...extFiles,
|
|
21697
|
+
{ relPath: "README.md", contents: scaffolderReadme(pluginId, kind, main.relPath) }
|
|
21698
|
+
];
|
|
21699
|
+
}
|
|
21700
|
+
|
|
21701
|
+
// cli/commands/plugins/create.ts
|
|
21702
|
+
var PluginsCreateCommand = class extends SmCommand {
|
|
21703
|
+
static paths = [["plugins", "create"]];
|
|
21704
|
+
static usage = Command26.Usage({
|
|
21705
|
+
category: "Plugins",
|
|
21706
|
+
description: "Scaffold a new plugin directory.",
|
|
21707
|
+
details: "Emits plugin.json + a per-kind extension stub + README. `<kind>` is one of: provider, extractor, analyzer, action, formatter, hook. The extractor stub ships one view contribution (slot `card.footer.left`) and one setting (`string-list`); edit to taste. Use `sm plugins slots list` to browse the slot / input-type catalog.",
|
|
21708
|
+
examples: [
|
|
21709
|
+
["Scaffold an extractor", "$0 plugins create extractor kw-counter"],
|
|
21710
|
+
["Scaffold an analyzer", "$0 plugins create analyzer my-linter"],
|
|
21711
|
+
["Scaffold a provider", "$0 plugins create provider my-vendor"]
|
|
21712
|
+
]
|
|
21713
|
+
});
|
|
21714
|
+
// First positional: the extension kind (required). Declared before
|
|
21715
|
+
// `pluginId` so clipanion assigns it the first positional slot.
|
|
21716
|
+
kind = Option25.String({ required: true, name: "kind" });
|
|
21717
|
+
pluginId = Option25.String({ required: true, name: "plugin-id" });
|
|
21718
|
+
at = Option25.String("--at", { required: false });
|
|
21719
|
+
force = Option25.Boolean("--force", false);
|
|
21720
|
+
async run() {
|
|
21721
|
+
const ansi = this.ansiFor("stderr");
|
|
21722
|
+
const errGlyph = ansi.red("\u2715");
|
|
21723
|
+
if (!EXTENSION_KINDS.includes(this.kind)) {
|
|
21724
|
+
this.printer.error(
|
|
21725
|
+
tx(PLUGINS_TEXTS.createInvalidKind, {
|
|
21726
|
+
glyph: errGlyph,
|
|
21727
|
+
kind: sanitizeForTerminal(this.kind),
|
|
21728
|
+
hint: ansi.dim(
|
|
21729
|
+
tx(PLUGINS_TEXTS.createInvalidKindHint, { kinds: EXTENSION_KINDS.join(", ") })
|
|
21730
|
+
)
|
|
21731
|
+
})
|
|
21732
|
+
);
|
|
21733
|
+
return ExitCode.Error;
|
|
21734
|
+
}
|
|
21735
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(this.pluginId)) {
|
|
21736
|
+
this.printer.error(
|
|
21737
|
+
tx(PLUGINS_TEXTS.createInvalidId, {
|
|
21738
|
+
glyph: errGlyph,
|
|
21739
|
+
id: sanitizeForTerminal(this.pluginId),
|
|
21740
|
+
hint: ansi.dim(PLUGINS_TEXTS.createInvalidIdHint)
|
|
21741
|
+
})
|
|
21742
|
+
);
|
|
21743
|
+
return ExitCode.Error;
|
|
21744
|
+
}
|
|
21745
|
+
const kind = this.kind;
|
|
21746
|
+
const ctx = defaultRuntimeContext();
|
|
21747
|
+
const baseDir = defaultProjectPluginsDir(ctx);
|
|
21748
|
+
const targetDir = this.at ? resolve33(this.at) : join18(baseDir, this.pluginId);
|
|
21749
|
+
if (existsSync25(targetDir) && !this.force) {
|
|
21750
|
+
this.printer.error(
|
|
21751
|
+
tx(PLUGINS_TEXTS.createRefuseOverwrite, {
|
|
21752
|
+
glyph: errGlyph,
|
|
21753
|
+
targetDir: sanitizeForTerminal(targetDir),
|
|
21754
|
+
hint: ansi.dim(PLUGINS_TEXTS.createRefuseOverwriteHint)
|
|
21755
|
+
})
|
|
21756
|
+
);
|
|
21757
|
+
return ExitCode.Error;
|
|
21758
|
+
}
|
|
21759
|
+
const specVersion = installedSpecVersion();
|
|
21760
|
+
const files = generateScaffold(kind, this.pluginId, specVersion);
|
|
21761
|
+
for (const file of files) {
|
|
21762
|
+
const abs = join18(targetDir, file.relPath);
|
|
21763
|
+
mkdirSync5(dirname17(abs), { recursive: true });
|
|
21764
|
+
writeFileSync(abs, file.contents);
|
|
21765
|
+
}
|
|
21766
|
+
const mainFile = `${kind}s/${this.pluginId}-${kind}/index.js`;
|
|
21767
|
+
this.printer.data(
|
|
21768
|
+
tx(PLUGINS_TEXTS.createSuccess, {
|
|
21769
|
+
targetDir: sanitizeForTerminal(targetDir),
|
|
21770
|
+
mainFile
|
|
21771
|
+
})
|
|
21772
|
+
);
|
|
21773
|
+
return ExitCode.Ok;
|
|
21774
|
+
}
|
|
21775
|
+
};
|
|
21776
|
+
|
|
21444
21777
|
// cli/commands/plugins/slots.ts
|
|
21445
21778
|
import { Command as Command27 } from "clipanion";
|
|
21446
21779
|
|
|
@@ -22078,7 +22411,7 @@ var SCAN_TEXTS = {
|
|
|
22078
22411
|
import { Command as Command30, Option as Option28 } from "clipanion";
|
|
22079
22412
|
|
|
22080
22413
|
// core/watcher/runtime.ts
|
|
22081
|
-
import { dirname as
|
|
22414
|
+
import { dirname as dirname18 } from "path";
|
|
22082
22415
|
|
|
22083
22416
|
// core/runtime/fresh-resolver.ts
|
|
22084
22417
|
async function buildFreshResolver(deps) {
|
|
@@ -22321,7 +22654,7 @@ function createWatcherRuntime(opts) {
|
|
|
22321
22654
|
roots: [
|
|
22322
22655
|
cwd,
|
|
22323
22656
|
// parent of `.skillmapignore`
|
|
22324
|
-
|
|
22657
|
+
dirname18(settingsPath)
|
|
22325
22658
|
// parent of `.skill-map/settings.json`
|
|
22326
22659
|
],
|
|
22327
22660
|
cwd,
|
|
@@ -26699,7 +27032,7 @@ function validateNoUi(noUi, uiDist) {
|
|
|
26699
27032
|
|
|
26700
27033
|
// server/paths.ts
|
|
26701
27034
|
import { existsSync as existsSync30, statSync as statSync10 } from "fs";
|
|
26702
|
-
import { dirname as
|
|
27035
|
+
import { dirname as dirname19, isAbsolute as isAbsolute11, join as join20, resolve as resolve37 } from "path";
|
|
26703
27036
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
26704
27037
|
var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
|
|
26705
27038
|
var PACKAGE_UI_REL = "ui";
|
|
@@ -26724,7 +27057,7 @@ function isUiBundleDir(path) {
|
|
|
26724
27057
|
function resolvePackageBundledUi() {
|
|
26725
27058
|
let here;
|
|
26726
27059
|
try {
|
|
26727
|
-
here =
|
|
27060
|
+
here = dirname19(fileURLToPath6(import.meta.url));
|
|
26728
27061
|
} catch {
|
|
26729
27062
|
return null;
|
|
26730
27063
|
}
|
|
@@ -26737,7 +27070,7 @@ function resolvePackageBundledUiFrom(here) {
|
|
|
26737
27070
|
if (isUiBundleDir(candidate)) return candidate;
|
|
26738
27071
|
const distHere = join20(current, "dist", PACKAGE_UI_REL);
|
|
26739
27072
|
if (isUiBundleDir(distHere)) return distHere;
|
|
26740
|
-
const parent =
|
|
27073
|
+
const parent = dirname19(current);
|
|
26741
27074
|
if (parent === current) return null;
|
|
26742
27075
|
current = parent;
|
|
26743
27076
|
}
|
|
@@ -26748,7 +27081,7 @@ function walkUpForUi(startDir) {
|
|
|
26748
27081
|
for (let i = 0; i < 64; i++) {
|
|
26749
27082
|
const candidate = join20(current, DEFAULT_UI_REL);
|
|
26750
27083
|
if (isUiBundleDir(candidate)) return candidate;
|
|
26751
|
-
const parent =
|
|
27084
|
+
const parent = dirname19(current);
|
|
26752
27085
|
if (parent === current) return null;
|
|
26753
27086
|
current = parent;
|
|
26754
27087
|
}
|
|
@@ -28587,7 +28920,7 @@ var STUB_COMMANDS = [
|
|
|
28587
28920
|
|
|
28588
28921
|
// cli/commands/tutorial.ts
|
|
28589
28922
|
import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync6, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync11 } from "fs";
|
|
28590
|
-
import { dirname as
|
|
28923
|
+
import { dirname as dirname20, join as join21, resolve as resolve39 } from "path";
|
|
28591
28924
|
import { createInterface as createInterface5 } from "readline";
|
|
28592
28925
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
28593
28926
|
import { Command as Command37, Option as Option35 } from "clipanion";
|
|
@@ -28650,14 +28983,14 @@ var VARIANT_SPECS = {
|
|
|
28650
28983
|
tutorial: {
|
|
28651
28984
|
slug: "sm-tutorial",
|
|
28652
28985
|
sourceDir: ".claude/skills/sm-tutorial",
|
|
28653
|
-
triggerEn: "
|
|
28654
|
-
triggerEs: "
|
|
28986
|
+
triggerEn: "run the tutorial",
|
|
28987
|
+
triggerEs: "ejecuta el tutorial"
|
|
28655
28988
|
},
|
|
28656
28989
|
master: {
|
|
28657
28990
|
slug: "sm-master",
|
|
28658
28991
|
sourceDir: ".claude/skills/sm-master",
|
|
28659
|
-
triggerEn: "
|
|
28660
|
-
triggerEs: "tutorial maestro"
|
|
28992
|
+
triggerEn: "run the master tutorial",
|
|
28993
|
+
triggerEs: "ejecuta el tutorial maestro"
|
|
28661
28994
|
}
|
|
28662
28995
|
};
|
|
28663
28996
|
var TutorialCommand = class extends SmCommand {
|
|
@@ -28733,7 +29066,7 @@ var TutorialCommand = class extends SmCommand {
|
|
|
28733
29066
|
}
|
|
28734
29067
|
try {
|
|
28735
29068
|
rmSync2(targetDir, { recursive: true, force: true });
|
|
28736
|
-
mkdirSync6(
|
|
29069
|
+
mkdirSync6(dirname20(targetDir), { recursive: true });
|
|
28737
29070
|
cpSync2(sourceDir, targetDir, { recursive: true });
|
|
28738
29071
|
} catch (err) {
|
|
28739
29072
|
this.printer.error(
|
|
@@ -28920,7 +29253,7 @@ function resolveSkillSourceDir(variant) {
|
|
|
28920
29253
|
const cached = cachedSourceDirs.get(variant);
|
|
28921
29254
|
if (cached !== void 0) return cached;
|
|
28922
29255
|
const spec = VARIANT_SPECS[variant];
|
|
28923
|
-
const here =
|
|
29256
|
+
const here = dirname20(fileURLToPath7(import.meta.url));
|
|
28924
29257
|
const candidates = [
|
|
28925
29258
|
// dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
|
|
28926
29259
|
resolve39(here, "../../..", spec.sourceDir),
|
|
@@ -29155,4 +29488,4 @@ function resolveBareDefault() {
|
|
|
29155
29488
|
process.exit(ExitCode.Error);
|
|
29156
29489
|
}
|
|
29157
29490
|
//# sourceMappingURL=cli.js.map
|
|
29158
|
-
//# debugId=
|
|
29491
|
+
//# debugId=a68e2b18-f1ae-5a4d-8cd7-72ddbf9688ab
|