tryharness 0.7.23 → 0.7.26

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.
Files changed (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
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)).replace(/\s+/g, " ").trim();
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 { return ""; }
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 (typeof block.text === "string" && ["text", "input_text", "output_text"].includes(block.type)) {
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 (["tool_use", "toolCall"].includes(block.type) && typeof block.name === "string") {
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(detail ? `[tool: ${block.name} ${detail}]` : `[tool: ${block.name}]`);
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(records.filter((row) => row.uuid).map((row) => [row.uuid, row]));
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) => row.text ? [{
78
- role: row.type === "user" ? "user" : "agent",
79
- text: row.text,
80
- ts: row.timestamp,
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: [...records].reverse().find((row) => row.timestamp)?.timestamp || null,
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: { cwd: typeof meta?.cwd === "string" ? meta.cwd : null, startedAt, endedAt },
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) meta = row.payload;
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) messages.push({ role: payload.role === "user" ? "user" : "agent", text: textValue, ts: typeof row.timestamp === "string" ? row.timestamp : null });
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: typeof header?.timestamp === "string" ? header.timestamp : null,
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) messages.push({ role: row.message?.role === "user" ? "user" : "agent", text: textValue, ts: typeof row.timestamp === "string" ? row.timestamp : null });
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)) return new Date(value).toISOString();
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 { envelope = JSON.parse(text); } catch {}
191
+ try {
192
+ envelope = JSON.parse(text);
193
+ } catch {}
154
194
  const info = envelope.info || envelope.session || {};
155
- const messages = (Array.isArray(envelope.messages) ? envelope.messages : []).flatMap((entry) => {
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
- role: messageInfo.role === "user" ? "user" : "agent",
161
- text: textValue,
162
- ts: asIso(messageInfo.timestamp ?? time.created ?? time.start),
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: typeof info.directory === "string" ? info.directory : typeof info.cwd === "string" ? info.cwd : null,
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: asIso(info.updated ?? time.updated) || messages.at(-1)?.ts || null,
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 !text.startsWith("<environment_context>")
181
- && !text.startsWith("<permissions instructions>")
182
- && !text.startsWith("<skills_instructions>");
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 = messages.find(titleCandidate) || messages.find((message) => message.role === "user");
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 || "").replace(/\s+/g, " ").trim().slice(0, TITLE_MAX),
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.map((message) => `${message.role === "user" ? "User" : "Agent"}: ${message.text}`).join("\n\n"),
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 = { "claude-code": parseClaude, codex: parseCodex, pi: parsePi, opencode: parseOpencode };
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") return parseSession(agent, await fs.promises.readFile(file, "utf8"));
207
- if (!["claude-code", "codex", "pi"].includes(agent)) throw new Error(`unsupported agent: ${agent}`);
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 { row = JSON.parse(line); } catch { continue; }
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 (row.type === "session_meta" && row.payload && !Object.keys(codexMeta).length) codexMeta = row.payload;
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) messages.push({ role: payload.role === "user" ? "user" : "agent", text: textValue, ts: typeof row.timestamp === "string" ? row.timestamp : null });
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) messages.push({ role: row.message?.role === "user" ? "user" : "agent", text: textValue, ts: typeof row.timestamp === "string" ? row.timestamp : null });
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") return normalize(finishCodex(codexMeta, messages, startedAt, endedAt));
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: process.platform === "win32" ? "\\\\.\\pipe\\tryharness-daemon" : path.join(harnessHome, "daemon.sock"),
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") return [process.env.HARNESS_CLAUDE_HISTORY_ROOT || path.join(os.homedir(), ".claude", "projects")];
28
- if (agent === "codex") return [process.env.HARNESS_CODEX_HISTORY_ROOT || path.join(os.homedir(), ".codex", "sessions")];
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(new Set([
31
- process.env.HARNESS_PI_HISTORY_ROOT,
32
- process.env.PI_CODING_AGENT_SESSION_DIR,
33
- process.env.PI_CODING_AGENT_DIR ? path.join(process.env.PI_CODING_AGENT_DIR, "sessions") : null,
34
- path.join(os.homedir(), ".pi", "agent", "sessions"),
35
- path.join(os.homedir(), ".local", "share", "pi-coding-agent", "sessions"),
36
- ].filter(Boolean)));
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 CHANGED
@@ -7,33 +7,71 @@
7
7
  // and a sha512-… integrity string is a hash in base64. OCR often runs a label into the value ("addressbc1q…"), so the
8
8
  // prefixed forms need no boundary before them.
9
9
  export const EVM = /[0O][xX][0-9a-fA-FOo]{38,42}(?![\w])/g;
10
- export const EVM_SHORT = /0[xX][0-9a-fA-F]{3,8}(?:…|\.{2,3})[0-9a-fA-F]{3,8}(?![\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;
11
12
  export const BECH32 = /(?:bc1|tb1|ltc1)[a-zA-Z0-9]{20,90}(?![\w])/g;
12
13
  export const BASE58ISH = /(?<![\w])[0-9A-Za-z]{25,90}(?![\w])/g;
13
- export const base58 = (token) => (token.match(/[0OIl]/g) || []).length <= 1 && (token.match(/\d/g) || []).length >= 2 && (token.match(/[A-Z]/g) || []).length >= 3 && (token.match(/[a-z]/g) || []).length >= 3 && /[g-zG-Z]/.test(token);
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);
14
20
  // Money: an amount with a currency sign or code (a lone digit after $ is a shell variable, not money).
15
- const CODES = "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";
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";
16
23
  const AMOUNT = "\\d{1,3}(?:[,']\\d{3})+(?:[.,]\\d+)?|\\d+(?:[.,]\\d+)?";
17
- export const MONEY = new RegExp(`(?<![\\w$€£¥])(?:[-+−]\\s?)?(?:(?:US)?[$€£¥₹₩₿Ξ]\\s?(?:${AMOUNT})(?:\\s?[KMBkmb](?![A-Za-z]))?|(?:${AMOUNT})(?:\\s?[KMBkmb](?=\\s?(?:${CODES})))?\\s?(?:(?:${CODES})(?![A-Za-z])|[€£₿Ξ]))`, "g");
18
- export const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}/g;
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;
19
30
  // Phone numbers: digit groups joined by single spaces, dots or dashes, with an optional +country code and (area code);
20
31
  // 10 to 15 digits (8 with a country code). Dates, times, IP addresses and version numbers do not qualify.
21
- export const PHONE = /(?<![\w.+/:#-])(?:\+\d{1,3}[ .-]?)?(?:\(\d{1,4}\)[ .-]?)?\d{2,5}(?:[ .-]\d{2,6}){1,4}(?![\w])/g;
32
+ export const PHONE =
33
+ /(?<![\w.+/:#-])(?:\+\d{1,3}[ .-]?)?(?:\(\d{1,4}\)[ .-]?)?\d{2,5}(?:[ .-]\d{2,6}){1,4}(?![\w])/g;
22
34
  export const phoneLike = (text) => {
23
35
  const digits = text.replace(/\D/g, "").length;
24
- return digits <= 15 && digits >= (text.startsWith("+") ? 8 : 10) && !/^\d{4}[-.]\d{1,2}[-.]\d{1,2}/.test(text) && !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(text);
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
+ );
25
42
  };
26
43
  /** Personal details in plain text: [{ start, end, kind }] (kind: wallet, email, phone, money), longest first where they overlap. */
27
44
  export function personalSpans(text) {
28
45
  const value = String(text);
29
46
  const found = [];
30
- const take = (pattern, kind, keep = () => true) => { for (const m of value.matchAll(pattern)) if (keep(m[0], value.slice(0, m.index))) found.push({ start: m.index, end: m.index + m[0].length, kind }); };
31
- take(EVM, "wallet"); take(EVM_SHORT, "wallet"); take(BECH32, "wallet");
32
- take(BASE58ISH, "wallet", (token, before) => base58(token) && !/sha\d+[-:][A-Za-z0-9+/]*$/i.test(before));
33
- take(EMAIL, "email", (token, before) => !/^git@/i.test(token) && !/:\/\/\S*$/.test(before));
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
+ );
34
66
  take(PHONE, "phone", phoneLike);
35
- take(MONEY, "money", (token) => !/^[-+−]?\s?(?:US)?[$€£¥₹₩₿Ξ]\s?\d$/.test(token));
67
+ take(
68
+ MONEY,
69
+ "money",
70
+ (token) => !/^[-+−]?\s?(?:US)?[$€£¥₹₩₿Ξ]\s?\d$/.test(token)
71
+ );
36
72
  const kept = [];
37
- for (const span of found.sort((a, b) => (b.end - b.start) - (a.end - a.start))) if (!kept.some((other) => other.start < span.end && span.start < other.end)) kept.push(span);
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);
38
76
  return kept.sort((a, b) => a.start - b.start);
39
77
  }