@crewhaus/tool-text 0.7.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 +62 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +575 -0
- package/dist/lib/diff.d.ts +82 -0
- package/dist/lib/diff.js +541 -0
- package/dist/lib/entities.d.ts +15 -0
- package/dist/lib/entities.js +56 -0
- package/dist/lib/format.d.ts +33 -0
- package/dist/lib/format.js +107 -0
- package/dist/lib/locate.d.ts +24 -0
- package/dist/lib/locate.js +43 -0
- package/dist/lib/log.d.ts +34 -0
- package/dist/lib/log.js +63 -0
- package/dist/lib/markdown.d.ts +34 -0
- package/dist/lib/markdown.js +120 -0
- package/dist/lib/normalize.d.ts +39 -0
- package/dist/lib/normalize.js +122 -0
- package/dist/lib/regex.d.ts +20 -0
- package/dist/lib/regex.js +36 -0
- package/dist/lib/similarity.d.ts +33 -0
- package/dist/lib/similarity.js +216 -0
- package/dist/lib/template.d.ts +42 -0
- package/dist/lib/template.js +88 -0
- package/package.json +45 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { lineStarts, offsetToLineCol } from "./locate";
|
|
2
|
+
/**
|
|
3
|
+
* Run a regex over text and return located matches.
|
|
4
|
+
*
|
|
5
|
+
* Always compiles with `g` so the scan advances, and a zero-width match bumps
|
|
6
|
+
* `lastIndex` by one so a pattern like `a*` terminates instead of spinning
|
|
7
|
+
* forever at the same offset.
|
|
8
|
+
*/
|
|
9
|
+
export function regexExtractAll(text, pattern, flags, maxMatches) {
|
|
10
|
+
const withGlobal = flags.includes("g") ? flags : `${flags}g`;
|
|
11
|
+
const re = new RegExp(pattern, withGlobal);
|
|
12
|
+
const starts = lineStarts(text);
|
|
13
|
+
const matches = [];
|
|
14
|
+
let truncated = false;
|
|
15
|
+
for (;;) {
|
|
16
|
+
const m = re.exec(text);
|
|
17
|
+
if (m === null)
|
|
18
|
+
break;
|
|
19
|
+
if (matches.length >= maxMatches) {
|
|
20
|
+
truncated = true;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
const { line, column } = offsetToLineCol(starts, m.index);
|
|
24
|
+
matches.push({
|
|
25
|
+
match: m[0],
|
|
26
|
+
index: m.index,
|
|
27
|
+
line,
|
|
28
|
+
column,
|
|
29
|
+
groups: { ...(m.groups ?? {}) },
|
|
30
|
+
captures: m.slice(1),
|
|
31
|
+
});
|
|
32
|
+
if (m[0].length === 0)
|
|
33
|
+
re.lastIndex += 1;
|
|
34
|
+
}
|
|
35
|
+
return { matches, truncated };
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Classic Levenshtein edit distance, two-row variant. */
|
|
2
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
3
|
+
/** Edit distance expressed as a 0..1 similarity. */
|
|
4
|
+
export declare function levenshteinRatio(a: string, b: string): number;
|
|
5
|
+
/** Jaro-Winkler, which rewards a shared prefix — good for names and ids. */
|
|
6
|
+
export declare function jaroWinkler(a: string, b: string): number;
|
|
7
|
+
/** Character n-grams, the basis for trigram similarity. */
|
|
8
|
+
export declare function nGrams(text: string, n: number): Set<string>;
|
|
9
|
+
export declare function jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number;
|
|
10
|
+
export type SimilarityMethod = "levenshtein" | "jaro" | "trigram" | "tokenJaccard";
|
|
11
|
+
export declare function similarity(a: string, b: string, method: SimilarityMethod): number;
|
|
12
|
+
export type FuzzyHit = {
|
|
13
|
+
readonly candidate: string;
|
|
14
|
+
readonly score: number;
|
|
15
|
+
readonly index: number;
|
|
16
|
+
};
|
|
17
|
+
/** Rank candidates against a query, best first, ties broken by input order. */
|
|
18
|
+
export declare function fuzzyRank(query: string, candidates: ReadonlyArray<string>, method: SimilarityMethod, minScore: number, limit: number): FuzzyHit[];
|
|
19
|
+
/** Very common words carry no signal; excluding them is what makes a keyword
|
|
20
|
+
* list readable rather than a list of "the, and, of". */
|
|
21
|
+
export declare const STOP_WORDS: ReadonlySet<string>;
|
|
22
|
+
export type Keyword = {
|
|
23
|
+
readonly term: string;
|
|
24
|
+
readonly count: number;
|
|
25
|
+
readonly score: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Rank terms by frequency with a length bonus, so multi-character terms beat
|
|
29
|
+
* short noise. Single-document term frequency only: there is no corpus here
|
|
30
|
+
* to compute a real inverse document frequency against, and inventing one
|
|
31
|
+
* would make the score depend on hidden state.
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractKeywords(text: string, limit: number, minLength: number): Keyword[];
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { tokenize } from "./locate";
|
|
2
|
+
/** Classic Levenshtein edit distance, two-row variant. */
|
|
3
|
+
export function levenshtein(a, b) {
|
|
4
|
+
if (a === b)
|
|
5
|
+
return 0;
|
|
6
|
+
if (a.length === 0)
|
|
7
|
+
return b.length;
|
|
8
|
+
if (b.length === 0)
|
|
9
|
+
return a.length;
|
|
10
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
11
|
+
for (let i = 1; i <= a.length; i++) {
|
|
12
|
+
const cur = new Array(b.length + 1);
|
|
13
|
+
cur[0] = i;
|
|
14
|
+
for (let j = 1; j <= b.length; j++) {
|
|
15
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
16
|
+
cur[j] = Math.min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
17
|
+
}
|
|
18
|
+
prev = cur;
|
|
19
|
+
}
|
|
20
|
+
return prev[b.length];
|
|
21
|
+
}
|
|
22
|
+
/** Edit distance expressed as a 0..1 similarity. */
|
|
23
|
+
export function levenshteinRatio(a, b) {
|
|
24
|
+
const longest = Math.max(a.length, b.length);
|
|
25
|
+
if (longest === 0)
|
|
26
|
+
return 1;
|
|
27
|
+
return 1 - levenshtein(a, b) / longest;
|
|
28
|
+
}
|
|
29
|
+
/** Jaro-Winkler, which rewards a shared prefix — good for names and ids. */
|
|
30
|
+
export function jaroWinkler(a, b) {
|
|
31
|
+
if (a === b)
|
|
32
|
+
return 1;
|
|
33
|
+
if (a.length === 0 || b.length === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
const window = Math.max(0, Math.floor(Math.max(a.length, b.length) / 2) - 1);
|
|
36
|
+
const aFlags = new Array(a.length).fill(false);
|
|
37
|
+
const bFlags = new Array(b.length).fill(false);
|
|
38
|
+
let matches = 0;
|
|
39
|
+
for (let i = 0; i < a.length; i++) {
|
|
40
|
+
const lo = Math.max(0, i - window);
|
|
41
|
+
const hi = Math.min(b.length - 1, i + window);
|
|
42
|
+
for (let j = lo; j <= hi; j++) {
|
|
43
|
+
if (bFlags[j] === true || a[i] !== b[j])
|
|
44
|
+
continue;
|
|
45
|
+
aFlags[i] = true;
|
|
46
|
+
bFlags[j] = true;
|
|
47
|
+
matches++;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (matches === 0)
|
|
52
|
+
return 0;
|
|
53
|
+
let transpositions = 0;
|
|
54
|
+
let k = 0;
|
|
55
|
+
for (let i = 0; i < a.length; i++) {
|
|
56
|
+
if (aFlags[i] !== true)
|
|
57
|
+
continue;
|
|
58
|
+
while (bFlags[k] !== true)
|
|
59
|
+
k++;
|
|
60
|
+
if (a[i] !== b[k])
|
|
61
|
+
transpositions++;
|
|
62
|
+
k++;
|
|
63
|
+
}
|
|
64
|
+
const t = transpositions / 2;
|
|
65
|
+
const jaro = (matches / a.length + matches / b.length + (matches - t) / matches) / 3;
|
|
66
|
+
let prefix = 0;
|
|
67
|
+
while (prefix < 4 && prefix < a.length && prefix < b.length && a[prefix] === b[prefix])
|
|
68
|
+
prefix++;
|
|
69
|
+
return jaro + prefix * 0.1 * (1 - jaro);
|
|
70
|
+
}
|
|
71
|
+
/** Character n-grams, the basis for trigram similarity. */
|
|
72
|
+
export function nGrams(text, n) {
|
|
73
|
+
const padded = ` ${text.toLowerCase()} `;
|
|
74
|
+
const out = new Set();
|
|
75
|
+
for (let i = 0; i + n <= padded.length; i++)
|
|
76
|
+
out.add(padded.slice(i, i + n));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
export function jaccard(a, b) {
|
|
80
|
+
if (a.size === 0 && b.size === 0)
|
|
81
|
+
return 1;
|
|
82
|
+
let intersection = 0;
|
|
83
|
+
for (const item of a)
|
|
84
|
+
if (b.has(item))
|
|
85
|
+
intersection++;
|
|
86
|
+
const union = a.size + b.size - intersection;
|
|
87
|
+
return union === 0 ? 0 : intersection / union;
|
|
88
|
+
}
|
|
89
|
+
export function similarity(a, b, method) {
|
|
90
|
+
if (method === "levenshtein")
|
|
91
|
+
return levenshteinRatio(a, b);
|
|
92
|
+
if (method === "jaro")
|
|
93
|
+
return jaroWinkler(a, b);
|
|
94
|
+
if (method === "trigram")
|
|
95
|
+
return jaccard(nGrams(a, 3), nGrams(b, 3));
|
|
96
|
+
return jaccard(new Set(tokenize(a)), new Set(tokenize(b)));
|
|
97
|
+
}
|
|
98
|
+
/** Rank candidates against a query, best first, ties broken by input order. */
|
|
99
|
+
export function fuzzyRank(query, candidates, method, minScore, limit) {
|
|
100
|
+
return candidates
|
|
101
|
+
.map((candidate, index) => ({ candidate, index, score: similarity(query, candidate, method) }))
|
|
102
|
+
.filter((h) => h.score >= minScore)
|
|
103
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
104
|
+
.slice(0, limit);
|
|
105
|
+
}
|
|
106
|
+
/** Very common words carry no signal; excluding them is what makes a keyword
|
|
107
|
+
* list readable rather than a list of "the, and, of". */
|
|
108
|
+
export const STOP_WORDS = new Set([
|
|
109
|
+
"a",
|
|
110
|
+
"an",
|
|
111
|
+
"and",
|
|
112
|
+
"are",
|
|
113
|
+
"as",
|
|
114
|
+
"at",
|
|
115
|
+
"be",
|
|
116
|
+
"but",
|
|
117
|
+
"by",
|
|
118
|
+
"for",
|
|
119
|
+
"from",
|
|
120
|
+
"has",
|
|
121
|
+
"have",
|
|
122
|
+
"he",
|
|
123
|
+
"her",
|
|
124
|
+
"his",
|
|
125
|
+
"if",
|
|
126
|
+
"in",
|
|
127
|
+
"into",
|
|
128
|
+
"is",
|
|
129
|
+
"it",
|
|
130
|
+
"its",
|
|
131
|
+
"of",
|
|
132
|
+
"on",
|
|
133
|
+
"or",
|
|
134
|
+
"she",
|
|
135
|
+
"that",
|
|
136
|
+
"the",
|
|
137
|
+
"their",
|
|
138
|
+
"them",
|
|
139
|
+
"then",
|
|
140
|
+
"there",
|
|
141
|
+
"these",
|
|
142
|
+
"they",
|
|
143
|
+
"this",
|
|
144
|
+
"to",
|
|
145
|
+
"was",
|
|
146
|
+
"were",
|
|
147
|
+
"which",
|
|
148
|
+
"who",
|
|
149
|
+
"will",
|
|
150
|
+
"with",
|
|
151
|
+
"you",
|
|
152
|
+
"your",
|
|
153
|
+
"we",
|
|
154
|
+
"our",
|
|
155
|
+
"i",
|
|
156
|
+
"not",
|
|
157
|
+
"no",
|
|
158
|
+
"can",
|
|
159
|
+
"could",
|
|
160
|
+
"should",
|
|
161
|
+
"would",
|
|
162
|
+
"do",
|
|
163
|
+
"does",
|
|
164
|
+
"did",
|
|
165
|
+
"been",
|
|
166
|
+
"being",
|
|
167
|
+
"had",
|
|
168
|
+
"how",
|
|
169
|
+
"what",
|
|
170
|
+
"when",
|
|
171
|
+
"where",
|
|
172
|
+
"why",
|
|
173
|
+
"all",
|
|
174
|
+
"any",
|
|
175
|
+
"each",
|
|
176
|
+
"more",
|
|
177
|
+
"most",
|
|
178
|
+
"other",
|
|
179
|
+
"some",
|
|
180
|
+
"such",
|
|
181
|
+
"than",
|
|
182
|
+
"too",
|
|
183
|
+
"very",
|
|
184
|
+
"just",
|
|
185
|
+
"also",
|
|
186
|
+
"may",
|
|
187
|
+
"might",
|
|
188
|
+
"must",
|
|
189
|
+
"one",
|
|
190
|
+
"two",
|
|
191
|
+
"get",
|
|
192
|
+
"got",
|
|
193
|
+
"make",
|
|
194
|
+
"made",
|
|
195
|
+
"use",
|
|
196
|
+
]);
|
|
197
|
+
/**
|
|
198
|
+
* Rank terms by frequency with a length bonus, so multi-character terms beat
|
|
199
|
+
* short noise. Single-document term frequency only: there is no corpus here
|
|
200
|
+
* to compute a real inverse document frequency against, and inventing one
|
|
201
|
+
* would make the score depend on hidden state.
|
|
202
|
+
*/
|
|
203
|
+
export function extractKeywords(text, limit, minLength) {
|
|
204
|
+
const counts = new Map();
|
|
205
|
+
for (const token of tokenize(text)) {
|
|
206
|
+
if (token.length < minLength)
|
|
207
|
+
continue;
|
|
208
|
+
if (STOP_WORDS.has(token))
|
|
209
|
+
continue;
|
|
210
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
return [...counts.entries()]
|
|
213
|
+
.map(([term, count]) => ({ term, count, score: count * Math.log2(term.length + 1) }))
|
|
214
|
+
.sort((a, b) => b.score - a.score || a.term.localeCompare(b.term))
|
|
215
|
+
.slice(0, limit);
|
|
216
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare class TemplateError extends Error {
|
|
2
|
+
readonly name = "TemplateError";
|
|
3
|
+
}
|
|
4
|
+
/** Walk a dotted path (`user.name`, `items.0.id`) through plain data. */
|
|
5
|
+
export declare function lookupPath(data: unknown, path: string): unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Render `{{path}}` placeholders against `data`.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately not a general template language: no loops, no conditionals,
|
|
10
|
+
* no partials, no arbitrary expressions. A harness needing those should
|
|
11
|
+
* build the string it wants and pass it in. What this guarantees is the part
|
|
12
|
+
* that matters — the same data and template always produce the same bytes,
|
|
13
|
+
* and under `strict` a typo in a placeholder is an error rather than an
|
|
14
|
+
* empty string silently shipped to a customer.
|
|
15
|
+
*
|
|
16
|
+
* `{{{path}}}` is accepted as a synonym for `{{path}}`; this renders text,
|
|
17
|
+
* not HTML, so there is no escaping distinction to draw.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderTemplateString(template: string, data: unknown, strict: boolean): {
|
|
20
|
+
text: string;
|
|
21
|
+
missing: string[];
|
|
22
|
+
};
|
|
23
|
+
export type ClassifyRule = {
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly patterns: ReadonlyArray<string>;
|
|
26
|
+
readonly weight?: number;
|
|
27
|
+
readonly regex?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type ClassifyResult = {
|
|
30
|
+
readonly label: string | null;
|
|
31
|
+
readonly score: number;
|
|
32
|
+
readonly matched: ReadonlyArray<string>;
|
|
33
|
+
readonly scores: Readonly<Record<string, number>>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Score text against operator-written rules and return the best label.
|
|
37
|
+
*
|
|
38
|
+
* Lexical only — phrase and regex matching with per-rule weights. No model
|
|
39
|
+
* and no embedding, so the same message always gets the same label, which is
|
|
40
|
+
* what makes it safe to route on.
|
|
41
|
+
*/
|
|
42
|
+
export declare function classifyByRules(text: string, rules: ReadonlyArray<ClassifyRule>, threshold: number, defaultLabel: string | null): ClassifyResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export class TemplateError extends Error {
|
|
2
|
+
name = "TemplateError";
|
|
3
|
+
}
|
|
4
|
+
/** Walk a dotted path (`user.name`, `items.0.id`) through plain data. */
|
|
5
|
+
export function lookupPath(data, path) {
|
|
6
|
+
if (path === ".")
|
|
7
|
+
return data;
|
|
8
|
+
let cur = data;
|
|
9
|
+
for (const part of path.split(".")) {
|
|
10
|
+
if (cur === null || cur === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (Array.isArray(cur)) {
|
|
13
|
+
const i = Number(part);
|
|
14
|
+
if (!Number.isInteger(i))
|
|
15
|
+
return undefined;
|
|
16
|
+
cur = cur[i];
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (typeof cur !== "object")
|
|
20
|
+
return undefined;
|
|
21
|
+
cur = cur[part];
|
|
22
|
+
}
|
|
23
|
+
return cur;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Render `{{path}}` placeholders against `data`.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately not a general template language: no loops, no conditionals,
|
|
29
|
+
* no partials, no arbitrary expressions. A harness needing those should
|
|
30
|
+
* build the string it wants and pass it in. What this guarantees is the part
|
|
31
|
+
* that matters — the same data and template always produce the same bytes,
|
|
32
|
+
* and under `strict` a typo in a placeholder is an error rather than an
|
|
33
|
+
* empty string silently shipped to a customer.
|
|
34
|
+
*
|
|
35
|
+
* `{{{path}}}` is accepted as a synonym for `{{path}}`; this renders text,
|
|
36
|
+
* not HTML, so there is no escaping distinction to draw.
|
|
37
|
+
*/
|
|
38
|
+
export function renderTemplateString(template, data, strict) {
|
|
39
|
+
const missing = [];
|
|
40
|
+
const text = template.replace(/\{\{\{?\s*([\w.]+)\s*\}?\}\}/g, (_full, path) => {
|
|
41
|
+
const value = lookupPath(data, path);
|
|
42
|
+
if (value === undefined || value === null) {
|
|
43
|
+
missing.push(path);
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "object")
|
|
47
|
+
return JSON.stringify(value);
|
|
48
|
+
return String(value);
|
|
49
|
+
});
|
|
50
|
+
const unique = [...new Set(missing)];
|
|
51
|
+
if (strict && unique.length > 0) {
|
|
52
|
+
throw new TemplateError(`template references ${unique.map((p) => `"${p}"`).join(", ")}, which the data does not provide`);
|
|
53
|
+
}
|
|
54
|
+
return { text, missing: unique };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Score text against operator-written rules and return the best label.
|
|
58
|
+
*
|
|
59
|
+
* Lexical only — phrase and regex matching with per-rule weights. No model
|
|
60
|
+
* and no embedding, so the same message always gets the same label, which is
|
|
61
|
+
* what makes it safe to route on.
|
|
62
|
+
*/
|
|
63
|
+
export function classifyByRules(text, rules, threshold, defaultLabel) {
|
|
64
|
+
const haystack = text.toLowerCase();
|
|
65
|
+
const scores = {};
|
|
66
|
+
const matched = [];
|
|
67
|
+
for (const rule of rules) {
|
|
68
|
+
const weight = rule.weight ?? 1;
|
|
69
|
+
let hits = 0;
|
|
70
|
+
for (const pattern of rule.patterns) {
|
|
71
|
+
const found = rule.regex === true
|
|
72
|
+
? new RegExp(pattern, "i").test(text)
|
|
73
|
+
: haystack.includes(pattern.toLowerCase());
|
|
74
|
+
if (found) {
|
|
75
|
+
hits += 1;
|
|
76
|
+
matched.push(pattern);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (hits > 0)
|
|
80
|
+
scores[rule.label] = (scores[rule.label] ?? 0) + hits * weight;
|
|
81
|
+
}
|
|
82
|
+
const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
83
|
+
const top = ranked[0];
|
|
84
|
+
if (top === undefined || top[1] < threshold) {
|
|
85
|
+
return { label: defaultLabel, score: top?.[1] ?? 0, matched, scores };
|
|
86
|
+
}
|
|
87
|
+
return { label: top[0], score: top[1], matched, scores };
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-text",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Deterministic text tools: regex extraction, diffing, unified-diff parsing, budget truncation, log compaction, outlining, templating, rule classification, normalization, similarity, and escaping",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/errors": "0.7.0",
|
|
19
|
+
"@crewhaus/tool-builder": "0.7.0",
|
|
20
|
+
"@crewhaus/tool-catalog": "0.7.0",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@crewhaus/tool-executor": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Max Meier",
|
|
29
|
+
"email": "max@crewhaus.ai",
|
|
30
|
+
"url": "https://crewhaus.ai"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
35
|
+
"directory": "packages/tool-text"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-text#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
45
|
+
}
|