laconic-skill 0.1.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 +43 -0
- package/bin/laconic-skill.mjs +171 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
npx laconic-skill check output.txt --receipt
|
|
2
|
+
|
|
3
|
+
# laconic-skill
|
|
4
|
+
|
|
5
|
+
Install and run in one command:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx laconic-skill check output.txt --receipt
|
|
9
|
+
cat output.txt | npx laconic-skill check - --receipt
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or install globally:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g laconic-skill
|
|
16
|
+
laconic check output.txt --receipt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What It Does
|
|
20
|
+
|
|
21
|
+
`laconic-skill` checks a text answer for concise Laconic output rules and can emit a tamper-evident CLI receipt.
|
|
22
|
+
|
|
23
|
+
Checks:
|
|
24
|
+
|
|
25
|
+
- direct answer, no filler opening
|
|
26
|
+
- complete answer, no trailing ellipsis
|
|
27
|
+
- brief answer
|
|
28
|
+
- no Markdown bold
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
laconic-skill --help
|
|
34
|
+
laconic-skill check output.txt
|
|
35
|
+
laconic-skill check output.txt --receipt
|
|
36
|
+
laconic check output.txt --receipt
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`--receipt` implies JSON output with checks, metrics, answer hash, and receipt hash.
|
|
40
|
+
|
|
41
|
+
Failed checks exit `1` but still emit receipt JSON when `--receipt` is used.
|
|
42
|
+
|
|
43
|
+
Receipts are tamper-evident CLI receipts. They are not factual verification and do not prove the answer is true.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { basename } from "node:path";
|
|
6
|
+
|
|
7
|
+
const VERSION = "0.1.0";
|
|
8
|
+
const FILLER_PREFIXES = [
|
|
9
|
+
"sure",
|
|
10
|
+
"certainly",
|
|
11
|
+
"of course",
|
|
12
|
+
"absolutely",
|
|
13
|
+
"as an ai",
|
|
14
|
+
"i'd be happy to",
|
|
15
|
+
"i can help",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function printHelp() {
|
|
19
|
+
console.log(`laconic-skill ${VERSION}
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
npx laconic-skill check <file> --receipt
|
|
23
|
+
npx laconic-skill check - --receipt
|
|
24
|
+
laconic check <file> --receipt
|
|
25
|
+
|
|
26
|
+
Commands:
|
|
27
|
+
check <file> Check an answer file for laconic output quality.
|
|
28
|
+
check - Read answer text from stdin.
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--receipt Include a tamper-evident JSON receipt.
|
|
32
|
+
--json Print JSON only.
|
|
33
|
+
-h, --help Show this help.
|
|
34
|
+
-v, --version Show version.
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(text) {
|
|
39
|
+
return createHash("sha256").update(text).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stableJson(value) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
45
|
+
}
|
|
46
|
+
if (value && typeof value === "object") {
|
|
47
|
+
return `{${Object.keys(value)
|
|
48
|
+
.sort()
|
|
49
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
50
|
+
.join(",")}}`;
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function countWords(text) {
|
|
56
|
+
const matches = text.trim().match(/\S+/g);
|
|
57
|
+
return matches ? matches.length : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readingTimeSeconds(text) {
|
|
61
|
+
return Math.max(1, Math.ceil((countWords(text) / 225) * 60));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runChecks(answer) {
|
|
65
|
+
const trimmed = answer.trim();
|
|
66
|
+
const normalized = trimmed.toLowerCase();
|
|
67
|
+
const hasMarkdownBold = /\*\*[^*]+\*\*|__[^_]+__/.test(answer);
|
|
68
|
+
const direct = normalized.length > 0 && !FILLER_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
69
|
+
const complete = normalized.length > 0 && !normalized.endsWith("...");
|
|
70
|
+
const brief = countWords(answer) <= 120;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
direct,
|
|
74
|
+
complete,
|
|
75
|
+
brief,
|
|
76
|
+
no_markdown_bold: !hasMarkdownBold,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildReceipt({ file, answer, checks }) {
|
|
81
|
+
const timestamp = new Date().toISOString();
|
|
82
|
+
const canonical = {
|
|
83
|
+
tool: "laconic-skill",
|
|
84
|
+
version: VERSION,
|
|
85
|
+
file: basename(file),
|
|
86
|
+
answer_hash: sha256(answer),
|
|
87
|
+
answer_length: countWords(answer),
|
|
88
|
+
reading_time_seconds: readingTimeSeconds(answer),
|
|
89
|
+
checks,
|
|
90
|
+
timestamp,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...canonical,
|
|
95
|
+
receipt_hash: sha256(stableJson(canonical)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readStdin() {
|
|
100
|
+
const chunks = [];
|
|
101
|
+
for await (const chunk of process.stdin) {
|
|
102
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
103
|
+
}
|
|
104
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getInputTarget(args) {
|
|
108
|
+
return args.find((arg) => arg === "-" || !arg.startsWith("-"));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function checkCommand(args) {
|
|
112
|
+
const file = getInputTarget(args);
|
|
113
|
+
const includeReceipt = args.includes("--receipt");
|
|
114
|
+
const jsonOnly = args.includes("--json");
|
|
115
|
+
|
|
116
|
+
if (!file) {
|
|
117
|
+
throw new Error("Missing file. Usage: laconic-skill check <file> --receipt");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const answer = file === "-" ? await readStdin() : await readFile(file, "utf8");
|
|
121
|
+
const checks = runChecks(answer);
|
|
122
|
+
const passed = Object.values(checks).every(Boolean);
|
|
123
|
+
const result = {
|
|
124
|
+
ok: passed,
|
|
125
|
+
file,
|
|
126
|
+
checks,
|
|
127
|
+
metrics: {
|
|
128
|
+
answer_length: countWords(answer),
|
|
129
|
+
reading_time_seconds: readingTimeSeconds(answer),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (includeReceipt) {
|
|
134
|
+
result.receipt = buildReceipt({ file, answer, checks });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (jsonOnly || includeReceipt) {
|
|
138
|
+
console.log(JSON.stringify(result, null, 2));
|
|
139
|
+
} else {
|
|
140
|
+
console.log(passed ? "OK" : "FAILED");
|
|
141
|
+
for (const [name, value] of Object.entries(checks)) {
|
|
142
|
+
console.log(`${name}: ${value}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process.exitCode = passed ? 0 : 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function main() {
|
|
150
|
+
const [, , command, ...args] = process.argv;
|
|
151
|
+
|
|
152
|
+
if (!command || command === "--help" || command === "-h") {
|
|
153
|
+
printHelp();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (command === "--version" || command === "-v") {
|
|
157
|
+
console.log(VERSION);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (command === "check") {
|
|
161
|
+
await checkCommand(args);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(`Unknown command: ${command}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((error) => {
|
|
169
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "laconic-skill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A command-line checker for Laconic answers and tamper-evident receipts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test test/*.test.mjs"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"laconic-skill": "bin/laconic-skill.mjs",
|
|
11
|
+
"laconic": "bin/laconic-skill.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"laconic",
|
|
19
|
+
"receipt",
|
|
20
|
+
"cli",
|
|
21
|
+
"answer-checker"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/electricwolfemarshmallowhypertext/laco.git",
|
|
26
|
+
"directory": "packages/laconic-skill"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/electricwolfemarshmallowhypertext/laco/tree/master/packages/laconic-skill#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/electricwolfemarshmallowhypertext/laco/issues"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|