thinkpool-pair 0.7.334 → 0.7.335

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 (2) hide show
  1. package/package.json +2 -1
  2. package/terminal-name.mjs +74 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.334",
3
+ "version": "0.7.335",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -113,6 +113,7 @@
113
113
  "@anthropic-ai/claude-agent-sdk": "^0.3.198",
114
114
  "@modelcontextprotocol/sdk": "^1.29.0",
115
115
  "@supabase/supabase-js": "^2.45.0",
116
+ "compromise": "14.16.0",
116
117
  "yaml": "2.9.0",
117
118
  "zod": "^4.4.3"
118
119
  },
package/terminal-name.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { nativeClaudeProviderAccessFailed } from './cross-terminal.mjs'
2
+ import nlp from 'compromise/two'
2
3
 
3
4
  const SKIP = new Set([
4
5
  'a', 'an', 'and', 'are', 'at', 'be', 'can', 'could', 'for', 'from', 'how', 'i',
@@ -18,12 +19,66 @@ const NOISE = /^(?:context|background|for reference|here(?:'s| is)|note|current(
18
19
  const EXPLANATION = /^(?:because|cause|since|so that|this is because)\b/i
19
20
  const CONSTRAINT = /^(?:users? can still|keep|must|never|should|without)\b/i
20
21
  const DEPENDENT_FRAGMENT = /^(?:after|before|by|during|for|from|in|instead(?: of)?|on|through|until|with|without)\b/i
21
- const INFORMATION_REQUEST = /^(?:which|what|who|where|when|why|how)\b/i
22
- const NEGATIVE_PREFERENCE = /^(?:(?:i|we)\s+)?(?:do not|don't|dont|would not|wouldn't|won't|wont)\s+(?:(?:want|wanna|need)(?:\s+to)?|use|include|choose)\b|^(?:(?:i|we)\s+)?(?:want|wanna|need)(?:\s+to)?\s+avoid\b/i
23
22
  const ISSUE = /\b(?:broken|buggy|crash(?:es|ed|ing)?|duplicate|error|fail(?:s|ed|ing|ure)?|flash(?:es|ed|ing)?|missing|no animation|not working|out of (?:scrollable )?view|stuck|wrong)\b/i
24
23
  const REQUEST = /^(?:please\s+)?(?:can|could|would|will)\s+(?:we|you)\b|^(?:please\s+)?(?:how about|let's|let us|we need to|i want you to)\b/i
25
24
  const SECRET = /(?<![\p{L}\p{N}])(?:sk-(?:proj-)?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|xox[baprs]-[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|glpat-[a-z0-9_-]{8,}|npm_[a-z0-9]{24,}|(?:sk|rk)_(?:live|test)_[a-z0-9]{8,}|whsec_[a-z0-9]{8,}|AIza[a-z0-9_-]{8,}|AKIA[A-Z0-9]{12,}|bearer\s+[a-z0-9._-]{8,}|eyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\.[a-z0-9_-]{8,})(?![\p{L}\p{N}])/giu
26
25
  const HANDOFF_MARKER = /^---\s*(?:the person's next message|current person request(?:\s*\([^\n]*\))?|side task)\s*---\s*$/gim
26
+ const QUESTION_WORDS = new Set(['how', 'what', 'when', 'where', 'which', 'who', 'whom', 'whose', 'why'])
27
+ const PREFERENCE_WORDS = new Set(['avoid', 'choose', 'include', 'need', 'use', 'want'])
28
+ const PREFERENCE_OBJECT_WORDS = new Set(['avoid', 'choose', 'include', 'use'])
29
+
30
+ const conceptsForTerm = (term) => new Set(
31
+ [term?.text, term?.normal, term?.machine, term?.implicit]
32
+ .map((value) => String(value || '').trim().toLowerCase())
33
+ .filter(Boolean),
34
+ )
35
+
36
+ const analyzeGrammar = (value, suppliedTerms = null) => {
37
+ const text = String(value || '').trim()
38
+ const terms = suppliedTerms || nlp(text).json({ terms: true }).flatMap((sentence) => sentence.terms || [])
39
+ const visible = terms
40
+ .filter((term) => String(term?.text || '').trim())
41
+ .map((term) => ({ text: term.text, concepts: conceptsForTerm(term), tags: new Set(term.tags || []) }))
42
+ const hasQuestionWord = visible.some((term) => [...term.concepts].some((word) => QUESTION_WORDS.has(word)))
43
+ const taggedQuestionWord = visible.some((term) => term.tags.has('QuestionWord'))
44
+ const negative = terms.some((term) => Array.isArray(term?.tags) && term.tags.includes('Negative'))
45
+ const preference = terms.some((term) => [...conceptsForTerm(term)].some((word) => PREFERENCE_WORDS.has(word)))
46
+ return {
47
+ text,
48
+ visible,
49
+ informationRequest: hasQuestionWord && (taggedQuestionWord || /\?\s*$/.test(text)),
50
+ negativePreference: negative && preference,
51
+ }
52
+ }
53
+
54
+ const parsedClauses = (value) => String(value || '')
55
+ .replace(/,\s*(?:but|however)\s+/gi, '\n')
56
+ .split(/\n+|;\s*/)
57
+ .flatMap((part) => {
58
+ const text = part.trim()
59
+ if (!text) return []
60
+ const sentences = nlp(text).json({ terms: true })
61
+ return sentences.length
62
+ ? sentences.map((sentence) => analyzeGrammar(sentence.text, sentence.terms || []))
63
+ : [analyzeGrammar(text)]
64
+ })
65
+ .filter((clause) => clause.text)
66
+
67
+ const questionSubject = (analysis) => {
68
+ const index = analysis.visible.findIndex((term) => [...term.concepts].some((word) => QUESTION_WORDS.has(word)))
69
+ return index >= 0 ? analysis.visible.slice(index + 1).map((term) => term.text).join(' ') : analysis.text
70
+ }
71
+
72
+ const negativePreferenceSubject = (analysis) => {
73
+ let index = -1
74
+ for (let i = 0; i < analysis.visible.length; i++) {
75
+ if ([...analysis.visible[i].concepts].some((word) => PREFERENCE_OBJECT_WORDS.has(word))) index = i
76
+ }
77
+ if (index < 0) {
78
+ index = analysis.visible.findIndex((term) => [...term.concepts].some((word) => word === 'need' || word === 'want'))
79
+ }
80
+ return index >= 0 ? analysis.visible.slice(index + 1).map((term) => term.text).join(' ') : analysis.text
81
+ }
27
82
 
28
83
  const ACTIONS = Object.freeze([
29
84
  { title: 'Fix', score: 100, re: /\b(?:fix|fixes|fixed|fixing|repair|repairs|repaired|repairing)\b/i },
@@ -117,35 +172,31 @@ const explicitIntent = (text) => {
117
172
  }
118
173
 
119
174
  const bestIntentClause = (text) => {
120
- const clauses = text
121
- .replace(/,\s*(?:but|however)\s+/gi, '\n')
122
- .split(/\n+|(?<=[.!?;])\s+/)
123
- .map((part) => part.trim())
124
- .filter(Boolean)
175
+ const clauses = parsedClauses(text)
125
176
  let best = null
126
- for (let i = 0; i < clauses.length; i++) {
127
- const clause = clauses[i].replace(/^[-*#>\d.)\s]+/, '').trim()
177
+ for (const parsed of clauses) {
178
+ const clause = parsed.text.replace(/^[-*#>\d.)\s]+/, '').trim()
128
179
  if (!clause || /^(?:https?:\/\/|\/|[A-Za-z]:\\)/.test(clause)) continue
129
- const words = clause.match(/[\p{L}\p{N}][\p{L}\p{N}+#.'-]*/gu) || []
180
+ const grammar = clause === parsed.text ? parsed : analyzeGrammar(clause)
130
181
  const action = bestAction(clause)
131
182
  let score = action?.score || 0
132
183
  if (REQUEST.test(clause)) score += 18
133
- if (INFORMATION_REQUEST.test(clause)) score += 36
184
+ if (grammar.informationRequest) score += 36
134
185
  if (ISSUE.test(clause)) score += /\b(?:crash|fail|flash|stuck|wrong)\w*\b/i.test(clause) ? 36 : 26
135
- if (words.length >= 3 && words.length <= 24) score += 10
136
- else if (words.length > 40) score -= 14
186
+ if (grammar.visible.length >= 3 && grammar.visible.length <= 24) score += 10
187
+ else if (grammar.visible.length > 40) score -= 14
137
188
  if (NOISE.test(clause)) score -= 30
138
189
  if (EXPLANATION.test(clause)) score -= 34
139
190
  if (CONSTRAINT.test(clause)) score -= 45
140
- if (NEGATIVE_PREFERENCE.test(clause)) score -= 70
191
+ if (grammar.negativePreference) score -= 70
141
192
  // A short dependent tail such as "From ground up" or "On mobile" adds
142
193
  // scope to the preceding request; it is not a standalone task. Recency is
143
194
  // deliberately not a signal here: the title should represent the main
144
195
  // theme, not whichever sentence happened to come last.
145
- if (!action && !REQUEST.test(clause) && !INFORMATION_REQUEST.test(clause) && DEPENDENT_FRAGMENT.test(clause)) score -= 24
196
+ if (!action && !REQUEST.test(clause) && !grammar.informationRequest && DEPENDENT_FRAGMENT.test(clause)) score -= 24
146
197
  if (!best || score > best.score) best = { clause, score }
147
198
  }
148
- return best?.clause || clauses[0] || null
199
+ return best?.clause || clauses[0]?.text || null
149
200
  }
150
201
 
151
202
  export function extractTerminalTask(value) {
@@ -210,9 +261,13 @@ const taskTitle = (value) => {
210
261
  .replace(/^\s*(?:how about|i (?:do not|don't) know|i guess|i want you to|we need to|your task is to|let(?:'s| us))\s+/i, '')
211
262
  .trim()
212
263
  if (!body) return null
213
- if (NEGATIVE_PREFERENCE.test(body)) return null
214
- const informationRequest = INFORMATION_REQUEST.test(body)
215
- if (informationRequest) body = body.replace(/^\s*(?:which|what)\s+/i, '').trim()
264
+ const grammar = analyzeGrammar(body)
265
+ if (grammar.negativePreference) {
266
+ const subject = contentWords(negativePreferenceSubject(grammar)).slice(0, 4)
267
+ return subject.length ? cleanTerminalName(['Avoid', ...subject.map(titleWord)].join(' ')) : null
268
+ }
269
+ const informationRequest = grammar.informationRequest
270
+ if (informationRequest) body = questionSubject(grammar).trim()
216
271
  const signalText = body
217
272
  body = body
218
273
  .replace(/\s*,?\s+(?:so (?:that|you|we|it)\b|because\b|cause\b|if you know what i mean\b)[\s\S]*$/i, '')