@besales/mcp 0.1.0 → 0.11.1

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 (94) hide show
  1. package/README.md +272 -17
  2. package/dist/auth/connection-store.d.ts +58 -0
  3. package/dist/auth/connection-store.js +208 -0
  4. package/dist/auth/connection-store.js.map +1 -0
  5. package/dist/auth/oauth-client.d.ts +27 -2
  6. package/dist/auth/oauth-client.js +62 -11
  7. package/dist/auth/oauth-client.js.map +1 -1
  8. package/dist/auth/session-workspace.d.ts +2 -0
  9. package/dist/auth/session-workspace.js +20 -0
  10. package/dist/auth/session-workspace.js.map +1 -0
  11. package/dist/auth/token-storage.d.ts +19 -5
  12. package/dist/auth/token-storage.js +11 -6
  13. package/dist/auth/token-storage.js.map +1 -1
  14. package/dist/cli.d.ts +2 -7
  15. package/dist/cli.js +111 -33
  16. package/dist/cli.js.map +1 -1
  17. package/dist/http/api-client.d.ts +4 -13
  18. package/dist/http/api-client.js +18 -18
  19. package/dist/http/api-client.js.map +1 -1
  20. package/dist/index.d.ts +8 -6
  21. package/dist/index.js +3 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/instructions/server-instructions.d.ts +15 -0
  24. package/dist/instructions/server-instructions.js +243 -0
  25. package/dist/instructions/server-instructions.js.map +1 -0
  26. package/dist/package-metadata.js +7 -1
  27. package/dist/package-metadata.js.map +1 -1
  28. package/dist/resources/concepts/feedback-sheets.md +77 -0
  29. package/dist/resources/concepts/sandbox.md +13 -0
  30. package/dist/resources/concepts/workbook-classification.md +241 -0
  31. package/dist/resources/docs/agent-behavior.md +393 -0
  32. package/dist/resources/docs/crm-integration.md +535 -0
  33. package/dist/resources/docs/files-and-uploads.md +295 -0
  34. package/dist/resources/docs/knowledge-base.md +521 -0
  35. package/dist/resources/docs/pipeline-builder.md +221 -0
  36. package/dist/resources/docs/pipeline-settings-deep.md +221 -0
  37. package/dist/resources/docs/platforms.md +513 -0
  38. package/dist/resources/docs/prompt-anatomy.md +298 -0
  39. package/dist/resources/docs/prompt-principles.md +289 -0
  40. package/dist/resources/registry.js +34 -12
  41. package/dist/resources/registry.js.map +1 -1
  42. package/dist/resources/workflows/compare-models.md +46 -0
  43. package/dist/resources/workflows/connect-crm-from-scratch.md +89 -0
  44. package/dist/resources/workflows/connect-datasource-from-scratch.md +92 -0
  45. package/dist/resources/workflows/extract-from-document.md +36 -0
  46. package/dist/resources/workflows/iterate-with-sandbox.md +31 -0
  47. package/dist/resources/workflows/platform-setup-from-scratch.md +113 -0
  48. package/dist/resources/workflows/production-readiness-check.md +41 -0
  49. package/dist/schemas/mcp-tools.json +2636 -182
  50. package/dist/server.js +2 -0
  51. package/dist/server.js.map +1 -1
  52. package/dist/tools/definitions/agent-design.d.ts +215 -0
  53. package/dist/tools/definitions/agent-design.js +643 -0
  54. package/dist/tools/definitions/agent-design.js.map +1 -0
  55. package/dist/tools/definitions/crm-platform.d.ts +211 -0
  56. package/dist/tools/definitions/crm-platform.js +1070 -0
  57. package/dist/tools/definitions/crm-platform.js.map +1 -0
  58. package/dist/tools/definitions/datasource.d.ts +40 -0
  59. package/dist/tools/definitions/datasource.js +196 -0
  60. package/dist/tools/definitions/datasource.js.map +1 -0
  61. package/dist/tools/definitions/knowledge.d.ts +215 -0
  62. package/dist/tools/definitions/knowledge.js +782 -0
  63. package/dist/tools/definitions/knowledge.js.map +1 -0
  64. package/dist/tools/definitions/model-comparison.d.ts +25 -0
  65. package/dist/tools/definitions/model-comparison.js +101 -0
  66. package/dist/tools/definitions/model-comparison.js.map +1 -0
  67. package/dist/tools/definitions/platform-setup.d.ts +412 -0
  68. package/dist/tools/definitions/platform-setup.js +738 -0
  69. package/dist/tools/definitions/platform-setup.js.map +1 -0
  70. package/dist/tools/definitions/session.d.ts +11 -0
  71. package/dist/tools/definitions/session.js +86 -0
  72. package/dist/tools/definitions/session.js.map +1 -0
  73. package/dist/tools/definitions/shared.d.ts +742 -0
  74. package/dist/tools/definitions/shared.js +773 -0
  75. package/dist/tools/definitions/shared.js.map +1 -0
  76. package/dist/tools/definitions.d.ts +873 -88
  77. package/dist/tools/definitions.js +14 -856
  78. package/dist/tools/definitions.js.map +1 -1
  79. package/dist/tools/registry.d.ts +3 -1
  80. package/dist/tools/registry.js +90 -11
  81. package/dist/tools/registry.js.map +1 -1
  82. package/dist/tools/result.d.ts +1 -1
  83. package/dist/tools/result.js +12 -4
  84. package/dist/tools/result.js.map +1 -1
  85. package/dist/utils/logger.js +2 -1
  86. package/dist/utils/logger.js.map +1 -1
  87. package/docs/host-setup.md +34 -15
  88. package/package.json +2 -2
  89. package/scripts/install-claude-desktop.js +89 -11
  90. package/scripts/mock-api-server.js +1 -1
  91. package/scripts/mock-credentials.js +49 -6
  92. package/dist/types/api-contract.gen.d.ts +0 -6975
  93. package/dist/types/api-contract.gen.js +0 -6
  94. package/dist/types/api-contract.gen.js.map +0 -1
package/README.md CHANGED
@@ -6,7 +6,13 @@ Status: public npm package candidate.
6
6
 
7
7
  ## Install
8
8
 
9
- Production users can run the MCP server with `npx`:
9
+ Requirements:
10
+
11
+ - Node.js 20+
12
+ - BeSales account with an active workspace
13
+ - Claude Desktop, Claude Code, Codex CLI, or another MCP-capable host
14
+
15
+ No global install is required. Production users can run the package with `npx`:
10
16
 
11
17
  ```bash
12
18
  npx -y @besales/mcp connect
@@ -14,6 +20,189 @@ npx -y @besales/mcp status
14
20
  npx -y @besales/mcp
15
21
  ```
16
22
 
23
+ `connect` opens the browser consent flow and stores an MCP API key plus the
24
+ selected workspace in the local keychain. Workspace-level tools use that
25
+ connected workspace by default, so normal users should not provide
26
+ `workspace_id` manually.
27
+
28
+ For production, do not set `BESALES_API_BASE_URL` or
29
+ `BESALES_OAUTH_AUTHORIZE_URL`. They default to:
30
+
31
+ - `BESALES_API_BASE_URL` → `https://core.besales.ai/api/v2` (backend API host —
32
+ used for MCP tool calls)
33
+ - `BESALES_OAUTH_AUTHORIZE_URL` →
34
+ `https://app.besales.ai/settings/mcp/consent` (frontend bridge page — opened
35
+ in the browser during `connect`; it mints a one-time setup ticket and forwards
36
+ to the backend consent page. After approval `connect` receives a one-time `code`
37
+ on the loopback callback and exchanges it with the PKCE `code_verifier` at
38
+ `/api/v2/auth/mcp-token` — the API key never travels in a URL)
39
+
40
+ For staging or local development override them per command:
41
+
42
+ ```bash
43
+ BESALES_API_BASE_URL=http://localhost:3000/api/v2 \
44
+ BESALES_OAUTH_AUTHORIZE_URL=http://localhost:5173/settings/mcp/consent \
45
+ npx -y @besales/mcp connect
46
+ ```
47
+
48
+ ## Configure Your MCP Host
49
+
50
+ After `connect`, add the server to your MCP host and restart the host.
51
+
52
+ Claude Desktop config:
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "besales": {
58
+ "command": "npx",
59
+ "args": ["-y", "@besales/mcp"]
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Claude Code:
66
+
67
+ ```bash
68
+ claude mcp add --scope user besales -- npx -y @besales/mcp
69
+ ```
70
+
71
+ Codex CLI:
72
+
73
+ ```bash
74
+ codex mcp add besales -- npx -y @besales/mcp
75
+ ```
76
+
77
+ Equivalent Codex config:
78
+
79
+ ```toml
80
+ [mcp_servers.besales]
81
+ command = "npx"
82
+ args = ["-y", "@besales/mcp"]
83
+ ```
84
+
85
+ For staging/local backends, include both backend and frontend bridge URLs in the
86
+ host env. For example:
87
+
88
+ ```toml
89
+ [mcp_servers.besales.env]
90
+ BESALES_API_BASE_URL = "http://localhost:3000/api/v2"
91
+ BESALES_OAUTH_AUTHORIZE_URL = "http://localhost:5173/settings/mcp/consent"
92
+ ```
93
+
94
+ ## Multiple Workspaces
95
+
96
+ `besales-mcp` can hold keys for several workspaces at once — even across
97
+ different accounts. Connect each one **once** (switch the account/workspace in
98
+ the browser before each `connect`); keys are cached in your OS keychain and
99
+ switching afterwards needs no browser.
100
+
101
+ ```bash
102
+ npx -y @besales/mcp connect # workspace A (browser logged into account A)
103
+ npx -y @besales/mcp connect # workspace B (re-login in the browser first)
104
+ npx -y @besales/mcp connections # list all connections, marks the active one
105
+ ```
106
+
107
+ Bind a host session to a specific workspace with `BESALES_WORKSPACE_ID` in that
108
+ server's `env` block — the cleanest "one session = one workspace" setup (one
109
+ project/profile per workspace):
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "besales": {
115
+ "command": "npx",
116
+ "args": ["-y", "@besales/mcp"],
117
+ "env": { "BESALES_WORKSPACE_ID": "<workspace-uuid>" }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ```toml
124
+ [mcp_servers.besales.env]
125
+ BESALES_WORKSPACE_ID = "<workspace-uuid>"
126
+ ```
127
+
128
+ Workspace selection precedence, per tool call:
129
+
130
+ 1. `BESALES_WORKSPACE_ID` (env) — authoritative; an explicit, mismatching
131
+ `workspace_id` argument is refused.
132
+ 2. an explicit `workspace_id` argument (only when the env var is not set).
133
+ 3. the session binding set by the `besales_workspace_use` tool (in-memory, this
134
+ session/process only — see below).
135
+ 4. the global active workspace set by `besales-mcp use <id>`.
136
+ 5. the only connected workspace (error if several are connected and none chosen).
137
+
138
+ `active` is global across all env-less sessions, and `BESALES_WORKSPACE_ID`
139
+ always overrides it. Get workspace ids from `besales-mcp connections`.
140
+
141
+ ### GUI hosts without per-session env (e.g. Claude Desktop)
142
+
143
+ When the host has no place to set `BESALES_WORKSPACE_ID` per session (you just
144
+ click "new session"), there are two ways to keep several workspaces apart.
145
+
146
+ **Recommended — one pinned named server per workspace (survives reconnects).**
147
+ Run once per workspace:
148
+
149
+ ```bash
150
+ yarn install:claude-desktop --name besales-acme --workspace <uuid-A>
151
+ yarn install:claude-desktop --name besales-globex --workspace <uuid-B>
152
+ ```
153
+
154
+ This adds env-pinned `mcpServers` entries (with a timestamped config backup);
155
+ restart Claude Desktop. Their tools appear namespaced —
156
+ `mcp__besales-acme__besales_*` (always workspace A) and
157
+ `mcp__besales-globex__besales_*` (always B). Because the pin lives in the host
158
+ config, it **survives MCP reconnects** and **never silently drifts** to the wrong
159
+ workspace. Both toolsets are visible in every session; you just address the one
160
+ you mean.
161
+
162
+ **Quick / in-session — `besales_workspace_use` (does NOT survive a reconnect).**
163
+ Ask the assistant to call **`besales_workspace_use <workspaceId>`** to bind the
164
+ current session in-memory. Each host session runs its own `besales-mcp` process,
165
+ so it is per-session and touches neither the global `active` nor other sessions.
166
+ Caveat: the binding is **in-memory only** — when the host respawns the server
167
+ (reconnect, which GUI hosts do often), it is **lost and falls back to the global
168
+ `active`**. Use it for a stable stretch, not as a durable lock; for that use the
169
+ pinned named servers above. `besales_workspace_current` shows the current binding
170
+ + source and lists connected workspaces; it is **refused** under a
171
+ `BESALES_WORKSPACE_ID` pin (env wins).
172
+
173
+ - `BESALES_WORKSPACE_ID` is read **once at process start** — exporting it after a
174
+ session is already open has no effect. Restart the session to re-pin via env.
175
+
176
+ ## First Smoke
177
+
178
+ Start a fresh host session after changing MCP config, then ask a natural
179
+ business request:
180
+
181
+ ```text
182
+ Use besales MCP to create an ICP for SaaS B2B sales automation.
183
+ ```
184
+
185
+ The assistant should discover and call the relevant `besales_*` tools itself.
186
+ Users should not mention `tool_search` and should not be asked for a
187
+ `workspace_id`.
188
+
189
+ Expected quick checks:
190
+
191
+ 1. `npx -y @besales/mcp status` shows connected credentials.
192
+ 2. The host shows the `besales` MCP server as connected.
193
+ 3. Direct tools such as `besales_icp_create` work.
194
+ 4. External flows use `*_get_instructions`, execute stages locally in the host,
195
+ then call the matching `*_submit` tool.
196
+
197
+ To switch workspace, connect each one once (see [Multiple Workspaces](#multiple-workspaces))
198
+ and either set `BESALES_WORKSPACE_ID` per host session or run
199
+ `npx -y @besales/mcp use <workspaceId>`. No browser re-auth is needed once a
200
+ workspace is connected.
201
+
202
+ If the host still asks for `workspace_id`, it is usually using an old cached
203
+ tool schema or an old package version. Restart the host/session and use
204
+ `@besales/mcp@latest` in the host args if needed.
205
+
17
206
  ## Development
18
207
 
19
208
  ```bash
@@ -56,40 +245,98 @@ yarn dev:mock:clear
56
245
  ## CLI
57
246
 
58
247
  ```bash
59
- node bin/besales-mcp.js connect
60
- node bin/besales-mcp.js status
61
- node bin/besales-mcp.js disconnect
62
- node bin/besales-mcp.js
248
+ node bin/besales-mcp.js connect # connect (or refresh) a workspace
249
+ node bin/besales-mcp.js connections # list connected workspaces (* = active)
250
+ node bin/besales-mcp.js use <workspaceId> # set the active (default) workspace
251
+ node bin/besales-mcp.js status # show the active workspace
252
+ node bin/besales-mcp.js disconnect # disconnect the active workspace only
253
+ node bin/besales-mcp.js disconnect <id> # disconnect one workspace
254
+ node bin/besales-mcp.js disconnect --all # disconnect every workspace
255
+ node bin/besales-mcp.js # start the MCP server
63
256
  ```
64
257
 
65
- `BESALES_API_BASE_URL` defaults to `https://app.besales.ai/api/v2`. For local
66
- development, set it to `http://localhost:3000/api/v2`.
258
+ `BESALES_API_BASE_URL` defaults to `https://core.besales.ai/api/v2` (backend).
259
+ `BESALES_OAUTH_AUTHORIZE_URL` defaults to
260
+ `https://app.besales.ai/settings/mcp/consent` (frontend bridge). For local
261
+ development point them at your local backend and frontend respectively.
67
262
 
68
263
  Tool calls require stored credentials. Run `node bin/besales-mcp.js connect`
69
- against a backend that implements `/api/v2/auth/mcp-connect`, or run
264
+ against a backend that implements the MCP OAuth endpoints
265
+ (`/api/v2/auth/mcp-connect*` + `/api/v2/auth/mcp-token`), or run
70
266
  `yarn dev:mock:seed` for mock-only smoke tests.
71
267
 
72
- For production users, do not set `BESALES_API_BASE_URL`. Run:
268
+ For production users, do not set `BESALES_API_BASE_URL` or
269
+ `BESALES_OAUTH_AUTHORIZE_URL`. Run:
73
270
 
74
271
  ```bash
75
272
  npx -y @besales/mcp connect
76
273
  ```
77
274
 
78
- The browser consent flow stores the selected workspace in the local keychain.
79
- Workspace-level tools default to that connected workspace, so users should not
80
- provide `workspace_id` manually. To switch workspace, run `besales-mcp
81
- disconnect`, switch/create the desired workspace in the web app if needed, then
82
- run `besales-mcp connect` again.
275
+ The browser consent flow stores the connected workspace key in the local
276
+ keychain. Workspace-level tools default to the session's workspace, so users
277
+ should not provide `workspace_id` manually. To work with several workspaces,
278
+ connect each once and pin sessions via `BESALES_WORKSPACE_ID` (or switch the
279
+ global default with `besales-mcp use <id>`) — see
280
+ [Multiple Workspaces](#multiple-workspaces).
281
+
282
+ ## Available tools (128 tools by category)
283
+
284
+ Counts are derived from `src/schemas/mcp-tools.json` (`summary.tools_by_category`)
285
+ and asserted in `src/tools/registry.spec.ts`:
286
+
287
+ | Category | Tools | Notes |
288
+ |---|---|---|
289
+ | ICP (profiles, dialogue import, analysis) | 5 | incl. `besales_icp_analysis_*` external pair |
290
+ | ICP — read (profile / segments / scenarios / samples) | 4 | `besales_icp_list` / `_get` / `_scenarios_list` / `_sample_messages` (read-only over existing GET `/api/v2/icp/*`) |
291
+ | Prompt generation / analysis / lint / patch sessions | 13 | `besales_prompt_*`, `besales_qa_generation_*` external pairs |
292
+ | Sandbox / simulation / test-scenario generation | 7 | `besales_sandbox_*`, `besales_simulation_*`, `besales_test_scenario_*` |
293
+ | Model comparison (bake-off) | 4 | `besales_model_catalog_list` + `besales_model_comparison_run_start` / `_get` / `_eval_submit` (on-demand multi-model prompt comparison) |
294
+ | Agent / router / triggers / behavior / facts | 13 | Platform Setup Bridge (incl. `besales_behavior_update`, `besales_pipeline_settings_update`, `besales_platform_settings_update`) + `besales_facts_*` |
295
+ | Knowledge — spaces | 6 | `besales_knowledge_space_*` |
296
+ | Knowledge — documents | 4 | incl. `besales_knowledge_document_upload` |
297
+ | Knowledge — Q&A | 7 | CRUD + semantic search + move |
298
+ | Knowledge — websites | 4 | crawl + Pinecone upsert |
299
+ | Knowledge — tables | 6 | Google Sheets link + reindex |
300
+ | Workbook ingestion (xlsx / Google Sheets) | 3 | `besales_workbook_*` |
301
+ | Feedback sheets (Google Sheets QA) | 5 | `besales_feedback_sheet_*` |
302
+ | Variables (unified) | 5 | `besales_variable_*` |
303
+ | CRM — pipelines, statuses, platform link | 11 | `besales_crm_pipeline_*`, `besales_platform_pipeline_*` |
304
+ | CRM — connection / field mapping / operators / activate | 7 | `besales_crm_create_*` (AmoCRM/Bitrix24 via browser-secrets), `besales_crm_field_mapping_*`, `besales_crm_operator_*`, `besales_crm_activate` (live token check + activate, deactivates other workspace CRMs) |
305
+ | Platform — provisioning / OAuth | 3 | `besales_platform_create_init`, `besales_platform_create_status`, `besales_platform_create_oauth_init` (Instagram/Avito) |
306
+ | Platform — clone | 3 | `besales_platform_clone_*` |
307
+ | Data sources (1С, категория «БД») | 9 | `besales_datasource_create_*` (Setup URL pattern), `_list` / `_health` / `_catalogs` / `_catalog_roles` / `_role_assign` / `_enable` / `_vectorize`. 1С — товарная БД через OData; чтение/запись — действия триггеров, не tools агента |
308
+ | Files | 3 | `besales_file_upload_request` + status + list |
309
+ | Context / overview / audit | 3 | `besales_workspace_overview`, `besales_platform_context_get`, `besales_audit_revert` |
310
+ | Session / workspace binding | 2 | `besales_workspace_use`, `besales_workspace_current` (in-memory per-session, local — no backend) |
311
+
312
+ Recent capabilities:
313
+
314
+ - **OAuth PKCE provisioning** — `besales_platform_create_oauth_init` opens the
315
+ authorization flow in the browser for Instagram/Avito so the OAuth code never
316
+ passes through the LLM.
317
+ - **CRM connection via browser-secrets** — `besales_crm_create_init` provisions
318
+ AmoCRM/Bitrix24 (and GetCourse/Telegram) using the Setup URL pattern, where the
319
+ user pastes credentials in the browser instead of through the chat.
320
+ - **Setup URL pattern** — `*_init` returns a one-time URL; the user completes
321
+ sensitive input in the browser and the host polls `*_status` until ready.
322
+ - **File upload** — two-step flow (`besales_file_upload_request` →
323
+ `besales_file_upload_status`) so binaries are uploaded directly, not via MCP.
324
+ - **Platform provisioning & clone** — create channels and clone an existing
325
+ platform configuration (`besales_platform_clone_preview` / `_execute`).
326
+ - **Knowledge ingestion** — full namespace/document/Q&A/website/table CRUD plus
327
+ workbook ingestion from xlsx and Google Sheets.
83
328
 
84
329
  ## Resources
85
330
 
86
- The package exposes 5 MCP concept resources:
331
+ The package exposes 7 MCP concept resources:
87
332
 
88
333
  - `besales://concepts/icp`
89
334
  - `besales://concepts/triggers`
90
335
  - `besales://concepts/sandbox`
91
336
  - `besales://concepts/handoff`
92
337
  - `besales://concepts/external-execution`
338
+ - `besales://concepts/feedback-sheets`
339
+ - `besales://concepts/workbook-classification`
93
340
 
94
341
  Inspect them locally:
95
342
 
@@ -97,7 +344,7 @@ Inspect them locally:
97
344
  yarn dlx @modelcontextprotocol/inspector node bin/besales-mcp.js
98
345
  ```
99
346
 
100
- The Inspector should show 31 tools and 5 resources.
347
+ The Inspector should show 128 tools and 23 resources.
101
348
 
102
349
  ## Claude Desktop
103
350
 
@@ -124,4 +371,12 @@ Claude Code, and Codex CLI are documented in
124
371
  - This package calls Animaly only through `ai-aniomaly` `/api/v2/*`.
125
372
  - Direct calls to `prompt-services` are intentionally out of scope.
126
373
 
127
- The package exposes 31 active MCP tools from `src/schemas/mcp-tools.json`.
374
+ The package exposes 128 active MCP tools from `src/schemas/mcp-tools.json`.
375
+
376
+ ### Token scopes
377
+
378
+ Read and PromptHub/ICP/sandbox operations are covered by the composite `mcp:*`
379
+ scope granted during `connect`. Setup mutations (knowledge ingestion, variables,
380
+ CRM pipelines, platform settings/provisioning, clone, the Platform Setup Bridge,
381
+ etc.) additionally require the `mcp:platform-setup` scope — a plain `mcp:*` key
382
+ on its own does not authorize setup mutations.
@@ -0,0 +1,58 @@
1
+ import { type CredentialStore } from './token-storage.js';
2
+ export declare const INDEX_ACCOUNT = "index";
3
+ export declare const ACTIVE_ACCOUNT = "active";
4
+ /** Имя keychain-account'а, хранящего api_key конкретного воркспейса. */
5
+ export declare function keyAccount(workspaceId: string): string;
6
+ /** Метаданные подключения (без секрета) — то, что лежит в `index`. */
7
+ export interface ConnectionMeta {
8
+ workspaceId: string;
9
+ keyPrefix: string;
10
+ connectedAt: string;
11
+ }
12
+ /**
13
+ * Ошибка резолва воркспейса: не подключён / неоднозначно / env-конфликт / нет ключа.
14
+ * Несёт КОНКРЕТНОЕ сообщение для агента и мапится в `result.ts` ДО `ApiClientError`,
15
+ * иначе схлопнулась бы в generic «Not connected. Run `besales-mcp connect`».
16
+ */
17
+ export declare class WorkspaceResolutionError extends Error {
18
+ constructor(message: string);
19
+ }
20
+ /**
21
+ * Мульти-воркспейс хранилище поверх keychain: несколько ключей (по одному на
22
+ * воркспейс), индекс метаданных без секретов и указатель `active`. Источник
23
+ * выбора воркспейса для сессии — см. resolveWorkspace() в tools/registry.ts.
24
+ */
25
+ export declare class ConnectionStore {
26
+ private readonly storage;
27
+ private legacyMigrationPromise?;
28
+ constructor(storage?: CredentialStore);
29
+ /**
30
+ * Одноразовая миграция legacy-пары (`api_key` + `workspace_id`) в новую схему.
31
+ * Мемоизирована per-instance; promise сбрасывается при ошибке, чтобы не залипнуть
32
+ * на rejected состоянии и повторить при следующем вызове.
33
+ */
34
+ migrateLegacy(): Promise<void>;
35
+ list(): Promise<ConnectionMeta[]>;
36
+ getKey(workspaceId: string): Promise<string | null>;
37
+ getActive(): Promise<string | null>;
38
+ setActive(workspaceId: string): Promise<void>;
39
+ /**
40
+ * Добавить/обновить подключение воркспейса. Атомарно: при сбое любой записи
41
+ * восстанавливает прежние key/index/active (refresh того же воркспейса не теряет
42
+ * рабочий ключ). Первое подключение становится `active`.
43
+ */
44
+ addOrReplace(workspaceId: string, apiKey: string): Promise<void>;
45
+ remove(workspaceId: string): Promise<void>;
46
+ /**
47
+ * Полная очистка (best-effort): удаляет ВСЕ `key:*` (включая osиротевшие после
48
+ * краша между записью ключа и индекса), `index`, `active` и legacy-пару. Каждое
49
+ * удаление независимо (Promise.allSettled) — частичный сбой не оставляет
50
+ * неудалённый секрет молча, а поднимает ошибку.
51
+ */
52
+ clear(): Promise<void>;
53
+ private runLegacyMigration;
54
+ private readIndex;
55
+ private writeIndex;
56
+ private restore;
57
+ private restoreAccount;
58
+ }
@@ -0,0 +1,208 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { maskApiKey } from '../utils/mask.js';
3
+ import { LEGACY_API_KEY_ACCOUNT, LEGACY_WORKSPACE_ID_ACCOUNT, TokenStorage, } from './token-storage.js';
4
+ export const INDEX_ACCOUNT = 'index';
5
+ export const ACTIVE_ACCOUNT = 'active';
6
+ const KEY_ACCOUNT_PREFIX = 'key:';
7
+ /** Имя keychain-account'а, хранящего api_key конкретного воркспейса. */
8
+ export function keyAccount(workspaceId) {
9
+ return `${KEY_ACCOUNT_PREFIX}${workspaceId}`;
10
+ }
11
+ /**
12
+ * Ошибка резолва воркспейса: не подключён / неоднозначно / env-конфликт / нет ключа.
13
+ * Несёт КОНКРЕТНОЕ сообщение для агента и мапится в `result.ts` ДО `ApiClientError`,
14
+ * иначе схлопнулась бы в generic «Not connected. Run `besales-mcp connect`».
15
+ */
16
+ export class WorkspaceResolutionError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'WorkspaceResolutionError';
20
+ }
21
+ }
22
+ /**
23
+ * Мульти-воркспейс хранилище поверх keychain: несколько ключей (по одному на
24
+ * воркспейс), индекс метаданных без секретов и указатель `active`. Источник
25
+ * выбора воркспейса для сессии — см. resolveWorkspace() в tools/registry.ts.
26
+ */
27
+ export class ConnectionStore {
28
+ storage;
29
+ legacyMigrationPromise;
30
+ constructor(storage = new TokenStorage()) {
31
+ this.storage = storage;
32
+ }
33
+ /**
34
+ * Одноразовая миграция legacy-пары (`api_key` + `workspace_id`) в новую схему.
35
+ * Мемоизирована per-instance; promise сбрасывается при ошибке, чтобы не залипнуть
36
+ * на rejected состоянии и повторить при следующем вызове.
37
+ */
38
+ async migrateLegacy() {
39
+ if (!this.legacyMigrationPromise) {
40
+ this.legacyMigrationPromise = this.runLegacyMigration().catch((error) => {
41
+ this.legacyMigrationPromise = undefined;
42
+ throw error;
43
+ });
44
+ }
45
+ return this.legacyMigrationPromise;
46
+ }
47
+ async list() {
48
+ return this.readIndex();
49
+ }
50
+ async getKey(workspaceId) {
51
+ return this.storage.get(keyAccount(workspaceId));
52
+ }
53
+ async getActive() {
54
+ return this.storage.get(ACTIVE_ACCOUNT);
55
+ }
56
+ async setActive(workspaceId) {
57
+ const index = await this.readIndex();
58
+ if (!index.some((connection) => connection.workspaceId === workspaceId)) {
59
+ throw new WorkspaceResolutionError(`Workspace ${workspaceId} is not connected. Run \`besales-mcp connect\` first.`);
60
+ }
61
+ await this.storage.set(ACTIVE_ACCOUNT, workspaceId);
62
+ }
63
+ /**
64
+ * Добавить/обновить подключение воркспейса. Атомарно: при сбое любой записи
65
+ * восстанавливает прежние key/index/active (refresh того же воркспейса не теряет
66
+ * рабочий ключ). Первое подключение становится `active`.
67
+ */
68
+ async addOrReplace(workspaceId, apiKey) {
69
+ const [prevKey, prevIndexRaw, prevActive] = await Promise.all([
70
+ this.storage.get(keyAccount(workspaceId)),
71
+ this.storage.get(INDEX_ACCOUNT),
72
+ this.storage.get(ACTIVE_ACCOUNT),
73
+ ]);
74
+ try {
75
+ await this.storage.set(keyAccount(workspaceId), apiKey);
76
+ const index = await this.readIndex();
77
+ const meta = {
78
+ workspaceId,
79
+ keyPrefix: maskApiKey(apiKey),
80
+ connectedAt: new Date().toISOString(),
81
+ };
82
+ const next = [
83
+ ...index.filter((connection) => connection.workspaceId !== workspaceId),
84
+ meta,
85
+ ];
86
+ await this.writeIndex(next);
87
+ if (!prevActive) {
88
+ await this.storage.set(ACTIVE_ACCOUNT, workspaceId);
89
+ }
90
+ }
91
+ catch (error) {
92
+ await this.restore(workspaceId, prevKey, prevIndexRaw, prevActive);
93
+ throw error;
94
+ }
95
+ }
96
+ async remove(workspaceId) {
97
+ // index пишем ДО удаления ключа: при сбое останется orphan-ключ (его подметёт
98
+ // clear()), а не висячая запись index без ключа.
99
+ const index = await this.readIndex();
100
+ await this.writeIndex(index.filter((connection) => connection.workspaceId !== workspaceId));
101
+ await this.storage.delete(keyAccount(workspaceId));
102
+ const active = await this.storage.get(ACTIVE_ACCOUNT);
103
+ if (active === workspaceId) {
104
+ await this.storage.delete(ACTIVE_ACCOUNT);
105
+ }
106
+ }
107
+ /**
108
+ * Полная очистка (best-effort): удаляет ВСЕ `key:*` (включая osиротевшие после
109
+ * краша между записью ключа и индекса), `index`, `active` и legacy-пару. Каждое
110
+ * удаление независимо (Promise.allSettled) — частичный сбой не оставляет
111
+ * неудалённый секрет молча, а поднимает ошибку.
112
+ */
113
+ async clear() {
114
+ const targets = new Set([
115
+ INDEX_ACCOUNT,
116
+ ACTIVE_ACCOUNT,
117
+ LEGACY_API_KEY_ACCOUNT,
118
+ LEGACY_WORKSPACE_ID_ACCOUNT,
119
+ ]);
120
+ for (const connection of await this.readIndex()) {
121
+ targets.add(keyAccount(connection.workspaceId));
122
+ }
123
+ try {
124
+ for (const account of await this.storage.accounts()) {
125
+ if (account.startsWith(KEY_ACCOUNT_PREFIX)) {
126
+ targets.add(account);
127
+ }
128
+ }
129
+ }
130
+ catch {
131
+ logger.warn('Could not enumerate keychain accounts during clear — using index only');
132
+ }
133
+ const results = await Promise.allSettled([...targets].map((account) => this.storage.delete(account)));
134
+ const failed = results.filter((result) => result.status === 'rejected').length;
135
+ if (failed > 0) {
136
+ throw new Error(`Failed to remove ${failed} keychain entr${failed === 1 ? 'y' : 'ies'} during clear`);
137
+ }
138
+ }
139
+ async runLegacyMigration() {
140
+ const existing = await this.readIndex();
141
+ if (existing.length > 0) {
142
+ return;
143
+ }
144
+ const [legacyKey, legacyWorkspaceId] = await Promise.all([
145
+ this.storage.get(LEGACY_API_KEY_ACCOUNT),
146
+ this.storage.get(LEGACY_WORKSPACE_ID_ACCOUNT),
147
+ ]);
148
+ if (!legacyKey || !legacyWorkspaceId) {
149
+ return;
150
+ }
151
+ // crash-safe: key + index + active пишутся ДО удаления legacy.
152
+ await this.storage.set(keyAccount(legacyWorkspaceId), legacyKey);
153
+ await this.writeIndex([
154
+ {
155
+ workspaceId: legacyWorkspaceId,
156
+ keyPrefix: maskApiKey(legacyKey),
157
+ connectedAt: new Date().toISOString(),
158
+ },
159
+ ]);
160
+ await this.storage.set(ACTIVE_ACCOUNT, legacyWorkspaceId);
161
+ await this.storage.delete(LEGACY_API_KEY_ACCOUNT);
162
+ await this.storage.delete(LEGACY_WORKSPACE_ID_ACCOUNT);
163
+ logger.info({ workspaceId: legacyWorkspaceId }, 'Migrated legacy credentials to multi-workspace store');
164
+ }
165
+ async readIndex() {
166
+ const raw = await this.storage.get(INDEX_ACCOUNT);
167
+ if (!raw) {
168
+ return [];
169
+ }
170
+ try {
171
+ const parsed = JSON.parse(raw);
172
+ if (!Array.isArray(parsed)) {
173
+ return [];
174
+ }
175
+ return parsed.filter(isConnectionMeta);
176
+ }
177
+ catch {
178
+ logger.warn('Connection index is corrupt — treating as empty');
179
+ return [];
180
+ }
181
+ }
182
+ async writeIndex(connections) {
183
+ await this.storage.set(INDEX_ACCOUNT, JSON.stringify(connections));
184
+ }
185
+ async restore(workspaceId, prevKey, prevIndexRaw, prevActive) {
186
+ await this.restoreAccount(keyAccount(workspaceId), prevKey);
187
+ await this.restoreAccount(INDEX_ACCOUNT, prevIndexRaw);
188
+ await this.restoreAccount(ACTIVE_ACCOUNT, prevActive);
189
+ }
190
+ async restoreAccount(account, previousValue) {
191
+ if (previousValue === null) {
192
+ await this.storage.delete(account);
193
+ }
194
+ else {
195
+ await this.storage.set(account, previousValue);
196
+ }
197
+ }
198
+ }
199
+ function isConnectionMeta(value) {
200
+ if (!value || typeof value !== 'object') {
201
+ return false;
202
+ }
203
+ const candidate = value;
204
+ return (typeof candidate.workspaceId === 'string' &&
205
+ typeof candidate.keyPrefix === 'string' &&
206
+ typeof candidate.connectedAt === 'string');
207
+ }
208
+ //# sourceMappingURL=connection-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-store.js","sourceRoot":"","sources":["../../src/auth/connection-store.ts"],"names":[],"mappings":"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"}