@codegame.dev/careerflow-mcp 1.1.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.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/dist/client.js +110 -0
- package/dist/client.js.map +1 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers.js +192 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.js +194 -0
- package/dist/index.js.map +1 -0
- package/dist/setup.js +296 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/admin.js +232 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/benefit-funds.js +243 -0
- package/dist/tools/benefit-funds.js.map +1 -0
- package/dist/tools/chat-extra.js +172 -0
- package/dist/tools/chat-extra.js.map +1 -0
- package/dist/tools/chat.js +125 -0
- package/dist/tools/chat.js.map +1 -0
- package/dist/tools/cms.js +145 -0
- package/dist/tools/cms.js.map +1 -0
- package/dist/tools/crm-admin.js +269 -0
- package/dist/tools/crm-admin.js.map +1 -0
- package/dist/tools/crm.js +632 -0
- package/dist/tools/crm.js.map +1 -0
- package/dist/tools/dev-tickets.js +94 -0
- package/dist/tools/dev-tickets.js.map +1 -0
- package/dist/tools/forms.js +105 -0
- package/dist/tools/forms.js.map +1 -0
- package/dist/tools/hr.js +517 -0
- package/dist/tools/hr.js.map +1 -0
- package/dist/tools/kanban-extra.js +403 -0
- package/dist/tools/kanban-extra.js.map +1 -0
- package/dist/tools/misc.js +159 -0
- package/dist/tools/misc.js.map +1 -0
- package/dist/tools/projects.js +138 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/tasks.js +342 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/time.js +129 -0
- package/dist/tools/time.js.map +1 -0
- package/dist/tools/workplace.js +110 -0
- package/dist/tools/workplace.js.map +1 -0
- package/dist/tools/workspace.js +199 -0
- package/dist/tools/workspace.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shahab Yousefi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# CareerFlow MCP Server
|
|
2
|
+
|
|
3
|
+
Connect an AI assistant to [CareerFlow](https://careerflow.codegame.dev) so it can read and
|
|
4
|
+
manage your tasks, track your time, message colleagues, work CRM deals, handle
|
|
5
|
+
HR requests, and more — on your behalf.
|
|
6
|
+
|
|
7
|
+
Works with any MCP-capable client — Claude Code, Claude Desktop, and others.
|
|
8
|
+
|
|
9
|
+
> The server holds no privileges of its own. It calls the same REST API the web
|
|
10
|
+
> app uses, authenticated as you, so your existing role still decides what it can
|
|
11
|
+
> do — and the token's permissions narrow that further.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
### The one-command way
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @codegame.dev/careerflow-mcp init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The setup wizard asks for your CareerFlow host and access token, detects which
|
|
24
|
+
MCP clients you have installed, and writes the configuration into Claude Code,
|
|
25
|
+
Claude Desktop, ZCode (Z.ai), or Cursor for you — or prints a ready-to-paste
|
|
26
|
+
JSON block for any other client. Token input is hidden as you type; it is never
|
|
27
|
+
passed on the command line.
|
|
28
|
+
|
|
29
|
+
Flags prefill the answers, so an onboarding page can print a one-liner per
|
|
30
|
+
company or client — only the token is still asked for (hidden):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @codegame.dev/careerflow-mcp init --base-url https://acme.careerflow.example --name acme --client claude-code
|
|
34
|
+
npx @codegame.dev/careerflow-mcp init --base-url https://acme.careerflow.example --name acme --company-id 24 --client zcode
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- `--base-url` — the CareerFlow host, so it is never typed by hand
|
|
38
|
+
- `--name` — the server's name in the client config (default `careerflow`);
|
|
39
|
+
use one name per company or install, e.g. `acme`, `acme-staging`
|
|
40
|
+
- `--company-id` — optional default company
|
|
41
|
+
- `--client` — `claude-code`, `claude-desktop`, `zcode`, `cursor`, or `other`
|
|
42
|
+
|
|
43
|
+
None of these flags is secret, so the one-liner is safe to publish. The access
|
|
44
|
+
token is never accepted as a flag — argv is readable by other processes on the
|
|
45
|
+
machine — so the wizard always prompts for it with hidden input.
|
|
46
|
+
|
|
47
|
+
### The manual way
|
|
48
|
+
|
|
49
|
+
1. Create an access token.
|
|
50
|
+
|
|
51
|
+
In CareerFlow: **Settings → Access Tokens → Create access token**.
|
|
52
|
+
|
|
53
|
+
Give it a name, an expiry, and only the permissions the assistant needs.
|
|
54
|
+
|
|
55
|
+
> **The token is shown once.** It is stored hashed and cannot be recovered
|
|
56
|
+
> afterwards. Copy it before closing the dialog.
|
|
57
|
+
|
|
58
|
+
2. Add it to your client.
|
|
59
|
+
|
|
60
|
+
**Claude Code**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add careerflow \
|
|
64
|
+
--env CAREERFLOW_BASE_URL=https://your-careerflow-host \
|
|
65
|
+
--env CAREERFLOW_TOKEN=cf_pat_xxxxxxxx \
|
|
66
|
+
--env CAREERFLOW_COMPANY_ID=24 \
|
|
67
|
+
-- npx -y @codegame.dev/careerflow-mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"careerflow": {
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["-y", "@codegame.dev/careerflow-mcp"],
|
|
78
|
+
"env": {
|
|
79
|
+
"CAREERFLOW_BASE_URL": "https://your-careerflow-host",
|
|
80
|
+
"CAREERFLOW_TOKEN": "cf_pat_xxxxxxxx",
|
|
81
|
+
"CAREERFLOW_COMPANY_ID": "24"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Config file locations:
|
|
89
|
+
- macOS — `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
90
|
+
- Windows — `%APPDATA%\Claude\claude_desktop_config.json`
|
|
91
|
+
- Linux — `~/.config/Claude/claude_desktop_config.json`
|
|
92
|
+
|
|
93
|
+
**ZCode (Z.ai)** — add to `~/.zcode/cli/config.json` (note the nested
|
|
94
|
+
`mcp.servers`, different from most clients' `mcpServers`):
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcp": {
|
|
99
|
+
"servers": {
|
|
100
|
+
"careerflow": {
|
|
101
|
+
"type": "stdio",
|
|
102
|
+
"command": "npx",
|
|
103
|
+
"args": ["-y", "@codegame.dev/careerflow-mcp"],
|
|
104
|
+
"env": {
|
|
105
|
+
"CAREERFLOW_BASE_URL": "https://your-careerflow-host",
|
|
106
|
+
"CAREERFLOW_TOKEN": "cf_pat_xxxxxxxx",
|
|
107
|
+
"CAREERFLOW_COMPANY_ID": "24"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For a project-shared server use `<repo>/.zcode/config.json` instead — but the
|
|
116
|
+
token would then sit in a versioned file; prefer user scope.
|
|
117
|
+
|
|
118
|
+
Restart the client. Ask it *"what are my tasks?"* to confirm the connection.
|
|
119
|
+
|
|
120
|
+
### Configuration
|
|
121
|
+
|
|
122
|
+
| Variable | Required | Description |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `CAREERFLOW_BASE_URL` | yes | API root, e.g. `https://api.example.com` |
|
|
125
|
+
| `CAREERFLOW_TOKEN` | yes | Your personal access token (`cf_pat_…`) |
|
|
126
|
+
| `CAREERFLOW_COMPANY_ID` | no | Default company, so tools need no `company_id` per call |
|
|
127
|
+
| `CAREERFLOW_TIMEOUT_MS` | no | Request timeout, default `30000` |
|
|
128
|
+
| `CAREERFLOW_GROUPS` | no | Register only these comma-separated tool groups |
|
|
129
|
+
| `CAREERFLOW_EXCLUDE_GROUPS` | no | Register everything except these comma-separated groups |
|
|
130
|
+
|
|
131
|
+
The token is read only from the environment, never from command-line arguments —
|
|
132
|
+
argv is readable by other processes on the same machine.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## What you can ask for
|
|
137
|
+
|
|
138
|
+
**Daily work**
|
|
139
|
+
- "What am I working on today?" · "Anything overdue?"
|
|
140
|
+
- "Mark the login-bug task done."
|
|
141
|
+
- "Start a timer on task 412." · "Log 45 minutes on the report task."
|
|
142
|
+
- "Add a comment on task 88 saying the API is now deployed."
|
|
143
|
+
- "Add a checklist to task 12 with three items." · "Attach this link to task 40."
|
|
144
|
+
|
|
145
|
+
**Supervising a team**
|
|
146
|
+
- "What is Sara working on?" · "Show everything pending review."
|
|
147
|
+
- "Create a task 'Prepare Q3 report' in Sprint 12, assign it to Ali, due Friday, high priority."
|
|
148
|
+
- "Move task 91 to Done." · "Accept task 77 and score it 4.5."
|
|
149
|
+
- "Create a project called 'Website Redesign' and add Sara as manager."
|
|
150
|
+
|
|
151
|
+
**Sales & CRM**
|
|
152
|
+
- "Show my open deals." · "Create a lead for Acme Corp from the website form."
|
|
153
|
+
- "Move the Acme deal to Negotiation and set the amount to $12,000."
|
|
154
|
+
- "Log a call with the Beta Inc contact." · "What's in my pipeline this month?"
|
|
155
|
+
|
|
156
|
+
**HR & people ops**
|
|
157
|
+
- "Show my latest payroll slip." · "Accept my performance review."
|
|
158
|
+
- "List open job applications for the Backend Engineer posting."
|
|
159
|
+
- "Am I registered for the current loan lottery?" · "Check in my mood: 4, feeling good today."
|
|
160
|
+
- "Reserve lunch for tomorrow."
|
|
161
|
+
|
|
162
|
+
**Around the work**
|
|
163
|
+
- "Any new announcements?" · "Message Reza that the deploy finished."
|
|
164
|
+
- "Create a chat group for the launch team with Sara, Ali, and Reza."
|
|
165
|
+
- "Open a support ticket about the slow dashboard load."
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Tools
|
|
170
|
+
|
|
171
|
+
164 tools. The everyday surface
|
|
172
|
+
([only part of it can be registered](#registering-only-the-tool-groups-you-need)):
|
|
173
|
+
|
|
174
|
+
| Group | Tools |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| **Identity & structure** | `whoami`, `list_companies`, `list_projects`, `list_boards`, `list_project_members`, `find_user` |
|
|
177
|
+
| **Tasks** | `list_my_tasks`, `search_tasks`, `get_task`, `create_task`, `update_task`, `complete_task`, `assign_task`, `move_task`, `review_task`, `list_task_comments`, `add_task_comment` |
|
|
178
|
+
| **Kanban (boards, labels, checklists, attachments)** | `create_board`, `edit_board`, `archive_board`, `list_labels`, `create_label`, `delete_label`, `add_task_checklist`, `add_checklist_item`, `complete_checklist_item`, `delete_checklist`, `add_task_link_attachment`, `delete_task_attachment`, `archive_task`, `duplicate_task`, `list_recurring_tasks`, `create_recurring_task`, `delete_recurring_task`, `list_kanban_activity` |
|
|
179
|
+
| **Projects** | `create_project`, `update_project`, `archive_project`, `project_stats`, `invite_project_member`, `authorize_project_member` |
|
|
180
|
+
| **Time** | `start_timer`, `stop_timer`, `log_time`, `list_task_time` |
|
|
181
|
+
| **Chat** | `list_chats`, `read_messages`, `send_message`, `create_chat_group`, `edit_chat_group`, `manage_chat_group_members`, `list_chat_group_members`, `leave_chat_group`, `mute_chat`, `edit_chat_message`, `delete_chat_message`, `mark_message_seen` |
|
|
182
|
+
| **Workplace** | `list_announcements`, `list_surveys`, `list_my_attendance` |
|
|
183
|
+
| **Company & user admin** | `create_user`, `update_user`, `set_user_status`, `set_user_role`, `get_user`, `list_roles`, `get_organization_chart`, `get_company`, `list_organization_units` |
|
|
184
|
+
| **CRM — contacts, deals, activities** | `crm_search_contacts`, `crm_get_contact`, `crm_create_contact`, `crm_update_contact`, `crm_delete_contact`, `crm_contact_timeline`, `crm_search_deals`, `crm_get_deal`, `crm_create_deal`, `crm_update_deal`, `crm_delete_deal`, `crm_deal_stage_summary`, `crm_add_deal_next_step`, `crm_list_deal_line_items`, `crm_add_deal_line_item`, `crm_delete_deal_line_item`, `crm_search_activities`, `crm_create_activity`, `crm_complete_activity`, `crm_delete_activity`, `crm_dashboard_summary`, `crm_dashboard_pipeline` |
|
|
185
|
+
| **CRM — pipelines & config** | `crm_list_pipelines`, `crm_create_pipeline`, `crm_delete_pipeline`, `crm_list_stages`, `crm_create_stage`, `crm_delete_stage`, `crm_list_lead_sources`, `crm_create_lead_source`, `crm_list_tags`, `crm_create_tag`, `crm_assign_tag`, `crm_list_custom_fields`, `crm_create_custom_field`, `crm_settings` |
|
|
186
|
+
| **HR — payroll, discipline, careers, onboarding, cafeteria** | `list_my_payrolls`, `respond_to_payroll`, `list_my_performance_reviews`, `respond_to_performance_review`, `list_disciplinary_notes`, `estimate_disciplinary_penalty`, `create_disciplinary_note`, `list_administrative_notes`, `get_administrative_note`, `create_administrative_note`, `list_careers`, `list_career_requests`, `update_career_request_status`, `get_onboarding_checklist`, `set_onboarding_stage`, `list_food_schedules`, `reserve_food`, `list_my_food_reservations`, `mood_checkin` |
|
|
187
|
+
| **Benefit fund (loans & lotteries)** | `benefit_fund_my_dashboard`, `benefit_fund_hr_dashboard`, `list_benefit_fund_loan_types`, `list_benefit_fund_loans`, `request_benefit_fund_loan`, `update_benefit_fund_loan_status`, `pay_benefit_fund_loan_installment`, `list_benefit_fund_lotteries`, `register_for_lottery`, `respond_to_lottery_win`, `benefit_fund_settings`, `list_benefit_fund_membership_requests`, `request_benefit_fund_membership`, `review_benefit_fund_membership_request` |
|
|
188
|
+
| **Forms** | `list_forms`, `get_form`, `list_form_submissions`, `update_form_submission_status`, `message_form_submission` |
|
|
189
|
+
| **CMS** | `list_posts`, `get_post`, `create_post`, `update_post`, `delete_post`, `list_custom_pages`, `get_custom_page` |
|
|
190
|
+
| **Dev tickets (support)** | `list_dev_tickets`, `get_dev_ticket`, `create_dev_ticket`, `reply_to_dev_ticket` |
|
|
191
|
+
| **Misc (addresses, surveys, announcements, billing)** | `list_my_addresses`, `add_address`, `create_survey`, `delete_survey`, `list_survey_answers`, `answer_survey`, `create_announcement`, `delete_announcement`, `list_transactions`, `list_payment_orders` |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Registering only the tool groups you need
|
|
196
|
+
|
|
197
|
+
Every registered tool's schema lands in the model's context, so a client that
|
|
198
|
+
loads all 164 tools spends real tokens on descriptions before you ask anything.
|
|
199
|
+
If the assistant only works one domain — a sales assistant, a task bot —
|
|
200
|
+
register just that slice:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"mcpServers": {
|
|
205
|
+
"careerflow": {
|
|
206
|
+
"command": "npx",
|
|
207
|
+
"args": ["-y", "@codegame.dev/careerflow-mcp"],
|
|
208
|
+
"env": {
|
|
209
|
+
"CAREERFLOW_BASE_URL": "https://your-careerflow-host",
|
|
210
|
+
"CAREERFLOW_TOKEN": "cf_pat_xxxxxxxx",
|
|
211
|
+
"CAREERFLOW_GROUPS": "workspace,tasks,time"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Group names (matching rows in the table above, `crm` covering both CRM rows,
|
|
219
|
+
`chat` both chat modules):
|
|
220
|
+
|
|
221
|
+
`workspace`, `tasks`, `kanban`, `projects`, `time`, `chat`, `workplace`,
|
|
222
|
+
`admin`, `crm`, `hr`, `benefit-funds`, `forms`, `cms`, `dev-tickets`, `misc`
|
|
223
|
+
|
|
224
|
+
`CAREERFLOW_GROUPS` is an allowlist; `CAREERFLOW_EXCLUDE_GROUPS` removes the
|
|
225
|
+
named groups from the full set. An unknown name fails at startup with the valid
|
|
226
|
+
list. Group filtering is a context-size control only — the token's scopes still
|
|
227
|
+
decide what actually works.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Permissions
|
|
232
|
+
|
|
233
|
+
Each token carries an explicit permission list. A tool call that needs a
|
|
234
|
+
permission the token lacks fails with a message naming the missing one — create a
|
|
235
|
+
new token with it added. The full list is shown on the token creation screen in
|
|
236
|
+
**Settings → Access Tokens → Create access token**; it's summarized here too.
|
|
237
|
+
|
|
238
|
+
Every scope comes in a `:read`/`:write` pair; some also accept a `:own` suffix
|
|
239
|
+
(e.g. `tasks:write:own`) that narrows the write to records the token's owner
|
|
240
|
+
created, owns, or is assigned to — ask CareerFlow support or check the token
|
|
241
|
+
creation screen if you want that narrower grant.
|
|
242
|
+
|
|
243
|
+
| Group | Scopes | Covers |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| Personal | `profile`, `files` | your own profile, sessions, addresses, onboarding; file/image/video uploads |
|
|
246
|
+
| Work | `tasks`, `projects`, `time`, `chat` | kanban boards/tasks/checklists/labels, projects & membership, time tracking, chat & groups |
|
|
247
|
+
| CRM | `crm_contacts`, `crm_deals`, `crm_activities`, `crm_settings` | contacts/leads, deals, activities, and CRM configuration (pipelines, stages, tags, custom fields) |
|
|
248
|
+
| Company | `company`, `users`, `roles`, `attendance` | company profile & org chart, user admin, roles, attendance |
|
|
249
|
+
| HR | `surveys`, `announcements`, `forms`, `payroll`, `performance`, `benefit_fund`, `notes`, `careers` | surveys, announcements, form builder, payroll/performance slips, benefit fund loans & lotteries, disciplinary/administrative notes, recruiting |
|
|
250
|
+
| Content | `cms` | posts, custom pages, categories |
|
|
251
|
+
| Billing | `billing`, `dev_tickets` | payment/product orders, transactions, support tickets |
|
|
252
|
+
| Platform (staff-only) | `admin` | charts, logs, and other platform-admin surfaces — only issuable to CareerFlow staff accounts |
|
|
253
|
+
|
|
254
|
+
Grant the narrowest set that does the job. A read-only assistant needs only
|
|
255
|
+
`profile:read`, `tasks:read`, and `projects:read`.
|
|
256
|
+
|
|
257
|
+
### Security notes
|
|
258
|
+
|
|
259
|
+
- A token acts as you, limited to its permissions. Treat it like a password.
|
|
260
|
+
- It cannot exceed your own role — the API enforces the same checks as the web app.
|
|
261
|
+
- A token cannot create, edit, or manage another token — token management is a
|
|
262
|
+
session-only action in the web app, so a leaked token cannot widen itself.
|
|
263
|
+
- Revoke any token you no longer use: **Settings → Access Tokens → Revoke**.
|
|
264
|
+
Revocation takes effect immediately.
|
|
265
|
+
- Several tools deliver something real and cannot be undone from inside the
|
|
266
|
+
tool call — `send_message`, `create_administrative_note`,
|
|
267
|
+
`message_form_submission` send to real people; `create_user`,
|
|
268
|
+
`set_user_status`, `set_user_role` change real accounts;
|
|
269
|
+
`request_benefit_fund_loan`, `update_benefit_fund_loan_status`,
|
|
270
|
+
`pay_benefit_fund_loan_installment`, and `respond_to_lottery_win` touch real
|
|
271
|
+
money. Their descriptions ask the assistant to confirm details with you
|
|
272
|
+
first — grant the underlying `:write` scope only if you want that
|
|
273
|
+
capability at all.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Running from source
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
git clone https://gitlab.com/dev.codegames/careerflow-mcp
|
|
281
|
+
cd careerflow-mcp
|
|
282
|
+
npm install
|
|
283
|
+
npm run build
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Point your client at the built entry point:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"mcpServers": {
|
|
291
|
+
"careerflow": {
|
|
292
|
+
"command": "node",
|
|
293
|
+
"args": ["/absolute/path/to/careerflow-mcp/dist/index.js"],
|
|
294
|
+
"env": { "CAREERFLOW_BASE_URL": "…", "CAREERFLOW_TOKEN": "…" }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Docker
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
docker build -t careerflow-mcp .
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"mcpServers": {
|
|
309
|
+
"careerflow": {
|
|
310
|
+
"command": "docker",
|
|
311
|
+
"args": [
|
|
312
|
+
"run", "-i", "--rm",
|
|
313
|
+
"-e", "CAREERFLOW_BASE_URL",
|
|
314
|
+
"-e", "CAREERFLOW_TOKEN",
|
|
315
|
+
"-e", "CAREERFLOW_COMPANY_ID",
|
|
316
|
+
"careerflow-mcp"
|
|
317
|
+
],
|
|
318
|
+
"env": {
|
|
319
|
+
"CAREERFLOW_BASE_URL": "https://your-careerflow-host",
|
|
320
|
+
"CAREERFLOW_TOKEN": "cf_pat_xxxxxxxx",
|
|
321
|
+
"CAREERFLOW_COMPANY_ID": "24"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`-i` is required: the server speaks MCP over stdin/stdout.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Troubleshooting
|
|
333
|
+
|
|
334
|
+
**"CareerFlow rejected the access token"** — the token is revoked, expired, or
|
|
335
|
+
mistyped. Check it in Settings → Access Tokens, and create a new one if needed.
|
|
336
|
+
|
|
337
|
+
**"The access token does not carry the scope this action needs"** — the token is
|
|
338
|
+
missing a permission. Create a new one with that permission added.
|
|
339
|
+
|
|
340
|
+
**"Company ID is not valid"** — your role does not permit that read, or
|
|
341
|
+
`CAREERFLOW_COMPANY_ID` points at a company you do not belong to. Run `whoami` to
|
|
342
|
+
see which company you are in.
|
|
343
|
+
|
|
344
|
+
**Tools do not appear** — restart the client after editing its config. Verify the
|
|
345
|
+
server starts on its own:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
CAREERFLOW_TOKEN=cf_pat_xxx CAREERFLOW_BASE_URL=https://host npx @codegame.dev/careerflow-mcp
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
It should print `careerflow-mcp connected (api: …)` to stderr and wait. Ctrl-C to exit.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Requirements
|
|
356
|
+
|
|
357
|
+
- Node.js 18 or newer
|
|
358
|
+
- A CareerFlow instance with personal access tokens enabled (migration 99 applied)
|
|
359
|
+
|
|
360
|
+
## Development
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
npm run dev # tsc --watch
|
|
364
|
+
npm run typecheck # types only
|
|
365
|
+
npm run format # prettier
|
|
366
|
+
npm run smoke # boot the built server and verify tool registration
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Publishing
|
|
370
|
+
|
|
371
|
+
`npm run prepublishOnly` (typecheck, build, smoke) runs automatically on
|
|
372
|
+
`npm publish`. On GitLab, the `publish` job in `.gitlab-ci.yml` publishes when
|
|
373
|
+
a `v*` tag is pushed and the job is confirmed manually; it needs `NPM_TOKEN`
|
|
374
|
+
(set it as a masked CI/CD variable under Settings > CI/CD > Variables):
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
git tag v1.1.0 && git push origin v1.1.0
|
|
378
|
+
# then confirm the `publish` job in CI/CD > Pipelines
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Or publish from a machine that is already verified with npm: `npm publish`.
|
|
382
|
+
|
|
383
|
+
### Compatibility
|
|
384
|
+
|
|
385
|
+
This client tracks the CareerFlow REST API. Behaviours it deliberately works
|
|
386
|
+
around, worth knowing before changing a tool:
|
|
387
|
+
|
|
388
|
+
- `kanbans/tasks/edit`, `crm/deals/edit`, and `users/edit` all replace the
|
|
389
|
+
whole row — a field left out falls back to a default (or null) rather than
|
|
390
|
+
keeping its current value. `update_task`, `crm_update_deal`, and
|
|
391
|
+
`update_user` each read the record first and merge onto it, so a partial
|
|
392
|
+
change cannot silently clear untouched fields. Apply the same read-then-merge
|
|
393
|
+
pattern to any other endpoint discovered to have this behavior.
|
|
394
|
+
- Several endpoints reject an unscoped company-wide read unless the caller holds
|
|
395
|
+
an admin role, so tools pass the caller's own user id to mean "mine".
|
|
396
|
+
- A few `search` endpoints require a filter the API otherwise has no "all"
|
|
397
|
+
value for — e.g. `careers/requests/search` requires `status`. `list_career_requests`
|
|
398
|
+
reflects that by making the field required rather than optional.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
errors;
|
|
4
|
+
constructor(message, status, errors) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.errors = errors;
|
|
8
|
+
this.name = "ApiError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Flattens the API's nested error map into one readable line, so a tool result
|
|
13
|
+
* tells the caller which field was rejected and why instead of "request failed".
|
|
14
|
+
*/
|
|
15
|
+
function describeErrors(body) {
|
|
16
|
+
if (body.error?.message) {
|
|
17
|
+
return body.error.message;
|
|
18
|
+
}
|
|
19
|
+
const errors = body.errors;
|
|
20
|
+
if (!errors || Array.isArray(errors)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const [field, reasons] of Object.entries(errors)) {
|
|
25
|
+
if (reasons && typeof reasons === "object") {
|
|
26
|
+
for (const reason of Object.values(reasons)) {
|
|
27
|
+
parts.push(`${field}: ${reason}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return parts.length ? parts.join("; ") : null;
|
|
32
|
+
}
|
|
33
|
+
export class CareerFlowClient {
|
|
34
|
+
config;
|
|
35
|
+
constructor(config) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
get defaultCompanyId() {
|
|
39
|
+
return this.config.companyId;
|
|
40
|
+
}
|
|
41
|
+
async get(path, query = {}) {
|
|
42
|
+
const url = new URL(`${this.config.baseUrl}/${path}`);
|
|
43
|
+
for (const [key, value] of Object.entries(query)) {
|
|
44
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
45
|
+
url.searchParams.set(key, String(value));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.request(url.toString(), { method: "GET" });
|
|
49
|
+
}
|
|
50
|
+
async post(path, body = {}) {
|
|
51
|
+
const url = `${this.config.baseUrl}/${path}`;
|
|
52
|
+
const payload = {};
|
|
53
|
+
for (const [key, value] of Object.entries(body)) {
|
|
54
|
+
if (value !== undefined && value !== null) {
|
|
55
|
+
payload[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return this.request(url, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify(payload)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async request(url, init) {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetch(url, {
|
|
70
|
+
...init,
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
headers: {
|
|
73
|
+
...(init.headers ?? {}),
|
|
74
|
+
token: this.config.token,
|
|
75
|
+
Accept: "application/json"
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
81
|
+
throw new ApiError(`Request timed out after ${this.config.timeoutMs}ms`, 408);
|
|
82
|
+
}
|
|
83
|
+
throw new ApiError(`Could not reach CareerFlow at ${this.config.baseUrl}: ${error.message}`, 0);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
const text = await response.text();
|
|
89
|
+
let body;
|
|
90
|
+
try {
|
|
91
|
+
body = text ? JSON.parse(text) : { successful: false };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new ApiError(`CareerFlow returned a non-JSON response (HTTP ${response.status})`, response.status);
|
|
95
|
+
}
|
|
96
|
+
if (response.status === 401) {
|
|
97
|
+
throw new ApiError("CareerFlow rejected the access token. It may be revoked, expired, or mistyped.", 401, body.errors);
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 403) {
|
|
100
|
+
throw new ApiError(describeErrors(body) ??
|
|
101
|
+
"The access token does not carry the scope this action needs. " +
|
|
102
|
+
"Re-create it in Settings > Access Tokens with the required scope.", 403, body.errors);
|
|
103
|
+
}
|
|
104
|
+
if (!response.ok || !body.successful) {
|
|
105
|
+
throw new ApiError(describeErrors(body) ?? `CareerFlow request failed (HTTP ${response.status})`, response.status, body.errors);
|
|
106
|
+
}
|
|
107
|
+
return body;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAmBA,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGxB;IACA;IAHV,YACC,OAAe,EACN,MAAc,EACd,MAAgB;QAEzB,KAAK,CAAC,OAAO,CAAC,CAAA;QAHL,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAU;QAGzB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;IACvB,CAAC;CACD;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,IAAiB;IACxC,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAA;IAC1B,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IAC1B,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAA;IACZ,CAAC;IACD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC5C,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,OAAiC,CAAC,EAAE,CAAC;gBACvE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,MAAM,EAAE,CAAC,CAAA;YAClC,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC9C,CAAC;AAED,MAAM,OAAO,gBAAgB;IACC;IAA7B,YAA6B,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAE/C,IAAI,gBAAgB;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAA;IAC7B,CAAC;IAED,KAAK,CAAC,GAAG,CAAc,IAAY,EAAE,QAAiC,EAAE;QACvE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC,CAAA;QACrD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAClD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBAC3D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;YACzC,CAAC;QACF,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAI,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED,KAAK,CAAC,IAAI,CAAc,IAAY,EAAE,OAAgC,EAAE;QACvE,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,EAAE,CAAA;QAC5C,MAAM,OAAO,GAA4B,EAAE,CAAA;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACjD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC3C,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;YACrB,CAAC;QACF,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAI,GAAG,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAC7B,CAAC,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,GAAW,EAAE,IAAiB;QACtD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAEzE,IAAI,QAAkB,CAAA;QACtB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,GAAG,IAAI;gBACP,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,OAAO,EAAE;oBACR,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;oBACvB,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;oBACxB,MAAM,EAAE,kBAAkB;iBAC1B;aACD,CAAC,CAAA;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC3D,MAAM,IAAI,QAAQ,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,GAAG,CAAC,CAAA;YAC9E,CAAC;YACD,MAAM,IAAI,QAAQ,CAAC,iCAAiC,IAAI,CAAC,MAAM,CAAC,OAAO,KAAM,KAAe,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;QAC3G,CAAC;gBAAS,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAClC,IAAI,IAAoB,CAAA;QAExB,IAAI,CAAC;YACJ,IAAI,GAAG,IAAI,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAoB,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,KAAK,EAAqB,CAAA;QAC/F,CAAC;QAAC,MAAM,CAAC;YACR,MAAM,IAAI,QAAQ,CAAC,iDAAiD,QAAQ,CAAC,MAAM,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;QACzG,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC7B,MAAM,IAAI,QAAQ,CACjB,gFAAgF,EAChF,GAAG,EACH,IAAI,CAAC,MAAM,CACX,CAAA;QACF,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC7B,MAAM,IAAI,QAAQ,CACjB,cAAc,CAAC,IAAI,CAAC;gBACnB,+DAA+D;oBAC9D,mEAAmE,EACrE,GAAG,EACH,IAAI,CAAC,MAAM,CACX,CAAA;QACF,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACtC,MAAM,IAAI,QAAQ,CACjB,cAAc,CAAC,IAAI,CAAC,IAAI,mCAAmC,QAAQ,CAAC,MAAM,GAAG,EAC7E,QAAQ,CAAC,MAAM,EACf,IAAI,CAAC,MAAM,CACX,CAAA;QACF,CAAC;QAED,OAAO,IAAI,CAAA;IACZ,CAAC;CACD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration, read once at startup from the environment.
|
|
3
|
+
*
|
|
4
|
+
* The token is never accepted as a CLI argument: argv is visible to every other
|
|
5
|
+
* process on the machine, while the environment of a process is not.
|
|
6
|
+
*/
|
|
7
|
+
function normaliseBaseUrl(raw) {
|
|
8
|
+
return raw.replace(/\/+$/, "");
|
|
9
|
+
}
|
|
10
|
+
export function loadConfig() {
|
|
11
|
+
const token = process.env.CAREERFLOW_TOKEN?.trim();
|
|
12
|
+
const baseUrl = process.env.CAREERFLOW_BASE_URL?.trim();
|
|
13
|
+
if (!token) {
|
|
14
|
+
throw new Error("CAREERFLOW_TOKEN is not set. Create a personal access token in CareerFlow " +
|
|
15
|
+
"under Settings > Access Tokens, then expose it to this server as CAREERFLOW_TOKEN.");
|
|
16
|
+
}
|
|
17
|
+
if (!baseUrl) {
|
|
18
|
+
throw new Error("CAREERFLOW_BASE_URL is not set. Point it at your CareerFlow API root, e.g. https://api.example.com");
|
|
19
|
+
}
|
|
20
|
+
const companyIdRaw = process.env.CAREERFLOW_COMPANY_ID?.trim();
|
|
21
|
+
const companyId = companyIdRaw ? Number(companyIdRaw) : undefined;
|
|
22
|
+
if (companyIdRaw && !Number.isFinite(companyId)) {
|
|
23
|
+
throw new Error(`CAREERFLOW_COMPANY_ID must be a number, got "${companyIdRaw}"`);
|
|
24
|
+
}
|
|
25
|
+
const timeoutRaw = process.env.CAREERFLOW_TIMEOUT_MS?.trim();
|
|
26
|
+
const timeoutMs = timeoutRaw ? Number(timeoutRaw) : 30_000;
|
|
27
|
+
if (timeoutRaw && !Number.isFinite(timeoutMs)) {
|
|
28
|
+
throw new Error(`CAREERFLOW_TIMEOUT_MS must be a number, got "${timeoutRaw}"`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
baseUrl: normaliseBaseUrl(baseUrl),
|
|
32
|
+
token,
|
|
33
|
+
companyId,
|
|
34
|
+
timeoutMs
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,SAAS,gBAAgB,CAAC,GAAW;IACpC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,UAAU;IACzB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAA;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,CAAA;IAEvD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACd,4EAA4E;YAC3E,oFAAoF,CACrF,CAAA;IACF,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACd,oGAAoG,CACpG,CAAA;IACF,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAA;IAC9D,MAAM,SAAS,GAAG,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAEjE,IAAI,YAAY,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,gDAAgD,YAAY,GAAG,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAA;IAC5D,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAE1D,IAAI,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,gDAAgD,UAAU,GAAG,CAAC,CAAA;IAC/E,CAAC;IAED,OAAO;QACN,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC;QAClC,KAAK;QACL,SAAS;QACT,SAAS;KACT,CAAA;AACF,CAAC"}
|