@hardlydifficult/ai-msg 1.0.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 ADDED
@@ -0,0 +1,77 @@
1
+ # @hardlydifficult/ai-msg
2
+
3
+ Opinionated utilities for extracting structured output from AI text responses.
4
+
5
+ LLM responses often wrap structured data in markdown code blocks, preamble text, or trailing commentary. This package reliably extracts JSON and code blocks from messy AI output.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @hardlydifficult/ai-msg
11
+ ```
12
+
13
+ For typed extraction with Zod validation:
14
+
15
+ ```bash
16
+ npm install @hardlydifficult/ai-msg zod
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### `extractJson(text): unknown | null`
22
+
23
+ Extracts the first valid JSON value from text using a three-pass strategy:
24
+
25
+ 1. Direct `JSON.parse` of the trimmed text
26
+ 2. Search markdown code blocks (json-tagged first, then any)
27
+ 3. Find balanced `{}` or `[]` substrings in prose
28
+
29
+ ```typescript
30
+ import { extractJson } from "@hardlydifficult/ai-msg";
31
+
32
+ extractJson('{"key": "value"}');
33
+ // { key: "value" }
34
+
35
+ extractJson('Here is the result:\n```json\n{"key": "value"}\n```\nDone.');
36
+ // { key: "value" }
37
+
38
+ extractJson('The answer is {"key": "value"} as shown.');
39
+ // { key: "value" }
40
+
41
+ extractJson("no json here");
42
+ // null
43
+ ```
44
+
45
+ ### `extractCodeBlock(text, lang?): string[]`
46
+
47
+ Extracts all markdown code blocks from text. Optionally filters by language tag.
48
+
49
+ ```typescript
50
+ import { extractCodeBlock } from "@hardlydifficult/ai-msg";
51
+
52
+ extractCodeBlock("```ts\nconst x = 1;\n```");
53
+ // ["const x = 1;"]
54
+
55
+ extractCodeBlock("```json\n{}\n```\n```ts\nconst x = 1;\n```", "json");
56
+ // ["{}"]
57
+
58
+ extractCodeBlock("no code blocks");
59
+ // []
60
+ ```
61
+
62
+ ### `extractTyped<T>(text, schema): T | null`
63
+
64
+ Extracts JSON from text and validates it against a Zod schema. Requires `zod` as a peer dependency.
65
+
66
+ ```typescript
67
+ import { extractTyped } from "@hardlydifficult/ai-msg";
68
+ import { z } from "zod";
69
+
70
+ const Person = z.object({ name: z.string(), age: z.number() });
71
+
72
+ extractTyped('{"name": "Alice", "age": 30}', Person);
73
+ // { name: "Alice", age: 30 }
74
+
75
+ extractTyped('{"name": "Alice", "age": "thirty"}', Person);
76
+ // null
77
+ ```
@@ -0,0 +1,2 @@
1
+ export declare function extractCodeBlock(text: string, lang?: string): string[];
2
+ //# sourceMappingURL=extractCodeBlock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractCodeBlock.d.ts","sourceRoot":"","sources":["../src/extractCodeBlock.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractCodeBlock = extractCodeBlock;
4
+ const codeBlockRegex = /^```(\w*)\s*\n([\s\S]*?)^```/gm;
5
+ function extractCodeBlock(text, lang) {
6
+ const results = [];
7
+ for (const match of text.matchAll(codeBlockRegex)) {
8
+ const tag = match[1];
9
+ const content = match[2];
10
+ if (lang !== undefined && tag.toLowerCase() !== lang.toLowerCase()) {
11
+ continue;
12
+ }
13
+ results.push(content.trimEnd());
14
+ }
15
+ return results;
16
+ }
17
+ //# sourceMappingURL=extractCodeBlock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractCodeBlock.js","sourceRoot":"","sources":["../src/extractCodeBlock.ts"],"names":[],"mappings":";;AAEA,4CAaC;AAfD,MAAM,cAAc,GAAG,gCAAgC,CAAC;AAExD,SAAgB,gBAAgB,CAAC,IAAY,EAAE,IAAa;IAC1D,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QAClD,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACnE,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function extractJson(text: string): unknown;
2
+ //# sourceMappingURL=extractJson.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractJson.d.ts","sourceRoot":"","sources":["../src/extractJson.ts"],"names":[],"mappings":"AAWA,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAuCjD"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractJson = extractJson;
4
+ const extractCodeBlock_js_1 = require("./extractCodeBlock.js");
5
+ const findBalanced_js_1 = require("./findBalanced.js");
6
+ function tryParse(text) {
7
+ try {
8
+ return JSON.parse(text);
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ function extractJson(text) {
15
+ // Pass 1: try the whole text
16
+ const direct = tryParse(text.trim());
17
+ if (direct !== undefined) {
18
+ return direct;
19
+ }
20
+ // Pass 2: code blocks — json-tagged first, then any
21
+ for (const block of (0, extractCodeBlock_js_1.extractCodeBlock)(text, "json")) {
22
+ const parsed = tryParse(block.trim());
23
+ if (parsed !== undefined) {
24
+ return parsed;
25
+ }
26
+ }
27
+ for (const block of (0, extractCodeBlock_js_1.extractCodeBlock)(text)) {
28
+ const parsed = tryParse(block.trim());
29
+ if (parsed !== undefined) {
30
+ return parsed;
31
+ }
32
+ }
33
+ // Pass 3: balanced braces / brackets
34
+ const obj = (0, findBalanced_js_1.findBalanced)(text, "{", "}");
35
+ if (obj !== null) {
36
+ const parsed = tryParse(obj);
37
+ if (parsed !== undefined) {
38
+ return parsed;
39
+ }
40
+ }
41
+ const arr = (0, findBalanced_js_1.findBalanced)(text, "[", "]");
42
+ if (arr !== null) {
43
+ const parsed = tryParse(arr);
44
+ if (parsed !== undefined) {
45
+ return parsed;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ //# sourceMappingURL=extractJson.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractJson.js","sourceRoot":"","sources":["../src/extractJson.ts"],"names":[],"mappings":";;AAWA,kCAuCC;AAlDD,+DAAyD;AACzD,uDAAiD;AAEjD,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAgB,WAAW,CAAC,IAAY;IACtC,6BAA6B;IAC7B,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,oDAAoD;IACpD,KAAK,MAAM,KAAK,IAAI,IAAA,sCAAgB,EAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAA,sCAAgB,EAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,MAAM,GAAG,GAAG,IAAA,8BAAY,EAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,IAAA,8BAAY,EAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type ZodType } from "zod";
2
+ export declare function extractTyped<T>(text: string, schema: ZodType<T>): T | null;
3
+ //# sourceMappingURL=extractTyped.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractTyped.d.ts","sourceRoot":"","sources":["../src/extractTyped.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AAInC,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAQ1E"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractTyped = extractTyped;
4
+ const extractJson_js_1 = require("./extractJson.js");
5
+ function extractTyped(text, schema) {
6
+ const json = (0, extractJson_js_1.extractJson)(text);
7
+ if (json === null) {
8
+ return null;
9
+ }
10
+ const result = schema.safeParse(json);
11
+ return result.success ? result.data : null;
12
+ }
13
+ //# sourceMappingURL=extractTyped.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractTyped.js","sourceRoot":"","sources":["../src/extractTyped.ts"],"names":[],"mappings":";;AAIA,oCAQC;AAVD,qDAA+C;AAE/C,SAAgB,YAAY,CAAI,IAAY,EAAE,MAAkB;IAC9D,MAAM,IAAI,GAAG,IAAA,4BAAW,EAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function findBalanced(text: string, openChar: string, closeChar: string): string | null;
2
+ //# sourceMappingURL=findBalanced.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findBalanced.d.ts","sourceRoot":"","sources":["../src/findBalanced.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CA6Cf"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findBalanced = findBalanced;
4
+ function findBalanced(text, openChar, closeChar) {
5
+ const start = text.indexOf(openChar);
6
+ if (start === -1) {
7
+ return null;
8
+ }
9
+ let depth = 0;
10
+ let inString = false;
11
+ let escaped = false;
12
+ for (let i = start; i < text.length; i++) {
13
+ const ch = text[i];
14
+ if (escaped) {
15
+ escaped = false;
16
+ continue;
17
+ }
18
+ if (ch === "\\") {
19
+ if (inString) {
20
+ escaped = true;
21
+ }
22
+ continue;
23
+ }
24
+ if (ch === '"') {
25
+ inString = !inString;
26
+ continue;
27
+ }
28
+ if (inString) {
29
+ continue;
30
+ }
31
+ if (ch === openChar) {
32
+ depth++;
33
+ }
34
+ else if (ch === closeChar) {
35
+ depth--;
36
+ if (depth === 0) {
37
+ return text.slice(start, i + 1);
38
+ }
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ //# sourceMappingURL=findBalanced.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findBalanced.js","sourceRoot":"","sources":["../src/findBalanced.ts"],"names":[],"mappings":";;AAAA,oCAiDC;AAjDD,SAAgB,YAAY,CAC1B,IAAY,EACZ,QAAgB,EAChB,SAAiB;IAEjB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAEnB,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,GAAG,KAAK,CAAC;YAChB,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,GAAG,CAAC,QAAQ,CAAC;YACrB,SAAS;QACX,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;YACpB,KAAK,EAAE,CAAC;QACV,CAAC;aAAM,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YAC5B,KAAK,EAAE,CAAC;YACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBAChB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { extractCodeBlock } from "./extractCodeBlock.js";
2
+ export { extractJson } from "./extractJson.js";
3
+ export { extractTyped } from "./extractTyped.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractTyped = exports.extractJson = exports.extractCodeBlock = void 0;
4
+ var extractCodeBlock_js_1 = require("./extractCodeBlock.js");
5
+ Object.defineProperty(exports, "extractCodeBlock", { enumerable: true, get: function () { return extractCodeBlock_js_1.extractCodeBlock; } });
6
+ var extractJson_js_1 = require("./extractJson.js");
7
+ Object.defineProperty(exports, "extractJson", { enumerable: true, get: function () { return extractJson_js_1.extractJson; } });
8
+ var extractTyped_js_1 = require("./extractTyped.js");
9
+ Object.defineProperty(exports, "extractTyped", { enumerable: true, get: function () { return extractTyped_js_1.extractTyped; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6DAAyD;AAAhD,uHAAA,gBAAgB,OAAA;AACzB,mDAA+C;AAAtC,6GAAA,WAAW,OAAA;AACpB,qDAAiD;AAAxC,+GAAA,YAAY,OAAA"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@hardlydifficult/ai-msg",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
14
+ "lint": "tsc --noEmit",
15
+ "clean": "rm -rf dist"
16
+ },
17
+ "peerDependencies": {
18
+ "zod": ">=3.0.0"
19
+ },
20
+ "peerDependenciesMeta": {
21
+ "zod": {
22
+ "optional": true
23
+ }
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "5.8.3",
27
+ "vitest": "1.6.1",
28
+ "zod": "3.24.4"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ }
33
+ }