laconic-skill 0.1.0 → 0.1.1

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/MCP.md ADDED
@@ -0,0 +1,47 @@
1
+ # laconic-skill MCP
2
+
3
+ Use `laconic-skill` from MCP-compatible agents.
4
+
5
+ ## Claude Desktop
6
+
7
+ ```json
8
+ {
9
+ "mcpServers": {
10
+ "laconic-skill": {
11
+ "command": "npx",
12
+ "args": ["-y", "laconic-skill", "laconic-skill-mcp"]
13
+ }
14
+ }
15
+ }
16
+ ```
17
+
18
+ ## Tools
19
+
20
+ ### laconic_check
21
+
22
+ Input:
23
+
24
+ ```json
25
+ {
26
+ "text": "Short direct answer.",
27
+ "receipt": true
28
+ }
29
+ ```
30
+
31
+ Output:
32
+
33
+ - check result
34
+ - metrics
35
+ - optional tamper-evident CLI receipt
36
+
37
+ ## Not Shipped In MCP v0
38
+
39
+ `laconic_rewrite`, `laconic_pipeline`, and `laconic_memory_search` are intentionally not exposed yet. This npm package currently ships real check/receipt behavior only.
40
+
41
+ ## Smoke Test
42
+
43
+ ```bash
44
+ npx laconic-skill-mcp smoke
45
+ ```
46
+
47
+ The smoke test starts the MCP server over stdio, lists tools, calls `laconic_check`, and prints the returned check result JSON.
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from "node:url";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ import { VERSION, checkText } from "../lib/core.mjs";
10
+
11
+ function createServer() {
12
+ const server = new McpServer({
13
+ name: "laconic-skill",
14
+ version: VERSION,
15
+ });
16
+
17
+ server.registerTool(
18
+ "laconic_check",
19
+ {
20
+ title: "Laconic Check",
21
+ description: "Check answer text for Laconic output rules and optionally return a tamper-evident receipt.",
22
+ inputSchema: {
23
+ text: z.string().describe("Answer text to check."),
24
+ receipt: z.boolean().optional().describe("Include a tamper-evident CLI receipt."),
25
+ },
26
+ },
27
+ async ({ text, receipt = false }) => {
28
+ const result = checkText({ text, file: "mcp-input", receipt });
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: JSON.stringify(result, null, 2),
34
+ },
35
+ ],
36
+ structuredContent: result,
37
+ };
38
+ },
39
+ );
40
+
41
+ return server;
42
+ }
43
+
44
+ async function runServer() {
45
+ const server = createServer();
46
+ const transport = new StdioServerTransport();
47
+ await server.connect(transport);
48
+ }
49
+
50
+ async function runSmoke() {
51
+ const transport = new StdioClientTransport({
52
+ command: process.execPath,
53
+ args: [fileURLToPath(import.meta.url)],
54
+ stderr: "pipe",
55
+ });
56
+ const client = new Client({
57
+ name: "laconic-skill-smoke",
58
+ version: VERSION,
59
+ });
60
+
61
+ await client.connect(transport);
62
+ try {
63
+ const tools = await client.listTools();
64
+ const checkResult = await client.callTool({
65
+ name: "laconic_check",
66
+ arguments: {
67
+ text: "Short direct answer.",
68
+ receipt: true,
69
+ },
70
+ });
71
+
72
+ console.log(
73
+ JSON.stringify(
74
+ {
75
+ ok: true,
76
+ tools: tools.tools.map((tool) => tool.name),
77
+ laconic_check: JSON.parse(checkResult.content[0].text),
78
+ },
79
+ null,
80
+ 2,
81
+ ),
82
+ );
83
+ } finally {
84
+ await client.close();
85
+ }
86
+ }
87
+
88
+ const command = process.argv[2];
89
+ if (command === "smoke") {
90
+ runSmoke().catch((error) => {
91
+ console.error(error instanceof Error ? error.message : String(error));
92
+ process.exitCode = 1;
93
+ });
94
+ } else {
95
+ runServer().catch((error) => {
96
+ console.error(error instanceof Error ? error.message : String(error));
97
+ process.exitCode = 1;
98
+ });
99
+ }
@@ -1,19 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createHash } from "node:crypto";
4
3
  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
- ];
4
+ import { VERSION, checkText } from "../lib/core.mjs";
17
5
 
18
6
  function printHelp() {
19
7
  console.log(`laconic-skill ${VERSION}
@@ -35,67 +23,6 @@ Options:
35
23
  `);
36
24
  }
37
25
 
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
26
  async function readStdin() {
100
27
  const chunks = [];
101
28
  for await (const chunk of process.stdin) {
@@ -118,32 +45,18 @@ async function checkCommand(args) {
118
45
  }
119
46
 
120
47
  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
- }
48
+ const result = checkText({ text: answer, file, receipt: includeReceipt });
136
49
 
137
50
  if (jsonOnly || includeReceipt) {
138
51
  console.log(JSON.stringify(result, null, 2));
139
52
  } else {
140
- console.log(passed ? "OK" : "FAILED");
141
- for (const [name, value] of Object.entries(checks)) {
53
+ console.log(result.ok ? "OK" : "FAILED");
54
+ for (const [name, value] of Object.entries(result.checks)) {
142
55
  console.log(`${name}: ${value}`);
143
56
  }
144
57
  }
145
58
 
146
- process.exitCode = passed ? 0 : 1;
59
+ process.exitCode = result.ok ? 0 : 1;
147
60
  }
148
61
 
149
62
  async function main() {
package/lib/core.mjs ADDED
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import { basename } from "node:path";
3
+
4
+ export const VERSION = "0.1.1";
5
+
6
+ const FILLER_PREFIXES = [
7
+ "sure",
8
+ "certainly",
9
+ "of course",
10
+ "absolutely",
11
+ "as an ai",
12
+ "i'd be happy to",
13
+ "i can help",
14
+ ];
15
+
16
+ export function sha256(text) {
17
+ return createHash("sha256").update(text).digest("hex");
18
+ }
19
+
20
+ export function stableJson(value) {
21
+ if (Array.isArray(value)) {
22
+ return `[${value.map(stableJson).join(",")}]`;
23
+ }
24
+ if (value && typeof value === "object") {
25
+ return `{${Object.keys(value)
26
+ .sort()
27
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
28
+ .join(",")}}`;
29
+ }
30
+ return JSON.stringify(value);
31
+ }
32
+
33
+ export function countWords(text) {
34
+ const matches = text.trim().match(/\S+/g);
35
+ return matches ? matches.length : 0;
36
+ }
37
+
38
+ export function readingTimeSeconds(text) {
39
+ return Math.max(1, Math.ceil((countWords(text) / 225) * 60));
40
+ }
41
+
42
+ export function runChecks(answer) {
43
+ const trimmed = answer.trim();
44
+ const normalized = trimmed.toLowerCase();
45
+ const hasMarkdownBold = /\*\*[^*]+\*\*|__[^_]+__/.test(answer);
46
+ const direct = normalized.length > 0 && !FILLER_PREFIXES.some((prefix) => normalized.startsWith(prefix));
47
+ const complete = normalized.length > 0 && !normalized.endsWith("...");
48
+ const brief = countWords(answer) <= 120;
49
+
50
+ return {
51
+ direct,
52
+ complete,
53
+ brief,
54
+ no_markdown_bold: !hasMarkdownBold,
55
+ };
56
+ }
57
+
58
+ export function buildReceipt({ file, answer, checks }) {
59
+ const timestamp = new Date().toISOString();
60
+ const canonical = {
61
+ tool: "laconic-skill",
62
+ version: VERSION,
63
+ file: file === "-" ? "-" : basename(file),
64
+ answer_hash: sha256(answer),
65
+ answer_length: countWords(answer),
66
+ reading_time_seconds: readingTimeSeconds(answer),
67
+ checks,
68
+ timestamp,
69
+ };
70
+
71
+ return {
72
+ ...canonical,
73
+ receipt_hash: sha256(stableJson(canonical)),
74
+ };
75
+ }
76
+
77
+ export function checkText({ text, file = "text", receipt = false }) {
78
+ const checks = runChecks(text);
79
+ const passed = Object.values(checks).every(Boolean);
80
+ const result = {
81
+ ok: passed,
82
+ file,
83
+ checks,
84
+ metrics: {
85
+ answer_length: countWords(text),
86
+ reading_time_seconds: readingTimeSeconds(text),
87
+ },
88
+ };
89
+
90
+ if (receipt) {
91
+ result.receipt = buildReceipt({ file, answer: text, checks });
92
+ }
93
+
94
+ return result;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laconic-skill",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A command-line checker for Laconic answers and tamper-evident receipts.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,10 +8,13 @@
8
8
  },
9
9
  "bin": {
10
10
  "laconic-skill": "bin/laconic-skill.mjs",
11
- "laconic": "bin/laconic-skill.mjs"
11
+ "laconic": "bin/laconic-skill.mjs",
12
+ "laconic-skill-mcp": "bin/laconic-skill-mcp.mjs"
12
13
  },
13
14
  "files": [
14
15
  "bin",
16
+ "lib",
17
+ "MCP.md",
15
18
  "README.md"
16
19
  ],
17
20
  "keywords": [
@@ -32,5 +35,8 @@
32
35
  "license": "MIT",
33
36
  "engines": {
34
37
  "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0"
35
41
  }
36
42
  }