tryharness 0.7.22 → 0.7.25
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 +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- package/lib/embedder.mjs +0 -156
package/lib/parsers.mjs
CHANGED
|
@@ -23,10 +23,14 @@ function parseLines(text) {
|
|
|
23
23
|
function compact(value, cap) {
|
|
24
24
|
if (value === undefined || value === null) return "";
|
|
25
25
|
try {
|
|
26
|
-
const text = (typeof value === "string" ? value : JSON.stringify(value))
|
|
26
|
+
const text = (typeof value === "string" ? value : JSON.stringify(value))
|
|
27
|
+
.replace(/\s+/g, " ")
|
|
28
|
+
.trim();
|
|
27
29
|
if (!text || text === "{}" || text === "[]") return "";
|
|
28
30
|
return text.length > cap ? text.slice(0, cap) + "…" : text;
|
|
29
|
-
} catch {
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
function contentText(content) {
|
|
@@ -35,11 +39,19 @@ function contentText(content) {
|
|
|
35
39
|
const parts = [];
|
|
36
40
|
for (const block of content) {
|
|
37
41
|
if (!block || typeof block !== "object") continue;
|
|
38
|
-
if (
|
|
42
|
+
if (
|
|
43
|
+
typeof block.text === "string" &&
|
|
44
|
+
["text", "input_text", "output_text"].includes(block.type)
|
|
45
|
+
) {
|
|
39
46
|
parts.push(block.text);
|
|
40
|
-
} else if (
|
|
47
|
+
} else if (
|
|
48
|
+
["tool_use", "toolCall"].includes(block.type) &&
|
|
49
|
+
typeof block.name === "string"
|
|
50
|
+
) {
|
|
41
51
|
const detail = compact(block.input, TOOL_INPUT_CLIP);
|
|
42
|
-
parts.push(
|
|
52
|
+
parts.push(
|
|
53
|
+
detail ? `[tool: ${block.name} ${detail}]` : `[tool: ${block.name}]`
|
|
54
|
+
);
|
|
43
55
|
} else if (block.type === "tool_result") {
|
|
44
56
|
const detail = compact(block.content, TOOL_RESULT_CLIP);
|
|
45
57
|
parts.push(detail ? `[tool result: ${detail}]` : "[tool result]");
|
|
@@ -61,7 +73,9 @@ function slimClaude(row) {
|
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
function finishClaude(records) {
|
|
64
|
-
const byId = new Map(
|
|
76
|
+
const byId = new Map(
|
|
77
|
+
records.filter((row) => row.uuid).map((row) => [row.uuid, row])
|
|
78
|
+
);
|
|
65
79
|
const chain = [];
|
|
66
80
|
const seen = new Set();
|
|
67
81
|
let cursor = records.at(-1);
|
|
@@ -74,16 +88,23 @@ function finishClaude(records) {
|
|
|
74
88
|
cursor = cursor.parentUuid ? byId.get(cursor.parentUuid) : undefined;
|
|
75
89
|
}
|
|
76
90
|
const source = chain.length >= records.length / 2 ? chain : records;
|
|
77
|
-
const messages = source.flatMap((row) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
const messages = source.flatMap((row) =>
|
|
92
|
+
row.text
|
|
93
|
+
? [
|
|
94
|
+
{
|
|
95
|
+
role: row.type === "user" ? "user" : "agent",
|
|
96
|
+
text: row.text,
|
|
97
|
+
ts: row.timestamp,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
: []
|
|
101
|
+
);
|
|
82
102
|
return {
|
|
83
103
|
meta: {
|
|
84
104
|
cwd: records.find((row) => row.cwd)?.cwd || null,
|
|
85
105
|
startedAt: records.find((row) => row.timestamp)?.timestamp || null,
|
|
86
|
-
endedAt:
|
|
106
|
+
endedAt:
|
|
107
|
+
[...records].reverse().find((row) => row.timestamp)?.timestamp || null,
|
|
87
108
|
},
|
|
88
109
|
messages,
|
|
89
110
|
};
|
|
@@ -95,7 +116,11 @@ function parseClaude(text) {
|
|
|
95
116
|
|
|
96
117
|
function finishCodex(meta, messages, startedAt, endedAt) {
|
|
97
118
|
return {
|
|
98
|
-
meta: {
|
|
119
|
+
meta: {
|
|
120
|
+
cwd: typeof meta?.cwd === "string" ? meta.cwd : null,
|
|
121
|
+
startedAt,
|
|
122
|
+
endedAt,
|
|
123
|
+
},
|
|
99
124
|
messages,
|
|
100
125
|
};
|
|
101
126
|
}
|
|
@@ -106,7 +131,8 @@ function parseCodex(text) {
|
|
|
106
131
|
let endedAt = null;
|
|
107
132
|
const messages = [];
|
|
108
133
|
for (const row of parseLines(text)) {
|
|
109
|
-
if (row.type === "session_meta" && row.payload && !Object.keys(meta).length)
|
|
134
|
+
if (row.type === "session_meta" && row.payload && !Object.keys(meta).length)
|
|
135
|
+
meta = row.payload;
|
|
110
136
|
if (typeof row.timestamp === "string") {
|
|
111
137
|
startedAt ||= row.timestamp;
|
|
112
138
|
endedAt = row.timestamp;
|
|
@@ -114,7 +140,12 @@ function parseCodex(text) {
|
|
|
114
140
|
const payload = row.type === "response_item" ? row.payload : null;
|
|
115
141
|
if (!payload || payload.type !== "message") continue;
|
|
116
142
|
const textValue = contentText(payload.content);
|
|
117
|
-
if (textValue)
|
|
143
|
+
if (textValue)
|
|
144
|
+
messages.push({
|
|
145
|
+
role: payload.role === "user" ? "user" : "agent",
|
|
146
|
+
text: textValue,
|
|
147
|
+
ts: typeof row.timestamp === "string" ? row.timestamp : null,
|
|
148
|
+
});
|
|
118
149
|
}
|
|
119
150
|
return finishCodex(meta, messages, startedAt, endedAt);
|
|
120
151
|
}
|
|
@@ -123,7 +154,8 @@ function finishPi(header, messages) {
|
|
|
123
154
|
return {
|
|
124
155
|
meta: {
|
|
125
156
|
cwd: typeof header?.cwd === "string" ? header.cwd : null,
|
|
126
|
-
startedAt:
|
|
157
|
+
startedAt:
|
|
158
|
+
typeof header?.timestamp === "string" ? header.timestamp : null,
|
|
127
159
|
endedAt: messages.at(-1)?.ts || null,
|
|
128
160
|
},
|
|
129
161
|
messages,
|
|
@@ -137,37 +169,57 @@ function parsePi(text) {
|
|
|
137
169
|
if (row.type === "session" && !Object.keys(header).length) header = row;
|
|
138
170
|
if (row.type !== "message") continue;
|
|
139
171
|
const textValue = contentText(row.message?.content);
|
|
140
|
-
if (textValue)
|
|
172
|
+
if (textValue)
|
|
173
|
+
messages.push({
|
|
174
|
+
role: row.message?.role === "user" ? "user" : "agent",
|
|
175
|
+
text: textValue,
|
|
176
|
+
ts: typeof row.timestamp === "string" ? row.timestamp : null,
|
|
177
|
+
});
|
|
141
178
|
}
|
|
142
179
|
return finishPi(header, messages);
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
function asIso(value) {
|
|
146
183
|
if (typeof value === "string") return value;
|
|
147
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
184
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
185
|
+
return new Date(value).toISOString();
|
|
148
186
|
return null;
|
|
149
187
|
}
|
|
150
188
|
|
|
151
189
|
function parseOpencode(text) {
|
|
152
190
|
let envelope = {};
|
|
153
|
-
try {
|
|
191
|
+
try {
|
|
192
|
+
envelope = JSON.parse(text);
|
|
193
|
+
} catch {}
|
|
154
194
|
const info = envelope.info || envelope.session || {};
|
|
155
|
-
const messages = (
|
|
195
|
+
const messages = (
|
|
196
|
+
Array.isArray(envelope.messages) ? envelope.messages : []
|
|
197
|
+
).flatMap((entry) => {
|
|
156
198
|
const messageInfo = entry?.info || entry?.message || {};
|
|
157
199
|
const textValue = contentText(entry?.parts || messageInfo.content);
|
|
158
200
|
const time = messageInfo.time || {};
|
|
159
|
-
return textValue
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
return textValue
|
|
202
|
+
? [
|
|
203
|
+
{
|
|
204
|
+
role: messageInfo.role === "user" ? "user" : "agent",
|
|
205
|
+
text: textValue,
|
|
206
|
+
ts: asIso(messageInfo.timestamp ?? time.created ?? time.start),
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
: [];
|
|
164
210
|
});
|
|
165
211
|
const time = info.time || {};
|
|
166
212
|
return {
|
|
167
213
|
meta: {
|
|
168
|
-
cwd:
|
|
214
|
+
cwd:
|
|
215
|
+
typeof info.directory === "string"
|
|
216
|
+
? info.directory
|
|
217
|
+
: typeof info.cwd === "string"
|
|
218
|
+
? info.cwd
|
|
219
|
+
: null,
|
|
169
220
|
startedAt: asIso(info.created ?? time.created) || messages[0]?.ts || null,
|
|
170
|
-
endedAt:
|
|
221
|
+
endedAt:
|
|
222
|
+
asIso(info.updated ?? time.updated) || messages.at(-1)?.ts || null,
|
|
171
223
|
},
|
|
172
224
|
messages,
|
|
173
225
|
};
|
|
@@ -177,24 +229,41 @@ function titleCandidate(message) {
|
|
|
177
229
|
if (message.role !== "user") return false;
|
|
178
230
|
const text = message.text.trim().toLowerCase();
|
|
179
231
|
if (text.length <= 2 || text.startsWith("[")) return false;
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
232
|
+
return (
|
|
233
|
+
!text.startsWith("<environment_context>") &&
|
|
234
|
+
!text.startsWith("<permissions instructions>") &&
|
|
235
|
+
!text.startsWith("<skills_instructions>")
|
|
236
|
+
);
|
|
183
237
|
}
|
|
184
238
|
|
|
185
239
|
function normalize({ meta, messages }) {
|
|
186
|
-
const firstUser =
|
|
240
|
+
const firstUser =
|
|
241
|
+
messages.find(titleCandidate) ||
|
|
242
|
+
messages.find((message) => message.role === "user");
|
|
187
243
|
return {
|
|
188
244
|
meta: {
|
|
189
245
|
...meta,
|
|
190
|
-
title: (firstUser?.text || "")
|
|
246
|
+
title: (firstUser?.text || "")
|
|
247
|
+
.replace(/\s+/g, " ")
|
|
248
|
+
.trim()
|
|
249
|
+
.slice(0, TITLE_MAX),
|
|
191
250
|
messageCount: messages.length,
|
|
192
251
|
},
|
|
193
|
-
transcript: messages
|
|
252
|
+
transcript: messages
|
|
253
|
+
.map(
|
|
254
|
+
(message) =>
|
|
255
|
+
`${message.role === "user" ? "User" : "Agent"}: ${message.text}`
|
|
256
|
+
)
|
|
257
|
+
.join("\n\n"),
|
|
194
258
|
};
|
|
195
259
|
}
|
|
196
260
|
|
|
197
|
-
const PARSERS = {
|
|
261
|
+
const PARSERS = {
|
|
262
|
+
"claude-code": parseClaude,
|
|
263
|
+
codex: parseCodex,
|
|
264
|
+
pi: parsePi,
|
|
265
|
+
opencode: parseOpencode,
|
|
266
|
+
};
|
|
198
267
|
|
|
199
268
|
export function parseSession(agent, raw) {
|
|
200
269
|
const parser = PARSERS[agent];
|
|
@@ -203,8 +272,10 @@ export function parseSession(agent, raw) {
|
|
|
203
272
|
}
|
|
204
273
|
|
|
205
274
|
export async function parseSessionFile(agent, file) {
|
|
206
|
-
if (agent === "opencode")
|
|
207
|
-
|
|
275
|
+
if (agent === "opencode")
|
|
276
|
+
return parseSession(agent, await fs.promises.readFile(file, "utf8"));
|
|
277
|
+
if (!["claude-code", "codex", "pi"].includes(agent))
|
|
278
|
+
throw new Error(`unsupported agent: ${agent}`);
|
|
208
279
|
|
|
209
280
|
const claudeRecords = [];
|
|
210
281
|
const messages = [];
|
|
@@ -217,14 +288,23 @@ export async function parseSessionFile(agent, file) {
|
|
|
217
288
|
for await (const line of lines) {
|
|
218
289
|
if (!line.trim()) continue;
|
|
219
290
|
let row;
|
|
220
|
-
try {
|
|
291
|
+
try {
|
|
292
|
+
row = JSON.parse(line);
|
|
293
|
+
} catch {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
221
296
|
if (agent === "claude-code") {
|
|
222
297
|
const slim = slimClaude(row);
|
|
223
298
|
if (slim) claudeRecords.push(slim);
|
|
224
299
|
continue;
|
|
225
300
|
}
|
|
226
301
|
if (agent === "codex") {
|
|
227
|
-
if (
|
|
302
|
+
if (
|
|
303
|
+
row.type === "session_meta" &&
|
|
304
|
+
row.payload &&
|
|
305
|
+
!Object.keys(codexMeta).length
|
|
306
|
+
)
|
|
307
|
+
codexMeta = row.payload;
|
|
228
308
|
if (typeof row.timestamp === "string") {
|
|
229
309
|
startedAt ||= row.timestamp;
|
|
230
310
|
endedAt = row.timestamp;
|
|
@@ -232,15 +312,26 @@ export async function parseSessionFile(agent, file) {
|
|
|
232
312
|
const payload = row.type === "response_item" ? row.payload : null;
|
|
233
313
|
if (!payload || payload.type !== "message") continue;
|
|
234
314
|
const textValue = contentText(payload.content);
|
|
235
|
-
if (textValue)
|
|
315
|
+
if (textValue)
|
|
316
|
+
messages.push({
|
|
317
|
+
role: payload.role === "user" ? "user" : "agent",
|
|
318
|
+
text: textValue,
|
|
319
|
+
ts: typeof row.timestamp === "string" ? row.timestamp : null,
|
|
320
|
+
});
|
|
236
321
|
continue;
|
|
237
322
|
}
|
|
238
323
|
if (row.type === "session" && !Object.keys(piHeader).length) piHeader = row;
|
|
239
324
|
if (row.type !== "message") continue;
|
|
240
325
|
const textValue = contentText(row.message?.content);
|
|
241
|
-
if (textValue)
|
|
326
|
+
if (textValue)
|
|
327
|
+
messages.push({
|
|
328
|
+
role: row.message?.role === "user" ? "user" : "agent",
|
|
329
|
+
text: textValue,
|
|
330
|
+
ts: typeof row.timestamp === "string" ? row.timestamp : null,
|
|
331
|
+
});
|
|
242
332
|
}
|
|
243
333
|
if (agent === "claude-code") return normalize(finishClaude(claudeRecords));
|
|
244
|
-
if (agent === "codex")
|
|
334
|
+
if (agent === "codex")
|
|
335
|
+
return normalize(finishCodex(codexMeta, messages, startedAt, endedAt));
|
|
245
336
|
return normalize(finishPi(piHeader, messages));
|
|
246
337
|
}
|
package/lib/paths.mjs
CHANGED
|
@@ -12,7 +12,10 @@ export const paths = {
|
|
|
12
12
|
config: path.join(harnessHome, "config.json"),
|
|
13
13
|
database: path.join(harnessHome, "harness.db"),
|
|
14
14
|
status: path.join(harnessHome, "daemon-status.json"),
|
|
15
|
-
socket:
|
|
15
|
+
socket:
|
|
16
|
+
process.platform === "win32"
|
|
17
|
+
? "\\\\.\\pipe\\tryharness-daemon"
|
|
18
|
+
: path.join(harnessHome, "daemon.sock"),
|
|
16
19
|
log: path.join(harnessHome, "daemon.log"),
|
|
17
20
|
phoneIdentity: path.join(harnessHome, "phone-identity.json"),
|
|
18
21
|
runs: path.join(harnessHome, "runs"),
|
|
@@ -24,16 +27,36 @@ export const paths = {
|
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export function historyRoots(agent) {
|
|
27
|
-
if (agent === "claude-code")
|
|
28
|
-
|
|
30
|
+
if (agent === "claude-code")
|
|
31
|
+
return [
|
|
32
|
+
process.env.HARNESS_CLAUDE_HISTORY_ROOT ||
|
|
33
|
+
path.join(os.homedir(), ".claude", "projects"),
|
|
34
|
+
];
|
|
35
|
+
if (agent === "codex")
|
|
36
|
+
return [
|
|
37
|
+
process.env.HARNESS_CODEX_HISTORY_ROOT ||
|
|
38
|
+
path.join(os.homedir(), ".codex", "sessions"),
|
|
39
|
+
];
|
|
29
40
|
if (agent === "pi") {
|
|
30
|
-
return Array.from(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
return Array.from(
|
|
42
|
+
new Set(
|
|
43
|
+
[
|
|
44
|
+
process.env.HARNESS_PI_HISTORY_ROOT,
|
|
45
|
+
process.env.PI_CODING_AGENT_SESSION_DIR,
|
|
46
|
+
process.env.PI_CODING_AGENT_DIR
|
|
47
|
+
? path.join(process.env.PI_CODING_AGENT_DIR, "sessions")
|
|
48
|
+
: null,
|
|
49
|
+
path.join(os.homedir(), ".pi", "agent", "sessions"),
|
|
50
|
+
path.join(
|
|
51
|
+
os.homedir(),
|
|
52
|
+
".local",
|
|
53
|
+
"share",
|
|
54
|
+
"pi-coding-agent",
|
|
55
|
+
"sessions"
|
|
56
|
+
),
|
|
57
|
+
].filter(Boolean)
|
|
58
|
+
)
|
|
59
|
+
);
|
|
37
60
|
}
|
|
38
61
|
return [];
|
|
39
62
|
}
|
package/lib/personal.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Personal details in text: an email, a phone number, a wallet address, an amount of money. The same patterns cover
|
|
2
|
+
// screenshots read by OCR (redact.mjs) and the words of the relay's decision-only requests to Harness (firewall.mjs).
|
|
3
|
+
|
|
4
|
+
// Wallet addresses: Ethereum-style 0x and 40 hex (OCR reads some 0s as Os), the shortened 0x1234…abcd form apps show,
|
|
5
|
+
// bech32 (bc1…), and base58 strings long and mixed enough to be addresses or keys rather than words or identifiers
|
|
6
|
+
// (Solana, Bitcoin legacy, base58 private keys), one misread character allowed. Hex alone is a hash, not an address,
|
|
7
|
+
// and a sha512-… integrity string is a hash in base64. OCR often runs a label into the value ("addressbc1q…"), so the
|
|
8
|
+
// prefixed forms need no boundary before them.
|
|
9
|
+
export const EVM = /[0O][xX][0-9a-fA-FOo]{38,42}(?![\w])/g;
|
|
10
|
+
export const EVM_SHORT =
|
|
11
|
+
/0[xX][0-9a-fA-F]{3,8}(?:…|\.{2,3})[0-9a-fA-F]{3,8}(?![\w])/g;
|
|
12
|
+
export const BECH32 = /(?:bc1|tb1|ltc1)[a-zA-Z0-9]{20,90}(?![\w])/g;
|
|
13
|
+
export const BASE58ISH = /(?<![\w])[0-9A-Za-z]{25,90}(?![\w])/g;
|
|
14
|
+
export const base58 = (token) =>
|
|
15
|
+
(token.match(/[0OIl]/g) || []).length <= 1 &&
|
|
16
|
+
(token.match(/\d/g) || []).length >= 2 &&
|
|
17
|
+
(token.match(/[A-Z]/g) || []).length >= 3 &&
|
|
18
|
+
(token.match(/[a-z]/g) || []).length >= 3 &&
|
|
19
|
+
/[g-zG-Z]/.test(token);
|
|
20
|
+
// Money: an amount with a currency sign or code (a lone digit after $ is a shell variable, not money).
|
|
21
|
+
const CODES =
|
|
22
|
+
"USD|USDC|USDT|EUR|GBP|JPY|CHF|CAD|AUD|ETH|WETH|stETH|BTC|WBTC|cbBTC|SOL|DAI|BNB|MATIC|POL|ARB|OP|AVAX|DOGE|XRP|ADA|DOT|LINK|UNI|AAVE|DEGEN";
|
|
23
|
+
const AMOUNT = "\\d{1,3}(?:[,']\\d{3})+(?:[.,]\\d+)?|\\d+(?:[.,]\\d+)?";
|
|
24
|
+
export const MONEY = new RegExp(
|
|
25
|
+
`(?<![\\w$€£¥])(?:[-+−]\\s?)?(?:(?:US)?[$€£¥₹₩₿Ξ]\\s?(?:${AMOUNT})(?:\\s?[KMBkmb](?![A-Za-z]))?|(?:${AMOUNT})(?:\\s?[KMBkmb](?=\\s?(?:${CODES})))?\\s?(?:(?:${CODES})(?![A-Za-z])|[€£₿Ξ]))`,
|
|
26
|
+
"g"
|
|
27
|
+
);
|
|
28
|
+
export const EMAIL =
|
|
29
|
+
/[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}/g;
|
|
30
|
+
// Phone numbers: digit groups joined by single spaces, dots or dashes, with an optional +country code and (area code);
|
|
31
|
+
// 10 to 15 digits (8 with a country code). Dates, times, IP addresses and version numbers do not qualify.
|
|
32
|
+
export const PHONE =
|
|
33
|
+
/(?<![\w.+/:#-])(?:\+\d{1,3}[ .-]?)?(?:\(\d{1,4}\)[ .-]?)?\d{2,5}(?:[ .-]\d{2,6}){1,4}(?![\w])/g;
|
|
34
|
+
export const phoneLike = (text) => {
|
|
35
|
+
const digits = text.replace(/\D/g, "").length;
|
|
36
|
+
return (
|
|
37
|
+
digits <= 15 &&
|
|
38
|
+
digits >= (text.startsWith("+") ? 8 : 10) &&
|
|
39
|
+
!/^\d{4}[-.]\d{1,2}[-.]\d{1,2}/.test(text) &&
|
|
40
|
+
!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(text)
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
/** Personal details in plain text: [{ start, end, kind }] (kind: wallet, email, phone, money), longest first where they overlap. */
|
|
44
|
+
export function personalSpans(text) {
|
|
45
|
+
const value = String(text);
|
|
46
|
+
const found = [];
|
|
47
|
+
const take = (pattern, kind, keep = () => true) => {
|
|
48
|
+
for (const m of value.matchAll(pattern))
|
|
49
|
+
if (keep(m[0], value.slice(0, m.index)))
|
|
50
|
+
found.push({ start: m.index, end: m.index + m[0].length, kind });
|
|
51
|
+
};
|
|
52
|
+
take(EVM, "wallet");
|
|
53
|
+
take(EVM_SHORT, "wallet");
|
|
54
|
+
take(BECH32, "wallet");
|
|
55
|
+
take(
|
|
56
|
+
BASE58ISH,
|
|
57
|
+
"wallet",
|
|
58
|
+
(token, before) =>
|
|
59
|
+
base58(token) && !/sha\d+[-:][A-Za-z0-9+/]*$/i.test(before)
|
|
60
|
+
);
|
|
61
|
+
take(
|
|
62
|
+
EMAIL,
|
|
63
|
+
"email",
|
|
64
|
+
(token, before) => !/^git@/i.test(token) && !/:\/\/\S*$/.test(before)
|
|
65
|
+
);
|
|
66
|
+
take(PHONE, "phone", phoneLike);
|
|
67
|
+
take(
|
|
68
|
+
MONEY,
|
|
69
|
+
"money",
|
|
70
|
+
(token) => !/^[-+−]?\s?(?:US)?[$€£¥₹₩₿Ξ]\s?\d$/.test(token)
|
|
71
|
+
);
|
|
72
|
+
const kept = [];
|
|
73
|
+
for (const span of found.sort((a, b) => b.end - b.start - (a.end - a.start)))
|
|
74
|
+
if (!kept.some((other) => other.start < span.end && span.start < other.end))
|
|
75
|
+
kept.push(span);
|
|
76
|
+
return kept.sort((a, b) => a.start - b.start);
|
|
77
|
+
}
|