@reconcrap/boss-recommend-mcp 1.2.9 → 1.3.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.
Files changed (36) hide show
  1. package/README.md +82 -1
  2. package/package.json +2 -1
  3. package/skills/boss-chat/README.md +5 -0
  4. package/skills/boss-chat/SKILL.md +69 -0
  5. package/skills/boss-recommend-pipeline/SKILL.md +40 -4
  6. package/src/adapters.js +19 -5
  7. package/src/boss-chat.js +436 -0
  8. package/src/cli.js +294 -129
  9. package/src/index.js +459 -108
  10. package/src/parser.js +4 -5
  11. package/src/pipeline.js +605 -8
  12. package/src/run-state.js +5 -0
  13. package/src/test-adapters-runtime.js +69 -0
  14. package/src/test-boss-chat.js +399 -0
  15. package/src/test-index-async.js +238 -4
  16. package/src/test-parser.js +33 -6
  17. package/src/test-pipeline.js +408 -1
  18. package/vendor/boss-chat-cli/README.md +134 -0
  19. package/vendor/boss-chat-cli/package.json +53 -0
  20. package/vendor/boss-chat-cli/src/app.js +769 -0
  21. package/vendor/boss-chat-cli/src/browser/chat-page.js +2681 -0
  22. package/vendor/boss-chat-cli/src/cli.js +1350 -0
  23. package/vendor/boss-chat-cli/src/mcp/server.js +149 -0
  24. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +193 -0
  25. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +260 -0
  26. package/vendor/boss-chat-cli/src/runtime/interaction.js +102 -0
  27. package/vendor/boss-chat-cli/src/runtime/run-control.js +102 -0
  28. package/vendor/boss-chat-cli/src/services/chrome-client.js +97 -0
  29. package/vendor/boss-chat-cli/src/services/llm.js +352 -0
  30. package/vendor/boss-chat-cli/src/services/profile-store.js +157 -0
  31. package/vendor/boss-chat-cli/src/services/report-store.js +19 -0
  32. package/vendor/boss-chat-cli/src/services/resume-capture.js +554 -0
  33. package/vendor/boss-chat-cli/src/services/state-store.js +217 -0
  34. package/vendor/boss-chat-cli/src/utils/customer-key.js +82 -0
  35. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +902 -56
  36. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +387 -1
@@ -0,0 +1,217 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const DEFAULT_STATE_TTL_HOURS = 24;
5
+
6
+ function createInitialState(profileName) {
7
+ return {
8
+ version: 1,
9
+ profileName,
10
+ updatedAt: null,
11
+ customers: {},
12
+ aliases: {},
13
+ };
14
+ }
15
+
16
+ function isRecoverableStateError(error) {
17
+ if (!error) return false;
18
+ if (error.code === 'ENOENT') return true;
19
+ if (error.name === 'SyntaxError') return true;
20
+ const message = String(error.message || '').toLowerCase();
21
+ return (
22
+ message.includes('unexpected end of json') ||
23
+ message.includes('unexpected token') ||
24
+ message.includes('json')
25
+ );
26
+ }
27
+
28
+ function stateBackupPath(filePath) {
29
+ const token = new Date().toISOString().replace(/[:.]/g, '-');
30
+ return `${filePath}.corrupt-${token}.bak`;
31
+ }
32
+
33
+ function parsePositiveNumber(value, fallback) {
34
+ const parsed = Number(value);
35
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
36
+ }
37
+
38
+ function resolveStateTtlMs(options = {}) {
39
+ const hours = parsePositiveNumber(
40
+ options.stateTtlHours ?? process.env.BOSS_CHAT_STATE_TTL_HOURS,
41
+ DEFAULT_STATE_TTL_HOURS,
42
+ );
43
+ return Math.floor(hours * 60 * 60 * 1000);
44
+ }
45
+
46
+ function isEntryExpired(entry, nowMs, ttlMs) {
47
+ if (!entry || typeof entry !== 'object') return true;
48
+ const updatedAtRaw = String(entry.updatedAt || '').trim();
49
+ if (!updatedAtRaw) return false;
50
+ const updatedAtMs = Date.parse(updatedAtRaw);
51
+ if (!Number.isFinite(updatedAtMs)) return false;
52
+ return nowMs - updatedAtMs > ttlMs;
53
+ }
54
+
55
+ function pruneExpiredState(state, ttlMs, nowMs = Date.now()) {
56
+ const nextCustomers = {};
57
+ const nextAliases = {};
58
+ const customers = state?.customers && typeof state.customers === 'object' ? state.customers : {};
59
+ const aliases = state?.aliases && typeof state.aliases === 'object' ? state.aliases : {};
60
+ let removedCount = 0;
61
+
62
+ for (const [key, entry] of Object.entries(customers)) {
63
+ if (isEntryExpired(entry, nowMs, ttlMs)) {
64
+ removedCount += 1;
65
+ continue;
66
+ }
67
+ nextCustomers[key] = entry;
68
+ }
69
+
70
+ for (const [alias, key] of Object.entries(aliases)) {
71
+ if (typeof alias !== 'string' || !alias) continue;
72
+ if (typeof key !== 'string' || !key) continue;
73
+ if (!nextCustomers[key]) continue;
74
+ nextAliases[alias] = key;
75
+ }
76
+
77
+ const changed =
78
+ removedCount > 0 ||
79
+ Object.keys(nextAliases).length !== Object.keys(aliases).length ||
80
+ Object.keys(nextCustomers).length !== Object.keys(customers).length;
81
+
82
+ return {
83
+ changed,
84
+ removedCount,
85
+ customers: nextCustomers,
86
+ aliases: nextAliases,
87
+ };
88
+ }
89
+
90
+ export class StateStore {
91
+ constructor(baseDir, profileName, options = {}) {
92
+ this.baseDir = baseDir;
93
+ this.profileName = profileName;
94
+ this.statesDir = path.join(baseDir, 'state');
95
+ this.filePath = path.join(this.statesDir, `${profileName}.json`);
96
+ this.state = createInitialState(profileName);
97
+ this.stateTtlMs = resolveStateTtlMs(options);
98
+ }
99
+
100
+ async load() {
101
+ await mkdir(this.statesDir, { recursive: true });
102
+ const defaults = createInitialState(this.profileName);
103
+ try {
104
+ const raw = await readFile(this.filePath, 'utf8');
105
+ const parsed = JSON.parse(String(raw || ''));
106
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
107
+ throw new SyntaxError('state file is not a JSON object');
108
+ }
109
+ this.state = {
110
+ ...defaults,
111
+ ...parsed,
112
+ customers: {
113
+ ...defaults.customers,
114
+ ...(parsed.customers || {}),
115
+ },
116
+ aliases: {
117
+ ...defaults.aliases,
118
+ ...(parsed.aliases || {}),
119
+ },
120
+ };
121
+
122
+ const pruned = pruneExpiredState(this.state, this.stateTtlMs);
123
+ if (pruned.changed) {
124
+ this.state.customers = pruned.customers;
125
+ this.state.aliases = pruned.aliases;
126
+ this.state.updatedAt = new Date().toISOString();
127
+ await this.persistState();
128
+ }
129
+ } catch (error) {
130
+ if (!isRecoverableStateError(error)) {
131
+ throw error;
132
+ }
133
+
134
+ this.state = defaults;
135
+ if (error?.code !== 'ENOENT') {
136
+ const backupPath = stateBackupPath(this.filePath);
137
+ await rename(this.filePath, backupPath);
138
+ }
139
+ await this.persistState();
140
+ }
141
+ return this.state;
142
+ }
143
+
144
+ has(customerKey) {
145
+ return Boolean(this.resolveKey(customerKey));
146
+ }
147
+
148
+ hasAny(customerKeys = []) {
149
+ return customerKeys.some((customerKey) => this.has(customerKey));
150
+ }
151
+
152
+ get(customerKey) {
153
+ const resolved = this.resolveKey(customerKey);
154
+ return resolved ? this.state.customers[resolved] || null : null;
155
+ }
156
+
157
+ keys() {
158
+ return new Set(Object.keys(this.state.customers));
159
+ }
160
+
161
+ resolveKey(customerKey) {
162
+ if (!customerKey) {
163
+ return null;
164
+ }
165
+ if (this.state.customers[customerKey]) {
166
+ return customerKey;
167
+ }
168
+ return this.state.aliases[customerKey] || null;
169
+ }
170
+
171
+ async record(customerKey, entry, aliases = []) {
172
+ this.state.customers[customerKey] = {
173
+ ...entry,
174
+ customerKey,
175
+ aliases,
176
+ updatedAt: new Date().toISOString(),
177
+ };
178
+ for (const alias of aliases) {
179
+ if (alias && alias !== customerKey) {
180
+ this.state.aliases[alias] = customerKey;
181
+ }
182
+ }
183
+ this.state.updatedAt = new Date().toISOString();
184
+ await this.persistState();
185
+ }
186
+
187
+ async persistState() {
188
+ await mkdir(this.statesDir, { recursive: true });
189
+ const tempPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
190
+ await writeFile(tempPath, `${JSON.stringify(this.state, null, 2)}\n`, 'utf8');
191
+ await rename(tempPath, this.filePath);
192
+ }
193
+ }
194
+
195
+ export class NoopStateStore {
196
+ async load() {
197
+ return { version: 1, customers: {} };
198
+ }
199
+
200
+ has() {
201
+ return false;
202
+ }
203
+
204
+ hasAny() {
205
+ return false;
206
+ }
207
+
208
+ get() {
209
+ return null;
210
+ }
211
+
212
+ keys() {
213
+ return new Set();
214
+ }
215
+
216
+ async record() {}
217
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ function normalized(value) {
4
+ return String(value || '').replace(/\s+/g, ' ').trim();
5
+ }
6
+
7
+ function shortHash(parts) {
8
+ return createHash('sha1').update(parts.filter(Boolean).join('|')).digest('hex').slice(0, 16);
9
+ }
10
+
11
+ function collectExplicitIds(customer = {}) {
12
+ return [
13
+ customer.customerId,
14
+ customer.domKey,
15
+ customer.listItemKey,
16
+ customer.reactKey,
17
+ customer.dc,
18
+ customer.id,
19
+ customer.uid,
20
+ customer.geekId,
21
+ customer.cardId,
22
+ ]
23
+ .map(normalized)
24
+ .filter(Boolean);
25
+ }
26
+
27
+ export function createCustomerKey(customer = {}) {
28
+ const [explicitId] = collectExplicitIds(customer);
29
+
30
+ if (explicitId) {
31
+ return `id:${explicitId}`;
32
+ }
33
+
34
+ const name = normalized(customer.name);
35
+ const company = normalized(customer.company);
36
+ const educationText = normalized(customer.educationText);
37
+ const textSnippet = normalized(customer.textSnippet);
38
+
39
+ const stableSeed = [name, company, educationText].filter(Boolean);
40
+ const seed = stableSeed.length > 0 ? stableSeed.join('|') : textSnippet;
41
+
42
+ if (!seed) {
43
+ throw new Error('Cannot create customer key without id or identifying text');
44
+ }
45
+
46
+ const digest = createHash('sha1').update(seed).digest('hex').slice(0, 16);
47
+ return `hash:${digest}`;
48
+ }
49
+
50
+ export function createCustomerAliases(customer = {}) {
51
+ const aliases = new Set();
52
+ const name = normalized(customer.name);
53
+ const company = normalized(customer.company);
54
+ const educationText = normalized(customer.educationText);
55
+ const textSnippet = normalized(customer.textSnippet).slice(0, 120);
56
+
57
+ for (const explicitId of collectExplicitIds(customer)) {
58
+ aliases.add(`id:${explicitId}`);
59
+ }
60
+
61
+ try {
62
+ aliases.add(createCustomerKey(customer));
63
+ } catch {}
64
+
65
+ if (name && company) {
66
+ aliases.add(`nc:${shortHash([name, company])}`);
67
+ }
68
+ if (name && educationText) {
69
+ aliases.add(`ne:${shortHash([name, educationText])}`);
70
+ }
71
+ if (company && educationText) {
72
+ aliases.add(`ce:${shortHash([company, educationText])}`);
73
+ }
74
+ if (name && company && educationText) {
75
+ aliases.add(`nce:${shortHash([name, company, educationText])}`);
76
+ }
77
+ if (!company && name && textSnippet) {
78
+ aliases.add(`ns:${shortHash([name, textSnippet])}`);
79
+ }
80
+
81
+ return Array.from(aliases);
82
+ }