@dot-skill/cli 0.4.2 → 0.5.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 +65 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +149 -28
- package/package.json +22 -7
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `@dot-skill/cli`
|
|
2
|
+
|
|
3
|
+
CLI implementation for the [Open `.skill` Protocol](https://github.com/dot-skill/dot-skill).
|
|
4
|
+
|
|
5
|
+
**Public install:** [`skillerr`](https://www.npmjs.com/package/skillerr) · **Bin:** `skill`
|
|
6
|
+
|
|
7
|
+
Users install `skillerr` (depends on this package). This package is the internal implementation.
|
|
8
|
+
|
|
9
|
+
## Install (public)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -g skillerr
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node ≥ 20. Set `SKILL_HOST` when creating (`cursor`, `ollama`, `claude`, …).
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
### Create
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export SKILL_HOST=cursor
|
|
23
|
+
skill init --title "Demo"
|
|
24
|
+
skill journey --summary "Redacted human+AI work; secrets as refs."
|
|
25
|
+
skill propose --json '[{"title":"Tone","body":"Keep answers short.","type":"decision"}]'
|
|
26
|
+
skill status
|
|
27
|
+
skill checkpoint
|
|
28
|
+
skill compile -m "Demo" --approve --mint
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Ingest / run
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
skill inspect ./file.skill
|
|
35
|
+
skill validate ./file.skill
|
|
36
|
+
skill verify-trust ./file.skill
|
|
37
|
+
skill load ./file.skill
|
|
38
|
+
skill run ./file.skill
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Multi-skill identify
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
skill agent-guide
|
|
45
|
+
skill extract ./journey.json -o ./extraction # or: skill segment …
|
|
46
|
+
# one workspace per selected candidate → contract-check → compile
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
skill --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Related
|
|
54
|
+
|
|
55
|
+
- [`skillerr`](https://www.npmjs.com/package/skillerr) — public install
|
|
56
|
+
- [`@dot-skill/protocol`](https://www.npmjs.com/package/@dot-skill/protocol)
|
|
57
|
+
- [`@dot-skill/core`](https://www.npmjs.com/package/@dot-skill/core)
|
|
58
|
+
- [`@dot-skill/runtime`](https://www.npmjs.com/package/@dot-skill/runtime)
|
|
59
|
+
- [`@dot-skill/workspace`](https://www.npmjs.com/package/@dot-skill/workspace)
|
|
60
|
+
|
|
61
|
+
Docs: [Agent](https://github.com/dot-skill/dot-skill/blob/main/docs/AGENT.md) · [Protocol](https://github.com/dot-skill/dot-skill/blob/main/docs/PROTOCOL.md)
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* AI agents create; humans review. Continuity drafts for handoff; release for mint.
|
|
6
6
|
*
|
|
7
|
+
* npm i -g skillerr # or: npx -y skillerr --help
|
|
7
8
|
* export SKILL_HOST=cursor
|
|
8
9
|
* skill init --title "…"
|
|
9
10
|
* skill propose --json '[…]'
|
|
10
11
|
* skill journey --summary "…"
|
|
11
12
|
* skill checkpoint # continuity draft (partial OK)
|
|
12
13
|
* skill compile -m "…" --mint # release (complete or refuse)
|
|
14
|
+
* skill inspect ./file.skill # ingest: inspect before run
|
|
13
15
|
* skill load ./file.skill # resume handoff in another AI
|
|
14
16
|
*/
|
|
15
17
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -4,54 +4,84 @@
|
|
|
4
4
|
*
|
|
5
5
|
* AI agents create; humans review. Continuity drafts for handoff; release for mint.
|
|
6
6
|
*
|
|
7
|
+
* npm i -g skillerr # or: npx -y skillerr --help
|
|
7
8
|
* export SKILL_HOST=cursor
|
|
8
9
|
* skill init --title "…"
|
|
9
10
|
* skill propose --json '[…]'
|
|
10
11
|
* skill journey --summary "…"
|
|
11
12
|
* skill checkpoint # continuity draft (partial OK)
|
|
12
13
|
* skill compile -m "…" --mint # release (complete or refuse)
|
|
14
|
+
* skill inspect ./file.skill # ingest: inspect before run
|
|
13
15
|
* skill load ./file.skill # resume handoff in another AI
|
|
14
16
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
19
|
+
import { join, resolve } from "node:path";
|
|
20
|
+
import { inspectSkill, inspectTrustView, migrateLegacySkill, toSkillMdAdapter, unpackSkill, validatePackageBytes, mintSkillPackage, verifyMintTrust, compileRecipeToSkill, compileSkillSource, approveCompilation, redactSecrets, CompileRefusalError, } from "@dot-skill/core";
|
|
18
21
|
import { runSkillArchive } from "@dot-skill/runtime";
|
|
19
22
|
import { lookup, list, verify as registryVerify, publish as registryPublish } from "@dot-skill/registry";
|
|
20
|
-
import { isValidAgentHost } from "@dot-skill/protocol";
|
|
23
|
+
import { agentCreateGuide, assessSkillContract, explainContractAssessment, extractSkillCandidates, formatAgentGuide, isValidAgentHost, scaffoldSkillContract, } from "@dot-skill/protocol";
|
|
21
24
|
import { initWorkspace, requireWorkspace, proposeSection, proposeMany, stage, unstage, status, compileWorkspace, checkpoint, discardSection, loadHead, loadSkillHandoff, setJourney, requireAgentHost, } from "@dot-skill/workspace";
|
|
22
|
-
|
|
25
|
+
function loadPackageVersion() {
|
|
26
|
+
const metadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
27
|
+
if (typeof metadata.version !== "string" || metadata.version.length === 0) {
|
|
28
|
+
throw new Error("Invalid @dot-skill/cli package version metadata");
|
|
29
|
+
}
|
|
30
|
+
return metadata.version;
|
|
31
|
+
}
|
|
32
|
+
const VERSION = loadPackageVersion();
|
|
23
33
|
function usage(exitCode = 1) {
|
|
24
34
|
console.log(`skill — Open .skill Protocol CLI v${VERSION}
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
Easily create, inspect, and run portable .skill packages.
|
|
37
|
+
Agents create; humans approve releases.
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
Create:
|
|
29
40
|
skill init [--title name]
|
|
30
|
-
skill status Completeness
|
|
31
|
-
skill propose --title T --body B
|
|
41
|
+
skill status Completeness + staged sections
|
|
42
|
+
skill propose --title T --body B Requires SKILL_HOST
|
|
32
43
|
skill propose --json '[...]'
|
|
33
44
|
skill journey --summary "…" Redacted human+AI journey (no secrets)
|
|
34
45
|
skill add [id...] Stage (default: ALL)
|
|
35
46
|
skill unstage [id...] | skill review | skill discard <id>
|
|
36
|
-
skill checkpoint [-m msg] Continuity
|
|
47
|
+
skill checkpoint [-m msg] Continuity handoff (partial OK)
|
|
37
48
|
skill compile -m "msg" [--approve] [--mint] [--profile release|continuity]
|
|
38
|
-
Release
|
|
39
|
-
skill load <file.skill> Resume
|
|
40
|
-
skill mint [--host name] Seal release
|
|
49
|
+
Release refuses if incomplete
|
|
50
|
+
skill load <file.skill> Resume continuity in another AI
|
|
51
|
+
skill mint [--host name] Seal release (host required)
|
|
52
|
+
|
|
53
|
+
Multi-skill identify:
|
|
54
|
+
skill agent-guide [--json] Exact create/identify protocol steps
|
|
55
|
+
skill extract <journey.json> [-o dir] [--profile release|continuity]
|
|
56
|
+
Candidate SkillContract/source scaffolds
|
|
57
|
+
skill segment … Alias of extract
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
skill inspect
|
|
59
|
+
Ingest / run:
|
|
60
|
+
skill inspect <file.skill> [--trust] TrustView (no compile / no model body)
|
|
61
|
+
skill validate <file.skill> Structure + hash integrity
|
|
62
|
+
skill unpack <file.skill>
|
|
63
|
+
skill verify-trust <file.skill> [--profile minted] [--allow-development-issuer]
|
|
64
|
+
skill run <file.skill> [--mode execute] [--allow-untrusted]
|
|
65
|
+
Dry-run by default; execute refuses
|
|
66
|
+
unsigned/dev seals without --allow-untrusted
|
|
44
67
|
skill pack <source.json> [-o out.skill] [--approve] [--profile release]
|
|
45
|
-
skill
|
|
46
|
-
skill
|
|
68
|
+
skill contract-template 0.5 authoring contract scaffold
|
|
69
|
+
skill contract-check <contract.json> Completeness + fixes
|
|
70
|
+
skill registry list|lookup <digest> Optional local transparency log
|
|
47
71
|
|
|
48
|
-
Env
|
|
49
|
-
SKILL_HOST (required)
|
|
50
|
-
SKILL_ENDPOINT SKILL_ACTOR
|
|
51
|
-
|
|
72
|
+
Env:
|
|
73
|
+
SKILL_HOST (required to create) SKILL_PROVIDER SKILL_MODEL
|
|
74
|
+
SKILL_DEPLOYMENT SKILL_ENDPOINT SKILL_ACTOR
|
|
75
|
+
SKILL_AGENT_RUNTIME SKILL_AGENT_VERSION SKILL_SESSION_ID
|
|
76
|
+
SKILL_AGENT_INVOCATION Agent runtime marker (anti env-only spoof)
|
|
77
|
+
SKILL_INPUT_TOKENS SKILL_OUTPUT_TOKENS
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
Notes:
|
|
80
|
+
SKILL_HOST alone never yields verified_issuer trust. Public-dev HMAC is
|
|
81
|
+
development-only. human/cli/shell/manual hosts are denylisted for mint.
|
|
82
|
+
|
|
83
|
+
Install: npm i -g skillerr → skill --help
|
|
84
|
+
Docs: https://github.com/dot-skill/dot-skill
|
|
55
85
|
`);
|
|
56
86
|
process.exit(exitCode);
|
|
57
87
|
}
|
|
@@ -73,6 +103,78 @@ async function main() {
|
|
|
73
103
|
return;
|
|
74
104
|
}
|
|
75
105
|
switch (cmd) {
|
|
106
|
+
case "agent-guide": {
|
|
107
|
+
if (flag(rest, "--json")) {
|
|
108
|
+
console.log(JSON.stringify(agentCreateGuide(), null, 2));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(formatAgentGuide());
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "extract":
|
|
116
|
+
case "segment": {
|
|
117
|
+
const file = rest.find((a) => !a.startsWith("-"));
|
|
118
|
+
if (!file) {
|
|
119
|
+
console.error("Usage: skill extract <journey.json> [-o dir] [--profile release|continuity]\n" +
|
|
120
|
+
" journey.json: { summary, candidates|topics: [...] }\n" +
|
|
121
|
+
" See: skill agent-guide");
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
const profile = opt(rest, "--profile") ?? "release";
|
|
125
|
+
const outDir = opt(rest, "-o") ?? opt(rest, "--out");
|
|
126
|
+
const raw = JSON.parse(await readFile(resolve(file), "utf8"));
|
|
127
|
+
const report = extractSkillCandidates(raw, {
|
|
128
|
+
profile,
|
|
129
|
+
host: process.env.SKILL_HOST,
|
|
130
|
+
});
|
|
131
|
+
if (outDir) {
|
|
132
|
+
const root = resolve(outDir);
|
|
133
|
+
await mkdir(root, { recursive: true });
|
|
134
|
+
await writeFile(join(root, "extraction.json"), `${JSON.stringify(report, null, 2)}\n`);
|
|
135
|
+
for (const scaffold of report.scaffolds) {
|
|
136
|
+
const dir = join(root, "candidates", scaffold.workspace_slug);
|
|
137
|
+
await mkdir(dir, { recursive: true });
|
|
138
|
+
await writeFile(join(dir, "contract.json"), `${JSON.stringify(scaffold.contract_scaffold, null, 2)}\n`);
|
|
139
|
+
await writeFile(join(dir, "source.json"), `${JSON.stringify(scaffold.source_scaffold, null, 2)}\n`);
|
|
140
|
+
await writeFile(join(dir, "assessment.json"), `${JSON.stringify({
|
|
141
|
+
candidate: scaffold.candidate,
|
|
142
|
+
missing: scaffold.missing,
|
|
143
|
+
next_steps: scaffold.next_steps,
|
|
144
|
+
}, null, 2)}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
console.log(JSON.stringify({
|
|
148
|
+
ok: true,
|
|
149
|
+
written: outDir ? resolve(outDir) : null,
|
|
150
|
+
...report,
|
|
151
|
+
note: "Scaffolds are intentionally incomplete. Complete each SkillContract, one workspace per candidate, then contract-check / status before release compile.",
|
|
152
|
+
}, null, 2));
|
|
153
|
+
// Non-zero if any candidate is incomplete (expected for fresh extract).
|
|
154
|
+
process.exit(report.scaffolds.every((s) => s.candidate.assessment.complete) ? 0 : 2);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "contract-template": {
|
|
158
|
+
console.log(JSON.stringify(scaffoldSkillContract(), null, 2));
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case "contract-check": {
|
|
162
|
+
const file = rest[0];
|
|
163
|
+
if (!file)
|
|
164
|
+
usage();
|
|
165
|
+
const profile = opt(rest, "--profile") ?? "release";
|
|
166
|
+
const parsed = JSON.parse(await readFile(resolve(file), "utf8"));
|
|
167
|
+
const contract = parsed && typeof parsed === "object" && "kind" in parsed && parsed.kind === "skill_source"
|
|
168
|
+
? parsed.contract
|
|
169
|
+
: parsed;
|
|
170
|
+
const assessment = assessSkillContract(contract, profile);
|
|
171
|
+
console.log(JSON.stringify({
|
|
172
|
+
assessment,
|
|
173
|
+
explanation: explainContractAssessment(assessment),
|
|
174
|
+
}, null, 2));
|
|
175
|
+
process.exit(assessment.complete ? 0 : 2);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
76
178
|
case "init": {
|
|
77
179
|
const title = opt(rest, "--title");
|
|
78
180
|
const { root, created } = await initWorkspace(process.cwd(), { title });
|
|
@@ -216,7 +318,7 @@ async function main() {
|
|
|
216
318
|
case "compile":
|
|
217
319
|
case "bake": {
|
|
218
320
|
if (cmd === "bake") {
|
|
219
|
-
console.error("note: `bake` is a
|
|
321
|
+
console.error("note: `bake` is a legacy alias; the protocol command is `skill compile`");
|
|
220
322
|
}
|
|
221
323
|
const root = requireWorkspace();
|
|
222
324
|
const profile = opt(rest, "--profile") ?? "release";
|
|
@@ -230,6 +332,9 @@ async function main() {
|
|
|
230
332
|
mint: flag(rest, "--mint"),
|
|
231
333
|
profile,
|
|
232
334
|
host: opt(rest, "--host"),
|
|
335
|
+
agent_runtime: process.env.SKILL_AGENT_RUNTIME ?? "@dot-skill/cli",
|
|
336
|
+
agent_version: process.env.SKILL_AGENT_VERSION ??
|
|
337
|
+
(process.env.SKILL_AGENT_RUNTIME ? "unknown" : VERSION),
|
|
233
338
|
input_tokens: opt(rest, "--input-tokens")
|
|
234
339
|
? Number(opt(rest, "--input-tokens"))
|
|
235
340
|
: undefined,
|
|
@@ -298,6 +403,8 @@ async function main() {
|
|
|
298
403
|
? redactSecrets(process.env.SKILL_ENDPOINT)
|
|
299
404
|
: undefined,
|
|
300
405
|
agent_runtime: process.env.SKILL_AGENT_RUNTIME ?? "@dot-skill/cli",
|
|
406
|
+
agent_version: process.env.SKILL_AGENT_VERSION ??
|
|
407
|
+
(process.env.SKILL_AGENT_RUNTIME ? "unknown" : VERSION),
|
|
301
408
|
});
|
|
302
409
|
const out = opt(rest, "-o") ?? file;
|
|
303
410
|
await writeFile(resolve(out), packageBytes);
|
|
@@ -314,7 +421,7 @@ async function main() {
|
|
|
314
421
|
case "publish": {
|
|
315
422
|
console.error("Publish is not part of the open .skill happy path.\n" +
|
|
316
423
|
"Share the .skill file (git, chat, drive). Optional local log: skill registry publish <file>\n" +
|
|
317
|
-
"Hosted registries are product concerns
|
|
424
|
+
"Hosted registries are product concerns, not this protocol.");
|
|
318
425
|
process.exit(2);
|
|
319
426
|
break;
|
|
320
427
|
}
|
|
@@ -322,7 +429,13 @@ async function main() {
|
|
|
322
429
|
const file = rest[0];
|
|
323
430
|
if (!file)
|
|
324
431
|
usage();
|
|
325
|
-
|
|
432
|
+
const bytes = new Uint8Array(await readFile(resolve(file)));
|
|
433
|
+
if (flag(rest, "--trust")) {
|
|
434
|
+
console.log(JSON.stringify(inspectTrustView(bytes), null, 2));
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
console.log(JSON.stringify(inspectSkill(bytes), null, 2));
|
|
438
|
+
}
|
|
326
439
|
break;
|
|
327
440
|
}
|
|
328
441
|
case "validate": {
|
|
@@ -409,7 +522,12 @@ async function main() {
|
|
|
409
522
|
inputs[k] = v.join("=");
|
|
410
523
|
}
|
|
411
524
|
}
|
|
412
|
-
const run = await runSkillArchive(new Uint8Array(await readFile(resolve(file))), { host: process.env.SKILL_HOST ?? "runtime" }, {
|
|
525
|
+
const run = await runSkillArchive(new Uint8Array(await readFile(resolve(file))), { host: process.env.SKILL_HOST ?? "runtime" }, {
|
|
526
|
+
mode,
|
|
527
|
+
inputs,
|
|
528
|
+
allow_untrusted: flag(rest, "--allow-untrusted"),
|
|
529
|
+
allow_development_issuer: flag(rest, "--allow-development-issuer"),
|
|
530
|
+
});
|
|
413
531
|
console.log(JSON.stringify(run, null, 2));
|
|
414
532
|
process.exit(run.status === "succeeded" || run.status === "paused" ? 0 : 2);
|
|
415
533
|
break;
|
|
@@ -419,7 +537,10 @@ async function main() {
|
|
|
419
537
|
if (!file)
|
|
420
538
|
usage();
|
|
421
539
|
const profile = (opt(rest, "--profile") ?? "minted");
|
|
422
|
-
console.log(JSON.stringify(verifyMintTrust(new Uint8Array(await readFile(resolve(file))), profile
|
|
540
|
+
console.log(JSON.stringify(verifyMintTrust(new Uint8Array(await readFile(resolve(file))), profile, {
|
|
541
|
+
allow_development_issuer: flag(rest, "--allow-development-issuer"),
|
|
542
|
+
allow_self_reported: flag(rest, "--allow-self-reported"),
|
|
543
|
+
}), null, 2));
|
|
423
544
|
break;
|
|
424
545
|
}
|
|
425
546
|
case "registry": {
|
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dot-skill/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "CLI implementation for portable .skill packages (install skillerr for the skill bin)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bharat Dudeja",
|
|
8
8
|
"url": "https://github.com/bharatdudeja13-cmd"
|
|
9
9
|
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"skill",
|
|
12
|
+
"skillerr",
|
|
13
|
+
"dot-skill",
|
|
14
|
+
".skill",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"agent-skills",
|
|
17
|
+
"cli",
|
|
18
|
+
"protocol"
|
|
19
|
+
],
|
|
10
20
|
"type": "module",
|
|
11
21
|
"bin": {
|
|
12
22
|
"skill": "./dist/cli.js",
|
|
@@ -21,9 +31,14 @@
|
|
|
21
31
|
"url": "https://github.com/dot-skill/dot-skill.git",
|
|
22
32
|
"directory": "packages/cli"
|
|
23
33
|
},
|
|
34
|
+
"homepage": "https://github.com/dot-skill/dot-skill#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/dot-skill/dot-skill/issues"
|
|
37
|
+
},
|
|
24
38
|
"files": [
|
|
25
39
|
"dist/cli.js",
|
|
26
40
|
"dist/cli.d.ts",
|
|
41
|
+
"README.md",
|
|
27
42
|
"LICENSE"
|
|
28
43
|
],
|
|
29
44
|
"scripts": {
|
|
@@ -32,11 +47,11 @@
|
|
|
32
47
|
"test": "node --test dist/conformance.test.js"
|
|
33
48
|
},
|
|
34
49
|
"dependencies": {
|
|
35
|
-
"@dot-skill/core": "^0.
|
|
36
|
-
"@dot-skill/protocol": "^0.
|
|
37
|
-
"@dot-skill/registry": "^0.
|
|
38
|
-
"@dot-skill/runtime": "^0.
|
|
39
|
-
"@dot-skill/workspace": "^0.
|
|
50
|
+
"@dot-skill/core": "^0.5.0",
|
|
51
|
+
"@dot-skill/protocol": "^0.5.0",
|
|
52
|
+
"@dot-skill/registry": "^0.5.0",
|
|
53
|
+
"@dot-skill/runtime": "^0.5.0",
|
|
54
|
+
"@dot-skill/workspace": "^0.5.0"
|
|
40
55
|
},
|
|
41
56
|
"devDependencies": {
|
|
42
57
|
"@types/node": "^22.15.21",
|