systa-mcp 1.0.8 → 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/SKILL.md +70 -39
- package/package.json +4 -2
- package/server.js +759 -55
package/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 1.0
|
|
2
|
+
version: 1.1.0
|
|
3
3
|
name: systa
|
|
4
4
|
description: |
|
|
5
5
|
Drive SysTa (Talep Takip Sistemi / SysTa) — a
|
|
@@ -94,36 +94,45 @@ yanıtındaki `glossary` alias'larının ayna kopyasıdır (MCP yokken de geçer
|
|
|
94
94
|
ASCII'dir — eşleştirmede aksanı yok say. Modülü bulduktan sonra `describe_module(modül)`
|
|
95
95
|
veya `describe_endpoint` ile gövdeyi netleştir.
|
|
96
96
|
|
|
97
|
-
| Doğal dil (alias)
|
|
98
|
-
|
|
|
99
|
-
| talep, talepler, is, destek talebi
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
97
|
+
| Doğal dil (alias) | Modül | Birincil giriş |
|
|
98
|
+
| ------------------------------------------------------------ | --------------- | ----------------------------------------------------------------- |
|
|
99
|
+
| talep, talepler, is, destek talebi | request | list_requests / create_request |
|
|
100
|
+
| geciken, overdue, gecikmis, sla ihlali, deadline gecmis | request/sla | list_requests(overdue:true / slaBreached:true) |
|
|
101
|
+
| toplu guncelle, hepsini, birden cok talep | request | bulk_update_requests |
|
|
102
|
+
| kullanici bul, isim cozumle, kime, hangi ahmet | user | resolve_user_by_name / list_users |
|
|
103
|
+
| sirket bul, kategori bul, ekip bul, id cozumle, lookup | multiple | list_lookup(kind:'company'/'category'/'team'/...) |
|
|
104
|
+
| projeyi klonla, projeyi cogalt, duplicate project | project | duplicate_project |
|
|
105
|
+
| snapshot fark, baseline karsilastir, plan diff | plan | compare_plan_baseline |
|
|
106
|
+
| yorumlar, ekler, degisiklik gecmisi, denetim | request | list_request_comments / list_request_files / list_request_history |
|
|
107
|
+
| gorev, task, kart, card | kanban | manage_request_card / move_request_card |
|
|
108
|
+
| pano, board, kanban / kolon, column, asama, liste | kanban | get_request_board / create_request_board |
|
|
109
|
+
| durum gecisi onizle, izinli gecisler, hangi duruma gecebilir | request | list_allowed_transitions |
|
|
110
|
+
| plan modu, sprint kapsami, plan mode setup | plan | create_plan_mode |
|
|
111
|
+
| efor, durum, oncelik, atanan, planla, denetim, gecmis | request | get_request / list_requests |
|
|
112
|
+
| proje, klonla | project | list_projects / create_project |
|
|
113
|
+
| onaya sunma, snapshot, sprint, sprinti kapat, optimize et | plan | submit_plan_for_approval / create_plan_baseline |
|
|
114
|
+
| onay, onayla, reddet | approvals | list_pending_approvals / respond_to_approval |
|
|
115
|
+
| sla, sure taahhudu | sla | (sla endpoints) |
|
|
116
|
+
| release, surum, go/no-go, deployment | release | create_release_list / add_release_item |
|
|
117
|
+
| musaitlik, izin (leave), vekil | user/automation | systa_api_call /user-availability, /user-leaves |
|
|
118
|
+
| vendor, sirket, musteri | company | systa_api_call GET /companies |
|
|
119
|
+
| organizasyon birimi, departman, bolum, birim | department | GET /api/departments |
|
|
120
|
+
| takim, ekip, team | team | GET /api/teams |
|
|
121
|
+
| yetki paketi, izin (permission) | security | (bundle/permission endpoints) |
|
|
122
|
+
| alt form, sub-form | forms | systa_api_call GET /forms/:id |
|
|
123
|
+
| rapor, raporlama, disa aktar, export, excel, csv | report | systa_api_call /reports/preview,/reports/export |
|
|
124
|
+
| ozel sorgu, sorgu, query builder, ad-hoc, veri cek | query-builder | querybuilder_describe_fields / \_execute_query |
|
|
125
|
+
| hesap tablosu, gridpad, tablo, excel, spreadsheet, hucre | spreadsheet | GET /api/spreadsheets |
|
|
126
|
+
| dosya, ek, attachment, yukle, upload, indir, download | file | upload_file_to / download_request_file |
|
|
127
|
+
| bildirim, notification, uyari, okundu, push | notification | GET /api/notifications |
|
|
128
|
+
| yetenek, beceri, skill, otomatik etiketleme | skill | GET /api/skills |
|
|
129
|
+
| duyuru, announcement, bildiri, ilan | announcement | GET /api/announcements |
|
|
130
|
+
| urun guncellemesi, surum notu, yenilikler, changelog | product-update | GET /api/product-updates |
|
|
131
|
+
| not, kisisel not, hatirlatma | note | GET /api/dashboard/notes |
|
|
132
|
+
| yer imi, favori, bookmark, kisayol | bookmark | GET /api/dashboard/bookmarks |
|
|
133
|
+
| degerlendirme, puanlama, anket, memnuniyet, geri bildirim | ratings | POST /api/ratings |
|
|
134
|
+
| github, repo, pull request, commit, issue aktar | github | systa_api_call /github/me/{connection,repos,import,push} |
|
|
135
|
+
| gosterge paneli, dashboard, widget, ozet, grafik, panel | dashboard | GET /api/dashboard/init |
|
|
127
136
|
|
|
128
137
|
> Bu tablo, katalogdaki ~25 modülün kullanıcı-yüzeyli olanlarından bir alt küme kapsar. Listede olmayan
|
|
129
138
|
> bir kelime için `list_capabilities` çağır — anahtarının erişebildiği güncel modül/alias
|
|
@@ -154,20 +163,26 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
|
|
|
154
163
|
### Talep aç (open a request) — `create_request` → `POST /api/requests`
|
|
155
164
|
|
|
156
165
|
1. Need `title` (min 5 chars) — if missing or too short, ask once. Never invent it.
|
|
157
|
-
2. Defaults: `channel='API'` (AI/MCP source), `
|
|
158
|
-
3.
|
|
166
|
+
2. Defaults: `channel='API'` (AI/MCP source), `statusId` ← default open/new status (resolve via `GET /api/statuses`, pick the non-final entry status), `priority='normal'` unless the user signals "acil/critical". `description` can be a plain string. **`companyId` — DIKKAT:** genelde `user.companyId` DEĞİL — B2B talepler MÜŞTERİ şirketi için açılır. Şirket adı netlesmediyse `systa_api_call GET /companies?search=<ad>` ile çöz veya kullanıcıya SOR. `prepare_create_request(companyId)` proaktif seçenekleri döner.
|
|
167
|
+
3. **Efor** (`estimatedEffort`/`personDay`): KANONIK GÜN. Kullanıcı "N saat" derse ÖNCE `list_capabilities.effortPolicy.hoursPerDay` ile böl (varsayılan 8), sonra yaz. "8 saat" → `estimatedEffort=1` (8/8=1 gün), "4 saat" → `estimatedEffort=0.5`. Aynı kural `manage_request_card.estimatedEffort` ve `bulk_update_requests.updates.estimatedEffort` için de geçerli.
|
|
168
|
+
4. Submit; report "Talep #<num> açıldı — '<title>'". If the user also said "bana ata", chain assignment below.
|
|
159
169
|
|
|
160
170
|
### Üzerimdeki işler / talepleri listele (what's on my plate) — `list_requests` → `GET /api/requests`
|
|
161
171
|
|
|
162
|
-
- "taleplerim / on me" → `assignedTo=<my userId>`. "açık/open" → final
|
|
172
|
+
- "taleplerim / on me" → `assignedTo=<my userId>`. "açık/open" → `openOnly:true`. "geciken/overdue" → `overdue:true` (deadline geçmiş + non-final auto-composed) veya `slaBreached:true` (SLA ihlali). "SLA durumu" → `slaStatus:['breached','active']`. **"dosyalı talepler / eki olan" → `hasAttachments:true`** (top-level talep eki). Deadline'a göre → `deadlineFrom`/`deadlineTo` (YYYY-MM-DD) ya da `deadlineWithinDays:N` (önümüzdeki N gün). Search kapsamını genişlet: `searchIn:['assignee','creator','comments',...]`. **Response boyutu:** `fields` varsayılan `'summary'` (satır başına ~1KB); `'detail'` (description/effort/counts eklenir) veya `'full'` (customFields/stakeHolders dahil, çok büyük). Default `limit=20-50`, sort `created_at desc`. Yanıt zarfı: `{items, total, limit, offset}` — sayfa sayısı `= ceil(total/limit)`. Summarize as a short list (numara + başlık + durum), not raw rows.
|
|
163
173
|
|
|
164
174
|
### Talep detayı (show a request) — `get_request` → `GET /api/requests/:requestNumber`
|
|
165
175
|
|
|
166
176
|
- Resolve the user's number/title to `requestNumber`. If ambiguous, `list_requests` first to find candidates, then fetch.
|
|
177
|
+
- **Alt kaynaklar (dedicated tools):** yorumlar → `list_request_comments(requestNumber)` (her yorumun `fileIds[]`'ı vardır — yorum ekleri buradan bulunur); ekler → `list_request_files(requestNumber)` (yalnız talep-birinci-seviye ekler; yorum/kart ekleri için `download_request_file` description'ındaki 7 kaynaklık listeye bak); değişiklik geçmişi → `list_request_history(requestNumber)` (kim ne değiştirdi, actor/timestamp/field/oldValue/newValue).
|
|
167
178
|
|
|
168
179
|
### Bana / birine ata (assign) — `assign_request` → `PUT /api/requests/:requestNumber`
|
|
169
180
|
|
|
170
|
-
- "bana/üzerime" → `assignedTo=<my userId
|
|
181
|
+
- "bana/üzerime" → `assignedTo=<my userId>` (list_capabilities.user.id). A name → **`resolve_user_by_name(name)`** — döner {matched, ambiguity, userId | candidates[]}. Ambiguity=true (birden çok eşleşme) durumunda kullanıcıya SOR, rastgele seçme. `list_users(search, companyId, ...)` browsing/multi-hit için; tek-isim çözümü için resolve_user_by_name tercih edilir. Partial update — only send `assignedTo`.
|
|
182
|
+
|
|
183
|
+
### Toplu güncelleme (bulk update) — `bulk_update_requests` → `PUT /api/requests/bulk`
|
|
184
|
+
|
|
185
|
+
- Birden fazla talebe AYNI değişikliği uygula ("bunları kritik yap", "5 talebi tamamla") — `requestIds:[...] + updates:{...}` şeklinde tek çağrı. `update_request`'i loop içinde çağırma. `updates` kabul eder: statusId, assignedTo, priority, deadline, categoryId, departmentId, projectId, estimatedEffort (**GÜN**), personDay (**GÜN**), plannedStartDate, plannedEndDate. Farklı update grupları için ayrı bulk çağrıları yap. Undo isteği: `systa_api_call POST /api/requests/bulk/undo` yanıtta dönen `batchId` ile.
|
|
171
186
|
|
|
172
187
|
### Yorum ekle (comment) — `add_request_comment` → `POST /api/requests/:requestNumber/comments`
|
|
173
188
|
|
|
@@ -175,7 +190,7 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
|
|
|
175
190
|
|
|
176
191
|
### Durum değiştir / tamamla / kapat (change status) — `change_request_status` → `PUT /api/requests/:requestNumber`
|
|
177
192
|
|
|
178
|
-
-
|
|
193
|
+
- **ÖNCE** `list_allowed_transitions(requestNumber)` çağır — transition kuralları hedef statüleri sınırlar (kategori/şirket/departman/öncelik bazlı). Dönen listede kullanıcının doğal-dil hedefini (`test asamasi`, `tamamla`, `yeniden ac`) `statusKey` / `statusId` ile eşleştir. `change_request_status` `statusId` VEYA `statusKey` alır (statusKey verirsen tool içeride list_allowed_transitions ile çözer, aksi halde global `/statuses` fallback). "tamamla/kapat" → `is_final=true` bir statü. Kullanıcı kapanış notu yazdıysa `comment` argümanı olarak geç.
|
|
179
194
|
|
|
180
195
|
### Proje aç (start a project) — `create_project` → `POST /api/projects`
|
|
181
196
|
|
|
@@ -183,16 +198,22 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
|
|
|
183
198
|
|
|
184
199
|
### Projemi / projeleri gör + durum (list / health) — `list_projects` / `get_project_health`
|
|
185
200
|
|
|
186
|
-
- List: `
|
|
201
|
+
- List: `list_projects` (default `limit=20`, recent first). Filtreler: `search`, `companyId`, `parentProjectId` (subtree — immediate children), `status`. "durum/sağlık" → combine `health-score` + `risk-score` + `sla-status` into one summary.
|
|
202
|
+
- **Klonla / cogalt:** `duplicate_project(id, newProjectName?)` — kaynak projenin tanımını (isim/company/dep/org/dates/weight/planned effort/governance) yeni bir projeye kopyalar. KOPYALANMAZ: alt proje, talep, board, milestone, snapshot, gerçekleşen efor/ilerleme (bunlar yeni projede sıfırdan başlar).
|
|
187
203
|
|
|
188
204
|
### Kanban kart ekle (add a task card) — `manage_request_card` (create)
|
|
189
205
|
|
|
190
|
-
- `boardId` is always tied to a `requestId` — first resolve the board (`get_request_board`, or `GET /api/requests/:requestId/kanban/hierarchy`). Defaults: first/entry column if `columnId` omitted, `priority='normal'`, unassigned unless "bana ata", `cardTitle` min 3 chars (ask to clarify if shorter). `manage_request_card` handles create / update / complete (the action is in the body) — call `describe_endpoint` if unsure of the fields.
|
|
206
|
+
- `boardId` is always tied to a `requestId` — first resolve the board (`get_request_board`, or `GET /api/requests/:requestId/kanban/hierarchy`). **Board yoksa** (`get_request_board` veya `manage_request_card` yanıtı 404 `KANBAN_BOARD_NOT_FOUND` / kod 2031) — ÖNCE `create_request_board(requestId)` çağır (opsiyonel `templateId` ile şablon; şablonları `systa_api_call GET /task-boards/templates` listeler), sonra kartı ekle. Defaults: first/entry column if `columnId` omitted, `priority='normal'`, unassigned unless "bana ata", `cardTitle` min 3 chars (ask to clarify if shorter). `manage_request_card` handles create / update / complete (the action is in the body) — call `describe_endpoint` if unsure of the fields.
|
|
191
207
|
|
|
192
208
|
### Kartı taşı / tamamla (move / complete card) — `move_request_card` / `manage_request_card`
|
|
193
209
|
|
|
194
210
|
- Move: `move_request_card` — resolve the target column name → `columnId` from the board detail; append to end if no position given. Complete: `manage_request_card` with the complete action — server sets `isCompleted=true`, 100%.
|
|
195
211
|
|
|
212
|
+
### Sprint / plan modu + snapshot (plan lifecycle) — `create_plan_mode` → `create_plan_baseline` → `compare_plan_baseline`
|
|
213
|
+
|
|
214
|
+
- **Sırayla:** (1) `create_plan_mode(name, boards?)` — sprint kapsamını aç. `boards` verirsen filterCriteria ile talepleri filtreleyen boardlar oluşur; **omit edersen tool otomatik tek varsayılan board ekler** (boş baseline'a düşmezsin). (2) `create_plan_baseline(modeId, baselineName)` — mode'un canlı durumunu snapshot alır ve `baselineId` döner. (3) `submit_plan_for_approval(baselineId, approverUserIds)` — onaya sunar. "sprinti kapat" ise `systa_api_call PATCH /api/plan/baselines/:baselineId/lifecycle` `{lifecycleStatus:'completed'}`. "plan optimize et / dagit" ise `distribute_plan_schedule`.
|
|
215
|
+
- **Snapshot farkı / baseline karşılaştırması:** "son snapshot ile bugünkü durum arasında ne değişmiş" → `compare_plan_baseline(baselineId)`. Ham snapshot'ı çekmeye çalışırsan `get_plan_baseline(baselineId)` **varsayılan olarak `fields:'summary'`** kullanır ve `snapshotData`'yı DAHİL ETMEZ — metadata + counts (totalCards, totalEffort, approvalStatus, hasSnapshot) döner. Gerçekten ham snapshot lazımsa `fields:'full'` (nadir, ~800KB).
|
|
216
|
+
|
|
196
217
|
### Onaya sun / onayla / bekleyenler (approvals)
|
|
197
218
|
|
|
198
219
|
- Submit (request approval): `request_approval` → `POST /api/approvals/batch`, `strategy='any_one'` default, approver names → ids. Plan onayı ayrı: `submit_plan_for_approval` → `POST /api/plan/approvals/submit` (`strategy='all_required'`).
|
|
@@ -211,6 +232,16 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
|
|
|
211
232
|
|
|
212
233
|
- `releaseType='standard'`, `requireAllStakeholderApprovals=false`, `companyId` from scope. Add items with `add_release_item`. Deployment workflow steps (`close-servers` → `start-db-migration` → `start-server-update` → `open-servers` → `complete`) are live ops — **never auto-advance blindly**; show which step you're on and confirm. Release notes: generate → review → publish (never auto-publish).
|
|
213
234
|
|
|
235
|
+
### GitHub: bağla / issue aktar / push (kişisel entegrasyon) — `github.self`
|
|
236
|
+
|
|
237
|
+
Kullanıcının kendi GitHub bağlantısı üzerinden talep açma/aktarma. Tümü `systa_api_call` (named tool yok); gereken scope `github.self` (anahtarda yoksa bu uçlar katalogda görünmez → kullanıcıya `github.self` scope'lu yeni anahtar üretmesini söyle).
|
|
238
|
+
|
|
239
|
+
- **PAT bağlama AI'dan YAPILMAZ.** Kullanıcı GitHub fine-grained PAT'ını SysTa web arayüzünden (Ayarlar → GitHub) bağlar; AI asla PAT/secret istemez/almaz. Bağlantı yoksa kullanıcıyı oraya yönlendir.
|
|
240
|
+
- **Bağlantı durumu:** `systa_api_call` `GET /github/me/connection` (maskeli meta). Bağlı değilse import/push deneme — önce web'den bağlanmasını söyle.
|
|
241
|
+
- **Repo listele:** `systa_api_call` `GET /github/me/repos` — PAT'ın eriştiği repo'lar.
|
|
242
|
+
- **Issue içe aktar (GitHub → SysTa talep):** `systa_api_call` `POST /github/me/import` — seçili repo'nun issue'larını kullanıcının KENDİ talepleri olarak içe aktarır (idempotent, createdBy=self). "şu repodan issue'ları SysTa'ya aktar / GitHub'dan talep aç" → bu. Mutating + canlı: hangi repo / kaç issue özetini göster, **onay al**.
|
|
243
|
+
- **Talep push (SysTa → GitHub issue):** `systa_api_call` `POST /github/me/push` — kullanıcının talebini kendi repo'suna GitHub issue'su olarak gönderir/günceller (idempotent, node_id). Dış yan-etki (GitHub'a yazar) → **onay al**.
|
|
244
|
+
|
|
214
245
|
## Zincirleme & hata kurtarma (chaining & error recovery)
|
|
215
246
|
|
|
216
247
|
SysTa returns structured errors. Recover deterministically instead of giving up.
|
|
@@ -222,7 +253,7 @@ SysTa returns structured errors. Recover deterministically instead of giving up.
|
|
|
222
253
|
- Retry once after a fix. If it fails again with the same field, surface it to the user rather than looping.
|
|
223
254
|
2. **`UNEXPECTED_FILE_FIELD` / `UNKNOWN_PARAM`** — you sent a flag/field the endpoint doesn't accept. Re-check with `describe_endpoint` and drop it. (Web/UI-only fields aren't valid on the API surface.)
|
|
224
255
|
3. **`404` on a `:requestNumber` / `:id`** — the handle didn't resolve. Run the matching list/search (`list_requests`, `list_projects`, or `systa_api_call` `GET /users?search=`, `GET /tags/search`) to map name/number → id, then retry. Never hard-code an id.
|
|
225
|
-
4. **ID resolution chain (general).** Most mutations need an id the user gave you as a name.
|
|
256
|
+
4. **ID resolution chain (general).** Most mutations need an id the user gave you as a name. Use dedicated tools when available (each returns rows with `{id, name/key}`); fall back to `systa_api_call` for kinds not covered. **company / team / department / organization / category / status → `list_lookup(kind:'<kind>', search:'<name>')`** — one tool covers six reference entities. **user → `resolve_user_by_name(name)`** — returns `{userId}` on unique match or `{ambiguity:true, candidates}` on multi-match (ASK user, do NOT auto-pick); `list_users(search, ...)` for browsing. "ben/bana" → `list_capabilities.user.id`. tag → `systa_api_call GET /tags/search`. board → request's kanban hierarchy (`get_request_board`; requestId→boardId→columnId). Resolve first, act second.
|
|
226
257
|
5. **`401/403`** — expired or out-of-scope key. Don't retry blindly; tell the user the auth/permission problem (in their language) and stop. For `403 permission`, name the capability the key lacks; don't attempt a privilege workaround.
|
|
227
258
|
6. **`409 conflict`** (optimistic concurrency on cards/edits) — re-read the entity to get the latest `updatedAt`, then retry the mutation once with the fresh value; warn the user only if it conflicts again.
|
|
228
259
|
7. **Dual-path "list vs create" endpoints** (e.g. `POST /api/sla/definitions`, `POST /api/sla/standby-configs`) — an empty/filter-only body lists; a full body creates. Choose the body shape that matches the intent; don't accidentally create when the user asked to list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "systa-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP stdio server for SysTa (Talep Takip Sistemi) — lets AI agents (Claude, Codex) use the SysTa REST API via a scoped API key. Zero npm dependencies (native Node).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "server.js",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"start": "node server.js"
|
|
16
|
+
"start": "node server.js",
|
|
17
|
+
"check:version-sync": "node scripts/check-version-sync.js",
|
|
18
|
+
"prepublishOnly": "node scripts/check-version-sync.js"
|
|
17
19
|
},
|
|
18
20
|
"engines": {
|
|
19
21
|
"node": ">=18"
|
package/server.js
CHANGED
|
@@ -420,12 +420,72 @@ const shapeOneModule = (caps, target) => {
|
|
|
420
420
|
};
|
|
421
421
|
};
|
|
422
422
|
|
|
423
|
+
// list_requests: 'openOnly' + 'overdue' both need the NON-final status ids -- GET
|
|
424
|
+
// /requests accepts statusId as an array, but has no excludeStatusIds. Fetch
|
|
425
|
+
// /statuses once and INCLUDE the non-final ones.
|
|
426
|
+
const resolveListRequestsStatusFilter = async (args) => {
|
|
427
|
+
const wantOnlyOpen = args.openOnly || args.overdue === true;
|
|
428
|
+
if (!wantOnlyOpen) {
|
|
429
|
+
return args.statusId;
|
|
430
|
+
}
|
|
431
|
+
const st = await systaFetch('GET', '/statuses').catch(() => null);
|
|
432
|
+
const rows = st && st.ok ? (st.data && (st.data.data || st.data)) || [] : [];
|
|
433
|
+
const openIds = (Array.isArray(rows) ? rows : []).filter((s) => s && !s.isFinal).map((s) => s.id);
|
|
434
|
+
return openIds.length > 0 ? openIds : args.statusId;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// list_requests: assemble the querystring the backend querySchema.js normalizes.
|
|
438
|
+
// Bracket-form (deadlineDaysRemaining[operator]) and comma-separated arrays are
|
|
439
|
+
// canonical for that validator; url.searchParams encodes brackets literally.
|
|
440
|
+
const buildListRequestsQuery = (args, statusId) => {
|
|
441
|
+
const query = {
|
|
442
|
+
limit: args.limit ?? 20,
|
|
443
|
+
offset: args.offset,
|
|
444
|
+
search: args.search,
|
|
445
|
+
statusId,
|
|
446
|
+
assignedTo: args.assignedTo,
|
|
447
|
+
companyId: args.companyId,
|
|
448
|
+
categoryId: args.categoryId,
|
|
449
|
+
departmentId: args.departmentId,
|
|
450
|
+
priority: args.priority,
|
|
451
|
+
// Blind-AI-friendly default: return the lightweight 'summary' shape
|
|
452
|
+
// (~1KB/row) unless the caller explicitly asks for detail/full.
|
|
453
|
+
fields: args.fields ?? 'summary',
|
|
454
|
+
};
|
|
455
|
+
if (Array.isArray(args.searchIn) && args.searchIn.length > 0) {
|
|
456
|
+
query.searchIn = args.searchIn.join(',');
|
|
457
|
+
}
|
|
458
|
+
if (Array.isArray(args.slaStatus) && args.slaStatus.length > 0) {
|
|
459
|
+
query.slaStatus = args.slaStatus.join(',');
|
|
460
|
+
}
|
|
461
|
+
if (args.slaBreached === true) {
|
|
462
|
+
query.slaBreached = true;
|
|
463
|
+
}
|
|
464
|
+
if (typeof args.hasAttachments === 'boolean') {
|
|
465
|
+
query.hasAttachments = args.hasAttachments;
|
|
466
|
+
}
|
|
467
|
+
if (args.deadlineFrom) {
|
|
468
|
+
query.deadlineFrom = args.deadlineFrom;
|
|
469
|
+
}
|
|
470
|
+
if (args.deadlineTo) {
|
|
471
|
+
query.deadlineTo = args.deadlineTo;
|
|
472
|
+
}
|
|
473
|
+
if (args.overdue === true) {
|
|
474
|
+
query['deadlineDaysRemaining[operator]'] = 'lt';
|
|
475
|
+
query['deadlineDaysRemaining[value]'] = 0;
|
|
476
|
+
} else if (typeof args.deadlineWithinDays === 'number') {
|
|
477
|
+
query['deadlineDaysRemaining[operator]'] = 'lte';
|
|
478
|
+
query['deadlineDaysRemaining[value]'] = args.deadlineWithinDays;
|
|
479
|
+
}
|
|
480
|
+
return query;
|
|
481
|
+
};
|
|
482
|
+
|
|
423
483
|
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
424
484
|
const TOOLS = [
|
|
425
485
|
{
|
|
426
486
|
name: 'list_requests',
|
|
427
487
|
description:
|
|
428
|
-
|
|
488
|
+
"List/search SysTa requests (talepler) with pagination and rich filters: status, assignee, company, category, department, priority, free-text, OVERDUE / SLA / DEADLINE. Common intents: 'uzerimdeki acik isler / my open work' => assignedTo=<me> + openOnly:true (excludes final statuses). 'geciken isler / overdue' => overdue:true (deadline gecmis + non-final) OR slaBreached:true (SLA ihlali). 'bu ay kapatilan' => finalOnly:true + completedFrom/completedTo (NOT YET SUPPORTED — use systa_api_call). search covers title/requestNumber/description/externalRef by default (NOT assignee/creator); use searchIn to expand. Multiple statuses: statusId can be an array [1,3,5]. Returns only requests the API key scope permits. Response envelope: {items:[], total, limit, offset} — totalPages = ceil(total/limit). RESPONSE SIZE: fields controls how much each row carries -- default 'summary' returns lightweight rows (~1KB each: id, requestNumber, title, statusName, priority, assignedTo, deadline, isOverdue, slaStatus, commentCount, fileCount). Use fields:'detail' when you need description/effort/counts, or fields:'full' for everything incl. customFields/stakeHolders (LARGE — S-A2 kanit: 44 talep tam moduyla ~300KB).",
|
|
429
489
|
inputSchema: {
|
|
430
490
|
type: 'object',
|
|
431
491
|
additionalProperties: false,
|
|
@@ -437,7 +497,29 @@ const TOOLS = [
|
|
|
437
497
|
description: 'Page size (1-100, default 20)',
|
|
438
498
|
},
|
|
439
499
|
offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
|
|
440
|
-
search: {
|
|
500
|
+
search: {
|
|
501
|
+
type: 'string',
|
|
502
|
+
description:
|
|
503
|
+
'Free-text; by default matches title, requestNumber, description, externalRef. For ATANAN (assignee) name lookup use resolve_user_by_name first, then filter by assignedTo. Broaden matching via searchIn.',
|
|
504
|
+
},
|
|
505
|
+
searchIn: {
|
|
506
|
+
type: 'array',
|
|
507
|
+
items: {
|
|
508
|
+
type: 'string',
|
|
509
|
+
enum: [
|
|
510
|
+
'title',
|
|
511
|
+
'description',
|
|
512
|
+
'requestnumber',
|
|
513
|
+
'externalref',
|
|
514
|
+
'category',
|
|
515
|
+
'creator',
|
|
516
|
+
'assignee',
|
|
517
|
+
'comments',
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
description:
|
|
521
|
+
'Expand which fields the search string is matched against (default: title+description+requestnumber+externalref).',
|
|
522
|
+
},
|
|
441
523
|
statusId: {
|
|
442
524
|
type: ['integer', 'array'],
|
|
443
525
|
items: { type: 'integer' },
|
|
@@ -455,37 +537,212 @@ const TOOLS = [
|
|
|
455
537
|
companyId: { type: 'integer', description: 'Filter by company id' },
|
|
456
538
|
categoryId: { type: 'integer', description: 'Filter by category id' },
|
|
457
539
|
departmentId: { type: 'integer', description: 'Filter by department id' },
|
|
458
|
-
priority: {
|
|
540
|
+
priority: {
|
|
541
|
+
type: 'string',
|
|
542
|
+
enum: ['low', 'normal', 'high', 'urgent', 'critical'],
|
|
543
|
+
description: 'Filter by priority key/level',
|
|
544
|
+
},
|
|
545
|
+
overdue: {
|
|
546
|
+
type: 'boolean',
|
|
547
|
+
description:
|
|
548
|
+
'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly.',
|
|
549
|
+
},
|
|
550
|
+
slaStatus: {
|
|
551
|
+
type: 'array',
|
|
552
|
+
items: { type: 'string', enum: ['active', 'paused', 'completed', 'breached'] },
|
|
553
|
+
description: 'SLA status filter (multi-value). Backend accepts comma-separated string.',
|
|
554
|
+
},
|
|
555
|
+
slaBreached: {
|
|
556
|
+
type: 'boolean',
|
|
557
|
+
description: 'true => only requests whose SLA has been breached.',
|
|
558
|
+
},
|
|
559
|
+
hasAttachments: {
|
|
560
|
+
type: 'boolean',
|
|
561
|
+
description:
|
|
562
|
+
'true => only requests with at least one active (non-deleted) top-level file attachment (files_table.request_id). Note: comment attachments and kanban card attachments live under separate tables -- this filter targets request-level uploads. Combine with fileCount>0 on the response for the same effect. Response row already carries fileCount so client-side filtering is possible on smaller pages.',
|
|
563
|
+
},
|
|
564
|
+
deadlineFrom: {
|
|
565
|
+
type: 'string',
|
|
566
|
+
description: 'Deadline >= this date (YYYY-MM-DD).',
|
|
567
|
+
},
|
|
568
|
+
deadlineTo: {
|
|
569
|
+
type: 'string',
|
|
570
|
+
description: 'Deadline <= this date (YYYY-MM-DD).',
|
|
571
|
+
},
|
|
572
|
+
deadlineWithinDays: {
|
|
573
|
+
type: 'integer',
|
|
574
|
+
description:
|
|
575
|
+
'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N).',
|
|
576
|
+
},
|
|
577
|
+
fields: {
|
|
578
|
+
type: 'string',
|
|
579
|
+
enum: ['summary', 'detail', 'full'],
|
|
580
|
+
description:
|
|
581
|
+
"Response projection profile: 'summary' (default, lightweight ~1KB/row: id/requestNumber/title/statusName/priority/assignee/deadline/isOverdue/slaStatus/commentCount/fileCount), 'detail' (adds description/effort/all counts), 'full' (everything incl. customFields/stakeHolders — LARGE).",
|
|
582
|
+
},
|
|
459
583
|
},
|
|
460
584
|
},
|
|
461
585
|
handler: async (args) => {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
586
|
+
const statusId = await resolveListRequestsStatusFilter(args);
|
|
587
|
+
const query = buildListRequestsQuery(args, statusId);
|
|
588
|
+
return systaFetch('GET', '/requests', { query });
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: 'list_users',
|
|
593
|
+
description:
|
|
594
|
+
'List/search SysTa users (kullanicilar) with pagination and filters (search, companyId, departmentId). Wraps GET /api/users?search=... Prefer resolve_user_by_name when you have a single name and want the id -- this tool is for browsing/multiple hits. Returns {id, fullName, email, companyName, departmentName} per user (subset).',
|
|
595
|
+
inputSchema: {
|
|
596
|
+
type: 'object',
|
|
597
|
+
additionalProperties: false,
|
|
598
|
+
properties: {
|
|
599
|
+
search: {
|
|
600
|
+
type: 'string',
|
|
601
|
+
description: 'Free-text search over fullName/email/username (case-insensitive).',
|
|
602
|
+
},
|
|
603
|
+
companyId: { type: 'integer', description: 'Filter by company id' },
|
|
604
|
+
departmentId: { type: 'integer', description: 'Filter by department id' },
|
|
605
|
+
limit: {
|
|
606
|
+
type: 'integer',
|
|
607
|
+
minimum: 1,
|
|
608
|
+
maximum: 100,
|
|
609
|
+
description: 'Page size (1-100, default 20)',
|
|
610
|
+
},
|
|
611
|
+
offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
handler: (args) =>
|
|
615
|
+
systaFetch('GET', '/users', {
|
|
477
616
|
query: {
|
|
478
|
-
limit: args.limit ?? 20,
|
|
479
|
-
offset: args.offset,
|
|
480
617
|
search: args.search,
|
|
481
|
-
statusId,
|
|
482
|
-
assignedTo: args.assignedTo,
|
|
483
618
|
companyId: args.companyId,
|
|
484
|
-
categoryId: args.categoryId,
|
|
485
619
|
departmentId: args.departmentId,
|
|
486
|
-
|
|
620
|
+
limit: args.limit ?? 20,
|
|
621
|
+
offset: args.offset,
|
|
622
|
+
},
|
|
623
|
+
}),
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: 'resolve_user_by_name',
|
|
627
|
+
description:
|
|
628
|
+
"Resolve a person's name (isim -> userId) for assignments and filters. Returns {userId, fullName, ambiguity} when a UNIQUE match is found; when MULTIPLE users match (e.g. 3 'Ahmet' users) returns {ambiguity:true, candidates:[{id, fullName, companyName, departmentName}], hint:'ASK the user which one'} -- DO NOT auto-pick the first candidate. Use userId with assign_request / assignedTo. When 'me/bana' is meant, use list_capabilities user.id instead (no resolve needed).",
|
|
629
|
+
inputSchema: {
|
|
630
|
+
type: 'object',
|
|
631
|
+
additionalProperties: false,
|
|
632
|
+
required: ['name'],
|
|
633
|
+
properties: {
|
|
634
|
+
name: { type: 'string', description: 'Person name to resolve (partial match allowed)' },
|
|
635
|
+
companyId: {
|
|
636
|
+
type: 'integer',
|
|
637
|
+
description: 'Restrict search to a specific company (optional)',
|
|
487
638
|
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
handler: async (args) => {
|
|
642
|
+
const res = await systaFetch('GET', '/users', {
|
|
643
|
+
query: { search: args.name, companyId: args.companyId, limit: 10 },
|
|
488
644
|
});
|
|
645
|
+
if (!res.ok) {
|
|
646
|
+
return res;
|
|
647
|
+
}
|
|
648
|
+
const users =
|
|
649
|
+
res.data?.data?.users || res.data?.data?.items || res.data?.data || res.data || [];
|
|
650
|
+
const arr = Array.isArray(users) ? users : [];
|
|
651
|
+
const trimmed = String(args.name).trim().toLowerCase();
|
|
652
|
+
const exact = arr.filter(
|
|
653
|
+
(u) =>
|
|
654
|
+
String(u.fullName || '')
|
|
655
|
+
.trim()
|
|
656
|
+
.toLowerCase() === trimmed,
|
|
657
|
+
);
|
|
658
|
+
const pool = exact.length > 0 ? exact : arr;
|
|
659
|
+
if (pool.length === 0) {
|
|
660
|
+
return {
|
|
661
|
+
status: 200,
|
|
662
|
+
ok: true,
|
|
663
|
+
data: {
|
|
664
|
+
success: true,
|
|
665
|
+
data: {
|
|
666
|
+
matched: false,
|
|
667
|
+
hint: `No user found matching "${args.name}". Ask the user for the full name or a different spelling.`,
|
|
668
|
+
candidates: [],
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (pool.length === 1) {
|
|
674
|
+
const u = pool[0];
|
|
675
|
+
return {
|
|
676
|
+
status: 200,
|
|
677
|
+
ok: true,
|
|
678
|
+
data: {
|
|
679
|
+
success: true,
|
|
680
|
+
data: {
|
|
681
|
+
matched: true,
|
|
682
|
+
ambiguity: false,
|
|
683
|
+
userId: u.id,
|
|
684
|
+
fullName: u.fullName,
|
|
685
|
+
companyName: u.companyName || null,
|
|
686
|
+
departmentName: u.departmentName || null,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
status: 200,
|
|
693
|
+
ok: true,
|
|
694
|
+
data: {
|
|
695
|
+
success: true,
|
|
696
|
+
data: {
|
|
697
|
+
matched: true,
|
|
698
|
+
ambiguity: true,
|
|
699
|
+
hint: `Multiple users matched "${args.name}"; ASK the user which one -- do not auto-pick.`,
|
|
700
|
+
candidates: pool.slice(0, 10).map((u) => ({
|
|
701
|
+
id: u.id,
|
|
702
|
+
fullName: u.fullName,
|
|
703
|
+
email: u.email || null,
|
|
704
|
+
companyName: u.companyName || null,
|
|
705
|
+
departmentName: u.departmentName || null,
|
|
706
|
+
})),
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: 'bulk_update_requests',
|
|
714
|
+
description:
|
|
715
|
+
"Update MULTIPLE SysTa requests in ONE call (topluca guncelle) -- wraps PUT /api/requests/bulk. Use for 'these 5 talepler'i kritik yap' / 'set assignedTo to X for #189, #190, #191'. Provide requestIds (an array of numeric request ids/numbers) and updates (a partial field set applied to ALL of them). Do NOT loop calling update_request -- one bulk call is atomic per group and faster. If you need DIFFERENT updates per request, call bulk once per group. After success the response returns a batchId -- pass it to systa_api_call POST /requests/bulk/undo to revert if needed.",
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
additionalProperties: false,
|
|
719
|
+
required: ['requestIds', 'updates'],
|
|
720
|
+
properties: {
|
|
721
|
+
requestIds: {
|
|
722
|
+
type: 'array',
|
|
723
|
+
items: { type: 'integer' },
|
|
724
|
+
minItems: 1,
|
|
725
|
+
maxItems: 500,
|
|
726
|
+
description: 'Request numbers to update (1-500).',
|
|
727
|
+
},
|
|
728
|
+
updates: {
|
|
729
|
+
type: 'object',
|
|
730
|
+
additionalProperties: true,
|
|
731
|
+
description:
|
|
732
|
+
'Partial fields applied to every request in requestIds: statusId, assignedTo, priority, deadline, categoryId, departmentId, projectId, estimatedEffort (DAYS), personDay (DAYS), plannedStartDate, plannedEndDate, description, title.',
|
|
733
|
+
},
|
|
734
|
+
reason: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description: 'Optional audit reason (why the bulk change).',
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
handler: (args) => {
|
|
741
|
+
const body = { requestIds: args.requestIds, updates: args.updates };
|
|
742
|
+
if (args.reason) {
|
|
743
|
+
body.reason = args.reason;
|
|
744
|
+
}
|
|
745
|
+
return systaFetch('PUT', '/requests/bulk', { body });
|
|
489
746
|
},
|
|
490
747
|
},
|
|
491
748
|
{
|
|
@@ -508,9 +765,128 @@ const TOOLS = [
|
|
|
508
765
|
return systaFetch('GET', `/requests/${encodeURIComponent(rn)}`);
|
|
509
766
|
},
|
|
510
767
|
},
|
|
768
|
+
{
|
|
769
|
+
name: 'list_request_files',
|
|
770
|
+
description:
|
|
771
|
+
'List the ATTACHMENTS (dosya ekleri) of a SysTa request (GET /requests/:requestNumber/files). Returns {id, fileName, fileType, fileSize, uploadedAt, uploadedBy}. Use these fileIds with download_request_file to fetch content. NOTE: request-level attachments are only ONE of the file sources in SysTa -- comment attachments (in comments[].fileIds) and kanban card attachments live separately (see download_request_file for the full source list).',
|
|
772
|
+
inputSchema: {
|
|
773
|
+
type: 'object',
|
|
774
|
+
additionalProperties: false,
|
|
775
|
+
required: ['requestNumber'],
|
|
776
|
+
properties: {
|
|
777
|
+
requestNumber: { type: 'integer', description: 'Request number' },
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
handler: (args) =>
|
|
781
|
+
systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/files`),
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: 'list_request_comments',
|
|
785
|
+
description:
|
|
786
|
+
"List the COMMENTS (yorumlar) of a SysTa request (GET /requests/:requestNumber/comments). Returns each comment's text/JSON, author, timestamp AND fileIds[] (attachment ids on the comment). The request's DESCRIPTION is materialized as comment #1 (commentNo=1). Comment attachments are a common file source -- use fileIds[] with download_request_file. Pagination via limit/offset if supported by the backend.",
|
|
787
|
+
inputSchema: {
|
|
788
|
+
type: 'object',
|
|
789
|
+
additionalProperties: false,
|
|
790
|
+
required: ['requestNumber'],
|
|
791
|
+
properties: {
|
|
792
|
+
requestNumber: { type: 'integer', description: 'Request number' },
|
|
793
|
+
limit: {
|
|
794
|
+
type: 'integer',
|
|
795
|
+
minimum: 1,
|
|
796
|
+
maximum: 100,
|
|
797
|
+
description: 'Page size',
|
|
798
|
+
},
|
|
799
|
+
offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
handler: (args) =>
|
|
803
|
+
systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/comments`, {
|
|
804
|
+
query: { limit: args.limit, offset: args.offset },
|
|
805
|
+
}),
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
name: 'list_request_history',
|
|
809
|
+
description:
|
|
810
|
+
"List the CHANGE HISTORY / AUDIT LOG (degisiklik gecmisi / denetim) of a SysTa request (GET /requests/:requestNumber/history). Returns each change with actor, timestamp, field, oldValue, newValue. This is SysTa's per-entity audit trail -- there is no single 'audit log' endpoint. Use for 'X talebinde neler olmus / who changed the status'.",
|
|
811
|
+
inputSchema: {
|
|
812
|
+
type: 'object',
|
|
813
|
+
additionalProperties: false,
|
|
814
|
+
required: ['requestNumber'],
|
|
815
|
+
properties: {
|
|
816
|
+
requestNumber: { type: 'integer', description: 'Request number' },
|
|
817
|
+
limit: {
|
|
818
|
+
type: 'integer',
|
|
819
|
+
minimum: 1,
|
|
820
|
+
maximum: 100,
|
|
821
|
+
description: 'Page size',
|
|
822
|
+
},
|
|
823
|
+
offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
handler: (args) =>
|
|
827
|
+
systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/history`, {
|
|
828
|
+
query: { limit: args.limit, offset: args.offset },
|
|
829
|
+
}),
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: 'list_lookup',
|
|
833
|
+
description:
|
|
834
|
+
"Unified id-resolution / lookup tool for reference entities (company / team / department / organization / category / status). Wraps the per-kind GET endpoints in ONE tool so blind AIs don't juggle 6 separate list tools. Call BEFORE create_request/update_request when you need to turn a NAME into an id ('Atlas Sigorta' -> companyId, 'Hata' -> categoryId, 'Test Ediliyor' -> statusId). Returns rows with {id, name/key} for the chosen kind. For USERS use resolve_user_by_name (name ambiguity handling is different).",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: 'object',
|
|
837
|
+
additionalProperties: false,
|
|
838
|
+
required: ['kind'],
|
|
839
|
+
properties: {
|
|
840
|
+
kind: {
|
|
841
|
+
type: 'string',
|
|
842
|
+
enum: ['company', 'team', 'department', 'organization', 'category', 'status'],
|
|
843
|
+
description: 'Which reference entity to look up',
|
|
844
|
+
},
|
|
845
|
+
search: {
|
|
846
|
+
type: 'string',
|
|
847
|
+
description: 'Free-text search (name/code contains)',
|
|
848
|
+
},
|
|
849
|
+
companyId: {
|
|
850
|
+
type: 'integer',
|
|
851
|
+
description:
|
|
852
|
+
'Scope organization/department lookups to a specific company (ignored for others)',
|
|
853
|
+
},
|
|
854
|
+
limit: {
|
|
855
|
+
type: 'integer',
|
|
856
|
+
minimum: 1,
|
|
857
|
+
maximum: 100,
|
|
858
|
+
description: 'Page size (default 20)',
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
handler: (args) => {
|
|
863
|
+
const KIND_TO_PATH = {
|
|
864
|
+
company: '/companies',
|
|
865
|
+
team: '/teams',
|
|
866
|
+
department: '/departments',
|
|
867
|
+
organization: '/organizations',
|
|
868
|
+
category: '/categories',
|
|
869
|
+
status: '/statuses',
|
|
870
|
+
};
|
|
871
|
+
const path = KIND_TO_PATH[args.kind];
|
|
872
|
+
if (!path) {
|
|
873
|
+
throw new Error(`list_lookup: unknown kind '${args.kind}'`);
|
|
874
|
+
}
|
|
875
|
+
const query = {
|
|
876
|
+
search: args.search,
|
|
877
|
+
limit: args.limit ?? 20,
|
|
878
|
+
};
|
|
879
|
+
// company scoping only makes sense for org unit hierarchies.
|
|
880
|
+
if (args.companyId != null && (args.kind === 'department' || args.kind === 'organization')) {
|
|
881
|
+
query.companyId = args.companyId;
|
|
882
|
+
}
|
|
883
|
+
return systaFetch('GET', path, { query });
|
|
884
|
+
},
|
|
885
|
+
},
|
|
511
886
|
{
|
|
512
887
|
name: 'list_projects',
|
|
513
|
-
description:
|
|
888
|
+
description:
|
|
889
|
+
'List/search SysTa projects (projeler) with pagination and filters. Project hierarchy: a project can have a parent_project_id (alt proje / sub-project). Filter to a subtree with parentProjectId=<id> (immediate children). NOTE: there is no root-only filter yet server-side; if you need only root projects, filter the response by projects with parentProjectId==null. Returns only projects the API key scope permits.',
|
|
514
890
|
inputSchema: {
|
|
515
891
|
type: 'object',
|
|
516
892
|
additionalProperties: false,
|
|
@@ -522,10 +898,34 @@ const TOOLS = [
|
|
|
522
898
|
description: 'Page size (1-100, default 20)',
|
|
523
899
|
},
|
|
524
900
|
offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
|
|
901
|
+
search: { type: 'string', description: 'Free-text search (project name/code/description)' },
|
|
902
|
+
companyId: { type: 'integer', description: 'Filter by company id' },
|
|
903
|
+
parentProjectId: {
|
|
904
|
+
type: 'integer',
|
|
905
|
+
description: 'Filter to the immediate children of this parent project',
|
|
906
|
+
},
|
|
907
|
+
status: {
|
|
908
|
+
type: 'string',
|
|
909
|
+
enum: ['active', 'passive', 'cancelled', 'completed'],
|
|
910
|
+
description: 'Filter by project lifecycle status',
|
|
911
|
+
},
|
|
912
|
+
sortBy: { type: 'string', description: 'Sort field (see project list docs)' },
|
|
913
|
+
sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction' },
|
|
525
914
|
},
|
|
526
915
|
},
|
|
527
916
|
handler: (args) =>
|
|
528
|
-
systaFetch('GET', '/projects', {
|
|
917
|
+
systaFetch('GET', '/projects', {
|
|
918
|
+
query: {
|
|
919
|
+
limit: args.limit ?? 20,
|
|
920
|
+
offset: args.offset,
|
|
921
|
+
search: args.search,
|
|
922
|
+
companyId: args.companyId,
|
|
923
|
+
parentProjectId: args.parentProjectId,
|
|
924
|
+
status: args.status,
|
|
925
|
+
sortBy: args.sortBy,
|
|
926
|
+
sortOrder: args.sortOrder,
|
|
927
|
+
},
|
|
928
|
+
}),
|
|
529
929
|
},
|
|
530
930
|
{
|
|
531
931
|
name: 'create_request',
|
|
@@ -564,27 +964,72 @@ const TOOLS = [
|
|
|
564
964
|
description:
|
|
565
965
|
'Organization-unit id (optional; may be required by the active form; must belong to companyId)',
|
|
566
966
|
},
|
|
967
|
+
departmentId: { type: 'integer', description: 'Department id (optional)' },
|
|
968
|
+
projectId: { type: 'integer', description: 'Link to a project (optional)' },
|
|
969
|
+
priority: {
|
|
970
|
+
type: 'string',
|
|
971
|
+
enum: ['low', 'normal', 'high', 'urgent', 'critical'],
|
|
972
|
+
description: "Priority key. Defaults to backend default (typically 'normal').",
|
|
973
|
+
},
|
|
974
|
+
deadline: {
|
|
975
|
+
type: 'string',
|
|
976
|
+
description: 'Deadline (YYYY-MM-DD or ISO-8601 datetime)',
|
|
977
|
+
},
|
|
978
|
+
plannedStartDate: {
|
|
979
|
+
type: 'string',
|
|
980
|
+
description: 'Planned start (YYYY-MM-DD). Must be <= plannedEndDate.',
|
|
981
|
+
},
|
|
982
|
+
plannedEndDate: {
|
|
983
|
+
type: 'string',
|
|
984
|
+
description: 'Planned end (YYYY-MM-DD). Must be >= plannedStartDate.',
|
|
985
|
+
},
|
|
986
|
+
estimatedEffort: {
|
|
987
|
+
type: 'number',
|
|
988
|
+
minimum: 0,
|
|
989
|
+
description:
|
|
990
|
+
"Estimated effort in CANONICAL DAYS (person-days). If user says 'N hours' divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
|
|
991
|
+
},
|
|
992
|
+
personDay: {
|
|
993
|
+
type: 'number',
|
|
994
|
+
minimum: 0,
|
|
995
|
+
description: 'Person-day scoring in CANONICAL DAYS.',
|
|
996
|
+
},
|
|
567
997
|
},
|
|
568
998
|
},
|
|
569
|
-
handler: (args) =>
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
999
|
+
handler: (args) => {
|
|
1000
|
+
const body = {
|
|
1001
|
+
title: args.title,
|
|
1002
|
+
description: args.description ?? '',
|
|
1003
|
+
companyId: args.companyId,
|
|
1004
|
+
statusId: args.statusId,
|
|
1005
|
+
channel: args.channel ?? 'API',
|
|
1006
|
+
};
|
|
1007
|
+
const passthrough = [
|
|
1008
|
+
'assignedTo',
|
|
1009
|
+
'assignedTeams',
|
|
1010
|
+
'categoryId',
|
|
1011
|
+
'organizationId',
|
|
1012
|
+
'departmentId',
|
|
1013
|
+
'projectId',
|
|
1014
|
+
'priority',
|
|
1015
|
+
'deadline',
|
|
1016
|
+
'plannedStartDate',
|
|
1017
|
+
'plannedEndDate',
|
|
1018
|
+
'estimatedEffort',
|
|
1019
|
+
'personDay',
|
|
1020
|
+
];
|
|
1021
|
+
for (const key of passthrough) {
|
|
1022
|
+
if (args[key] !== undefined && args[key] !== null) {
|
|
1023
|
+
body[key] = args[key];
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return systaFetch('POST', '/requests', { body });
|
|
1027
|
+
},
|
|
583
1028
|
},
|
|
584
1029
|
{
|
|
585
1030
|
name: 'prepare_create_request',
|
|
586
1031
|
description:
|
|
587
|
-
'Proactively fetch everything create_request needs for a company: the ACTIVE request form\'s required system fields PLUS valid id options (statuses, categories, organizations, departments). Call this FIRST whenever the user wants to "talep ac / open a request" so you fill categoryId/organizationId/statusId up front and avoid the 400 VALIDATION_FAILED roundtrip. Returns requiredFields[] and options{statusId,categoryId,organizationId,departmentId}. statusId options carry isFinal (true => a "tamamla/kapat" status). companyId is required (org/department options are scoped to it). A missing option list means that lookup is out of this key scope -- ask the user or use systa_api_call.',
|
|
1032
|
+
'Proactively fetch everything create_request needs for a company: the ACTIVE request form\'s required system fields PLUS valid id options (statuses, categories, organizations, departments). Call this FIRST whenever the user wants to "talep ac / open a request" so you fill categoryId/organizationId/statusId up front and avoid the 400 VALIDATION_FAILED roundtrip. Returns requiredFields[] and options{statusId,categoryId,organizationId,departmentId}. statusId options carry isFinal (true => a "tamamla/kapat" status). companyId is required (org/department options are scoped to it). NOTE ON companyId: this is the MUSTERI (client) company the request is FOR -- often DIFFERENT from your own user.companyId (which is the internal handler company). If the user has not stated a company, resolve by name (systa_api_call GET /companies?search=...) or ASK the user; do NOT default to user.companyId or pick one from the list at random. A missing option list means that lookup is out of this key scope -- ask the user or use systa_api_call.',
|
|
588
1033
|
inputSchema: {
|
|
589
1034
|
type: 'object',
|
|
590
1035
|
additionalProperties: false,
|
|
@@ -748,17 +1193,44 @@ const TOOLS = [
|
|
|
748
1193
|
});
|
|
749
1194
|
},
|
|
750
1195
|
},
|
|
1196
|
+
{
|
|
1197
|
+
name: 'list_allowed_transitions',
|
|
1198
|
+
description:
|
|
1199
|
+
"Preview the target statuses the request's CURRENT status is allowed to move to (durum gecisi onizle). SysTa restricts status transitions by workflow rules (category/company/department/priority scoped); calling this BEFORE change_request_status avoids a 400 TRANSITION_NOT_ALLOWED round-trip and lets you map a natural-language target ('test asamasi', 'tamamla', 'yeniden ac') to a specific statusId / statusKey from the returned list. Wraps GET /requests/:requestNumber/allowed-transitions.",
|
|
1200
|
+
inputSchema: {
|
|
1201
|
+
type: 'object',
|
|
1202
|
+
additionalProperties: false,
|
|
1203
|
+
required: ['requestNumber'],
|
|
1204
|
+
properties: {
|
|
1205
|
+
requestNumber: {
|
|
1206
|
+
type: 'integer',
|
|
1207
|
+
description: "Request number whose current status's allowed transitions to preview",
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
handler: (args) =>
|
|
1212
|
+
systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`),
|
|
1213
|
+
},
|
|
751
1214
|
{
|
|
752
1215
|
name: 'change_request_status',
|
|
753
1216
|
description:
|
|
754
|
-
'Move a SysTa request to a target status (durum degistir / tamamla / kapat / yeniden ac), optionally with a closing comment.
|
|
1217
|
+
'Move a SysTa request to a target status (durum degistir / tamamla / kapat / yeniden ac), optionally with a closing comment. BEFORE calling, use list_allowed_transitions(requestNumber) to see valid target statuses -- transition rules restrict which statuses are reachable from the current one. Give EITHER statusId OR statusKey (statusKey resolves via GET /statuses; a natural-language user phrase like "test asamasi" maps to statusKey~test). "tamamla/kapat" => a final status (isFinal=true); "yeniden ac" => a non-final one. A given comment is posted right after the transition (plain text, auto-wrapped to TipTap). A status change may trigger a workflow gate/approval -- if the API returns 400 TRANSITION_NOT_ALLOWED read the error.details, pick a valid target and retry; do NOT retry blindly.',
|
|
755
1218
|
inputSchema: {
|
|
756
1219
|
type: 'object',
|
|
757
1220
|
additionalProperties: false,
|
|
758
|
-
required: ['requestNumber'
|
|
1221
|
+
required: ['requestNumber'],
|
|
759
1222
|
properties: {
|
|
760
1223
|
requestNumber: { type: 'integer', description: 'Request number to transition' },
|
|
761
|
-
statusId: {
|
|
1224
|
+
statusId: {
|
|
1225
|
+
type: 'integer',
|
|
1226
|
+
description:
|
|
1227
|
+
'Target status id (from list_allowed_transitions). Provide statusId OR statusKey.',
|
|
1228
|
+
},
|
|
1229
|
+
statusKey: {
|
|
1230
|
+
type: 'string',
|
|
1231
|
+
description:
|
|
1232
|
+
"Target status key (e.g. 'completed', 'testing', 'in_progress'). Provide statusId OR statusKey.",
|
|
1233
|
+
},
|
|
762
1234
|
comment: {
|
|
763
1235
|
type: 'string',
|
|
764
1236
|
description: 'Optional closing/transition comment (plain text)',
|
|
@@ -766,8 +1238,45 @@ const TOOLS = [
|
|
|
766
1238
|
},
|
|
767
1239
|
},
|
|
768
1240
|
handler: async (args) => {
|
|
1241
|
+
let statusId = args.statusId ?? null;
|
|
1242
|
+
if (statusId == null && args.statusKey) {
|
|
1243
|
+
// Resolve statusKey -> statusId via allowed-transitions (workflow-aware:
|
|
1244
|
+
// prefers targets legal from the current status) with a fallback to the
|
|
1245
|
+
// global statuses list if allowed-transitions doesn't match the key.
|
|
1246
|
+
const allowedRes = await systaFetch(
|
|
1247
|
+
'GET',
|
|
1248
|
+
`/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`,
|
|
1249
|
+
);
|
|
1250
|
+
const allowed = allowedRes?.data?.data?.allowedTransitions || [];
|
|
1251
|
+
const hit = allowed.find(
|
|
1252
|
+
(t) => String(t.statusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
|
|
1253
|
+
);
|
|
1254
|
+
if (hit) {
|
|
1255
|
+
statusId = hit.id ?? hit.statusId;
|
|
1256
|
+
}
|
|
1257
|
+
if (statusId == null) {
|
|
1258
|
+
const statusesRes = await systaFetch('GET', '/statuses');
|
|
1259
|
+
const all = statusesRes?.data?.data?.statuses || statusesRes?.data?.data || [];
|
|
1260
|
+
const g = all.find(
|
|
1261
|
+
(s) => String(s.statusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
|
|
1262
|
+
);
|
|
1263
|
+
if (g) {
|
|
1264
|
+
statusId = g.id;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (statusId == null) {
|
|
1268
|
+
throw new Error(
|
|
1269
|
+
`change_request_status: statusKey='${args.statusKey}' could not be resolved -- call list_allowed_transitions to see valid keys`,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (statusId == null) {
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
'change_request_status: provide statusId or statusKey (see list_allowed_transitions)',
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
769
1278
|
const res = await systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
|
|
770
|
-
body: { statusId
|
|
1279
|
+
body: { statusId },
|
|
771
1280
|
});
|
|
772
1281
|
if (args.comment && String(args.comment).trim()) {
|
|
773
1282
|
const doc = {
|
|
@@ -784,7 +1293,7 @@ const TOOLS = [
|
|
|
784
1293
|
{
|
|
785
1294
|
name: 'update_request',
|
|
786
1295
|
description:
|
|
787
|
-
|
|
1296
|
+
"Update fields of an existing SysTa request (talebi guncelle/duzenle) -- PARTIAL: only the fields you pass change. For STATUS use change_request_status, for ASSIGNEE use assign_request, for COMMENTS use add_request_comment (focused intents). Use this for title/description/category/department/priority/deadline/planned dates/effort/project edits. Resolve categoryId/departmentId/projectId by name first. estimatedEffort/actualEffort/personDay are in CANONICAL DAYS -- if the user says 'N hours', divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
|
|
788
1297
|
inputSchema: {
|
|
789
1298
|
type: 'object',
|
|
790
1299
|
additionalProperties: false,
|
|
@@ -796,8 +1305,36 @@ const TOOLS = [
|
|
|
796
1305
|
categoryId: { type: 'integer', description: 'Category id (resolve by name)' },
|
|
797
1306
|
departmentId: { type: 'integer', description: 'Department id' },
|
|
798
1307
|
projectId: { type: 'integer', description: 'Link to project id' },
|
|
799
|
-
priority: {
|
|
800
|
-
|
|
1308
|
+
priority: {
|
|
1309
|
+
type: 'string',
|
|
1310
|
+
enum: ['low', 'normal', 'high', 'urgent', 'critical'],
|
|
1311
|
+
description: 'Priority key/level',
|
|
1312
|
+
},
|
|
1313
|
+
deadline: { type: 'string', description: 'Deadline (ISO date YYYY-MM-DD or ISO-8601)' },
|
|
1314
|
+
plannedStartDate: {
|
|
1315
|
+
type: 'string',
|
|
1316
|
+
description: 'Planned start (YYYY-MM-DD). Must be <= plannedEndDate.',
|
|
1317
|
+
},
|
|
1318
|
+
plannedEndDate: {
|
|
1319
|
+
type: 'string',
|
|
1320
|
+
description: 'Planned end (YYYY-MM-DD). Must be >= plannedStartDate.',
|
|
1321
|
+
},
|
|
1322
|
+
estimatedEffort: {
|
|
1323
|
+
type: 'number',
|
|
1324
|
+
minimum: 0,
|
|
1325
|
+
description:
|
|
1326
|
+
"Estimated effort in CANONICAL DAYS (person-days). '8 saat' => 8 / hoursPerDay.",
|
|
1327
|
+
},
|
|
1328
|
+
actualEffort: {
|
|
1329
|
+
type: 'number',
|
|
1330
|
+
minimum: 0,
|
|
1331
|
+
description: 'Actual effort in CANONICAL DAYS (person-days).',
|
|
1332
|
+
},
|
|
1333
|
+
personDay: {
|
|
1334
|
+
type: 'number',
|
|
1335
|
+
minimum: 0,
|
|
1336
|
+
description: 'Person-day scoring in CANONICAL DAYS.',
|
|
1337
|
+
},
|
|
801
1338
|
},
|
|
802
1339
|
},
|
|
803
1340
|
handler: (args) => {
|
|
@@ -839,10 +1376,43 @@ const TOOLS = [
|
|
|
839
1376
|
return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, { body });
|
|
840
1377
|
},
|
|
841
1378
|
},
|
|
1379
|
+
{
|
|
1380
|
+
name: 'create_request_board',
|
|
1381
|
+
description:
|
|
1382
|
+
'Create the kanban task board (gorev panosu) for a SysTa request when it does NOT yet have one. Call THIS when get_request_board or manage_request_card returns 404 KANBAN_BOARD_NOT_FOUND (code 2031) -- do not retry the read/manage call blindly. Optional templateId picks a preset column layout; without it a default column set is created. Discover templates with systa_api_call GET /task-boards/templates. After board create you can immediately use manage_request_card to add cards.',
|
|
1383
|
+
inputSchema: {
|
|
1384
|
+
type: 'object',
|
|
1385
|
+
additionalProperties: false,
|
|
1386
|
+
required: ['requestId'],
|
|
1387
|
+
properties: {
|
|
1388
|
+
requestId: {
|
|
1389
|
+
type: 'integer',
|
|
1390
|
+
description: 'Request number to attach the board to',
|
|
1391
|
+
},
|
|
1392
|
+
templateId: {
|
|
1393
|
+
type: 'integer',
|
|
1394
|
+
description: 'Optional task-board template id (GET /task-boards/templates lists them)',
|
|
1395
|
+
},
|
|
1396
|
+
boardName: { type: 'string', description: 'Optional board display name' },
|
|
1397
|
+
},
|
|
1398
|
+
},
|
|
1399
|
+
handler: (args) => {
|
|
1400
|
+
const body = {};
|
|
1401
|
+
if (args.templateId != null) {
|
|
1402
|
+
body.templateId = args.templateId;
|
|
1403
|
+
}
|
|
1404
|
+
if (args.boardName) {
|
|
1405
|
+
body.boardName = args.boardName;
|
|
1406
|
+
}
|
|
1407
|
+
return systaFetch('POST', `/requests/${encodeURIComponent(args.requestId)}/kanban`, {
|
|
1408
|
+
body,
|
|
1409
|
+
});
|
|
1410
|
+
},
|
|
1411
|
+
},
|
|
842
1412
|
{
|
|
843
1413
|
name: 'get_request_board',
|
|
844
1414
|
description:
|
|
845
|
-
"Read the kanban task board (gorev panosu) of a SysTa request: its columns and the cards in each. READ-only -- to add/edit a card use manage_request_card, to move/reorder use move_request_card; for the request's own fields/comments use get_request/add_request_comment. requestId is the request number.",
|
|
1415
|
+
"Read the kanban task board (gorev panosu) of a SysTa request: its columns and the cards in each. READ-only -- to add/edit a card use manage_request_card, to move/reorder use move_request_card; for the request's own fields/comments use get_request/add_request_comment. requestId is the request number. If the response is 404 KANBAN_BOARD_NOT_FOUND (code 2031) the request has NO board yet -- call create_request_board(requestId) FIRST, then retry.",
|
|
846
1416
|
inputSchema: {
|
|
847
1417
|
type: 'object',
|
|
848
1418
|
additionalProperties: false,
|
|
@@ -871,7 +1441,7 @@ const TOOLS = [
|
|
|
871
1441
|
{
|
|
872
1442
|
name: 'manage_request_card',
|
|
873
1443
|
description:
|
|
874
|
-
'Create a card in a board column (requestId + columnId + cardTitle) OR update an existing card (requestId + cardId, partial fields). Does NOT move/reorder cards -- use move_request_card; read ids first with get_request_board. Resolve columnId and assignee ids before calling.',
|
|
1444
|
+
'Create a card in a board column (requestId + columnId + cardTitle) OR update an existing card (requestId + cardId, partial fields). Does NOT move/reorder cards -- use move_request_card; read ids first with get_request_board. Resolve columnId and assignee ids before calling. PREREQUISITE: the request MUST have a kanban board -- if get_request_board or this call returns 404 KANBAN_BOARD_NOT_FOUND (code 2031), call create_request_board(requestId) first, then retry.',
|
|
875
1445
|
inputSchema: {
|
|
876
1446
|
type: 'object',
|
|
877
1447
|
additionalProperties: false,
|
|
@@ -892,6 +1462,12 @@ const TOOLS = [
|
|
|
892
1462
|
},
|
|
893
1463
|
assignedTeams: { type: 'array', items: { type: 'integer' } },
|
|
894
1464
|
priority: { type: 'string', description: 'low|normal|high|urgent|critical' },
|
|
1465
|
+
estimatedEffort: {
|
|
1466
|
+
type: 'number',
|
|
1467
|
+
minimum: 0,
|
|
1468
|
+
description:
|
|
1469
|
+
"Estimated effort in CANONICAL DAYS (person-days). If user says 'N hours' divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
|
|
1470
|
+
},
|
|
895
1471
|
tags: { type: 'array', items: { type: 'string' } },
|
|
896
1472
|
deadline: { type: 'string', description: 'ISO-8601 datetime' },
|
|
897
1473
|
cardColor: { type: 'string', description: '#RRGGBB hex' },
|
|
@@ -1064,6 +1640,32 @@ const TOOLS = [
|
|
|
1064
1640
|
return systaFetch('POST', '/projects', { body });
|
|
1065
1641
|
},
|
|
1066
1642
|
},
|
|
1643
|
+
{
|
|
1644
|
+
name: 'duplicate_project',
|
|
1645
|
+
description:
|
|
1646
|
+
"Clone/duplicate a SysTa project (POST /projects/:id/duplicate) -- copies the source project's DEFINITION (name, company/department/org, parent, dates, weight, planned effort, governance toggles) into a new project. Optional {newProjectName}. NOT COPIED: sub-projects, requests, board, milestones, snapshots, actual effort/progress -- these are start-fresh on the new project. Returns the new project's id/code. Requires the project.create scope.",
|
|
1647
|
+
inputSchema: {
|
|
1648
|
+
type: 'object',
|
|
1649
|
+
additionalProperties: false,
|
|
1650
|
+
required: ['id'],
|
|
1651
|
+
properties: {
|
|
1652
|
+
id: { type: 'integer', description: 'Source project id to clone from' },
|
|
1653
|
+
newProjectName: {
|
|
1654
|
+
type: 'string',
|
|
1655
|
+
description: "New project's name (optional; server appends '(Copy)' if omitted)",
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
handler: (args) => {
|
|
1660
|
+
const body = {};
|
|
1661
|
+
if (args.newProjectName) {
|
|
1662
|
+
body.newProjectName = args.newProjectName;
|
|
1663
|
+
}
|
|
1664
|
+
return systaFetch('POST', `/projects/${encodeURIComponent(args.id)}/duplicate`, {
|
|
1665
|
+
body,
|
|
1666
|
+
});
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1067
1669
|
{
|
|
1068
1670
|
name: 'get_project',
|
|
1069
1671
|
description: 'Get one SysTa project (proje) by its numeric id, including sub-projects.',
|
|
@@ -1372,10 +1974,48 @@ const TOOLS = [
|
|
|
1372
1974
|
}
|
|
1373
1975
|
},
|
|
1374
1976
|
},
|
|
1977
|
+
{
|
|
1978
|
+
name: 'create_plan_mode',
|
|
1979
|
+
description:
|
|
1980
|
+
"Create a plan mode / sprint scope (POST /plan/modes). A plan mode is the CONTAINER for a sprint's boards; create_plan_baseline then snapshots its live state. Boards is TECHNICALLY optional server-side but a mode WITHOUT boards produces empty baselines -- if you omit boards this tool auto-adds a single default board ({boardName:'Gorev Tahtasi'}) so create_plan_baseline works out of the box. Give boards[] explicitly when you want multiple boards or filterCriteria that pre-filters requests per board. Returns the created mode's id (use it for create_plan_baseline modeId).",
|
|
1981
|
+
inputSchema: {
|
|
1982
|
+
type: 'object',
|
|
1983
|
+
additionalProperties: false,
|
|
1984
|
+
required: ['name'],
|
|
1985
|
+
properties: {
|
|
1986
|
+
name: { type: 'string', description: 'Mode / sprint name (min 1 char)' },
|
|
1987
|
+
description: { type: 'string', description: 'Optional description' },
|
|
1988
|
+
companyId: {
|
|
1989
|
+
type: 'integer',
|
|
1990
|
+
description: 'Scope to a specific company (optional)',
|
|
1991
|
+
},
|
|
1992
|
+
boards: {
|
|
1993
|
+
type: 'array',
|
|
1994
|
+
description:
|
|
1995
|
+
'Boards for the mode. If omitted a single default board is inserted. Each board: {boardName, filterCriteria?, targetCriteria?, displayOrder?}.',
|
|
1996
|
+
items: { type: 'object', additionalProperties: true },
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
handler: (args) => {
|
|
2001
|
+
const body = { name: args.name };
|
|
2002
|
+
if (args.description !== undefined) {
|
|
2003
|
+
body.description = args.description;
|
|
2004
|
+
}
|
|
2005
|
+
if (args.companyId != null) {
|
|
2006
|
+
body.companyId = args.companyId;
|
|
2007
|
+
}
|
|
2008
|
+
body.boards =
|
|
2009
|
+
Array.isArray(args.boards) && args.boards.length > 0
|
|
2010
|
+
? args.boards
|
|
2011
|
+
: [{ boardName: 'Görev Tahtası', displayOrder: 1 }];
|
|
2012
|
+
return systaFetch('POST', '/plan/modes', { body });
|
|
2013
|
+
},
|
|
2014
|
+
},
|
|
1375
2015
|
{
|
|
1376
2016
|
name: 'create_plan_baseline',
|
|
1377
2017
|
description:
|
|
1378
|
-
'Create a plan baseline/snapshot for a plan mode (POST /plan/modes/:modeId/baselines). Omit boards -- the server snapshots the mode live state (built for AI clients with no board state). Returns baselineId for submit_plan_for_approval.',
|
|
2018
|
+
'Create a plan baseline/snapshot for a plan mode (POST /plan/modes/:modeId/baselines). Omit boards -- the server snapshots the mode live state (built for AI clients with no board state). Returns baselineId for submit_plan_for_approval. modeId comes from create_plan_mode.',
|
|
1379
2019
|
inputSchema: {
|
|
1380
2020
|
type: 'object',
|
|
1381
2021
|
additionalProperties: false,
|
|
@@ -1400,6 +2040,46 @@ const TOOLS = [
|
|
|
1400
2040
|
});
|
|
1401
2041
|
},
|
|
1402
2042
|
},
|
|
2043
|
+
{
|
|
2044
|
+
name: 'get_plan_baseline',
|
|
2045
|
+
description:
|
|
2046
|
+
"Get a single plan baseline (GET /plan/baselines/:baselineId). Blind-AI-friendly: fields defaults to 'summary' which STRIPS snapshotData (can be ~800KB on real plans) and returns just metadata + counts (baselineName, totalCards, totalEffort, approvalStatus, periodStart/End, createdAt, hasSnapshot boolean). Pass fields:'full' ONLY when you truly need the raw board/card snapshot (rare). For 'ne degisti / snapshot fark' use compare_plan_baseline, NOT the raw snapshot.",
|
|
2047
|
+
inputSchema: {
|
|
2048
|
+
type: 'object',
|
|
2049
|
+
additionalProperties: false,
|
|
2050
|
+
required: ['baselineId'],
|
|
2051
|
+
properties: {
|
|
2052
|
+
baselineId: { type: 'integer', description: 'Baseline id' },
|
|
2053
|
+
fields: {
|
|
2054
|
+
type: 'string',
|
|
2055
|
+
enum: ['summary', 'full'],
|
|
2056
|
+
description:
|
|
2057
|
+
"Response profile. Default: 'summary' (snapshot omitted, metadata only). 'full' returns the raw snapshot_data (heavy).",
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
},
|
|
2061
|
+
handler: (args) =>
|
|
2062
|
+
systaFetch('GET', `/plan/baselines/${encodeURIComponent(args.baselineId)}`, {
|
|
2063
|
+
query: { fields: args.fields ?? 'summary' },
|
|
2064
|
+
}),
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
name: 'compare_plan_baseline',
|
|
2068
|
+
description:
|
|
2069
|
+
"Compare a plan baseline snapshot (snapshot fark / baseline karsilastir) against the CURRENT board state (POST /plan/baselines/:baselineId/compare). Returns the diff (added/removed/changed cards, affected boards) WITHOUT streaming the raw baseline snapshot -- much smaller than GET /plan/baselines/:baselineId (which can be ~800KB). Use this whenever the user asks 'son snapshot ile bugunku durum arasindaki fark' / 'plani onceki halle karsilastir'. baselineId comes from create_plan_baseline or list_plan_baselines (systa_api_call GET /plan/baselines?modeId=). read_only.",
|
|
2070
|
+
inputSchema: {
|
|
2071
|
+
type: 'object',
|
|
2072
|
+
additionalProperties: false,
|
|
2073
|
+
required: ['baselineId'],
|
|
2074
|
+
properties: {
|
|
2075
|
+
baselineId: { type: 'integer', description: 'Baseline id to compare against live state' },
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
handler: (args) =>
|
|
2079
|
+
systaFetch('POST', `/plan/baselines/${encodeURIComponent(args.baselineId)}/compare`, {
|
|
2080
|
+
body: {},
|
|
2081
|
+
}),
|
|
2082
|
+
},
|
|
1403
2083
|
{
|
|
1404
2084
|
name: 'submit_plan_for_approval',
|
|
1405
2085
|
description:
|
|
@@ -1714,7 +2394,7 @@ const TOOLS = [
|
|
|
1714
2394
|
{
|
|
1715
2395
|
name: 'querybuilder_describe_fields',
|
|
1716
2396
|
description:
|
|
1717
|
-
"Discover queryable fields for a reportType: mode 'fields' (metadata/fieldKeys), 'options' (filter value options), or 'suggest' (q search). Never invent field names -- read mode='fields' first.",
|
|
2397
|
+
"Discover queryable fields for a reportType: mode 'fields' (metadata/fieldKeys), 'options' (filter value options), or 'suggest' (q search). Never invent field names -- read mode='fields' first. LARGE RESPONSE WARNING: mode='fields' can return 60KB+ (dozens of fields per reportType); if you are only looking for a common concept ('kapatilan/completed', 'atanan/assignee', 'kategori', 'tarih'), call mode='suggest' with a q term instead of dumping the whole list. Report reportType common fields: request→statusId/statusKey/completedAt/createdAt/priority/assignedTo/companyId; sla→slaStatus/breachedAt/dueAt; user→userId/isActive/isSuspended; project→statusId/lifecycle/healthScore. 'kapatilmis talep' = statusKey∈(completed,cancelled,rejected) OR isFinal=true (bkz. glossary 'kapatilmis talep').",
|
|
1718
2398
|
inputSchema: {
|
|
1719
2399
|
type: 'object',
|
|
1720
2400
|
additionalProperties: false,
|
|
@@ -1855,10 +2535,21 @@ const TOOLS = [
|
|
|
1855
2535
|
{
|
|
1856
2536
|
name: 'download_request_file',
|
|
1857
2537
|
description:
|
|
1858
|
-
'Download a SysTa attachment (
|
|
2538
|
+
'Download a SysTa attachment (any file the API key can access) to the LOCAL disk and ' +
|
|
1859
2539
|
'return the saved path. Requires the "file.download" scope; all access checks run ' +
|
|
1860
|
-
'server-side.
|
|
1861
|
-
'
|
|
2540
|
+
'server-side. Saves into a temp directory unless savePath is given. ' +
|
|
2541
|
+
'SysTa files live under SEVEN different sources -- fileId is resolved from ONE of these ' +
|
|
2542
|
+
'list endpoints (most user-visible files are actually COMMENT or KANBAN attachments, not ' +
|
|
2543
|
+
'top-level request attachments): ' +
|
|
2544
|
+
'(1) request attachment: list_request_files(requestNumber) / GET /requests/:n/files, ' +
|
|
2545
|
+
'(2) comment attachment: list_request_comments(requestNumber) -- each comment carries fileIds[], ' +
|
|
2546
|
+
'(3) kanban card attachment: systa_api_call GET /requests/:reqId/kanban/cards/:cardId/files, ' +
|
|
2547
|
+
'(4) kanban card comment attachment: systa_api_call GET /requests/:reqId/kanban/cards/:cardId/comments (comment.fileIds[]), ' +
|
|
2548
|
+
'(5) project file: systa_api_call GET /projects/:id/files, ' +
|
|
2549
|
+
'(6) sub-form submission attachment: systa_api_call GET /form-submissions/:id/files, ' +
|
|
2550
|
+
'(7) announcement attachment: systa_api_call GET /announcements/:id. ' +
|
|
2551
|
+
'If list_request_files returns empty, the request likely has NO top-level attachments ' +
|
|
2552
|
+
'yet -- check comments (source 2) BEFORE concluding there is no file.',
|
|
1862
2553
|
inputSchema: {
|
|
1863
2554
|
type: 'object',
|
|
1864
2555
|
additionalProperties: false,
|
|
@@ -2098,20 +2789,30 @@ const TOOL_MAP = new Map(TOOLS.map((tool) => [tool.name, tool]));
|
|
|
2098
2789
|
const TOOL_REQUIRED_PERMISSION = {
|
|
2099
2790
|
list_requests: 'request.read',
|
|
2100
2791
|
get_request: 'request.tabs.general.view',
|
|
2792
|
+
list_request_files: 'request.tabs.files.view',
|
|
2793
|
+
list_request_comments: 'request.comment.read',
|
|
2794
|
+
list_request_history: 'request.tabs.history.view',
|
|
2795
|
+
list_lookup: 'request.read',
|
|
2101
2796
|
list_projects: 'project.read',
|
|
2797
|
+
list_users: 'user.read',
|
|
2798
|
+
resolve_user_by_name: 'user.read',
|
|
2102
2799
|
create_request: 'request.create',
|
|
2103
2800
|
prepare_create_request: 'request.create',
|
|
2104
2801
|
add_request_comment: 'request.comment.create',
|
|
2105
2802
|
change_request_status: 'request.update',
|
|
2803
|
+
list_allowed_transitions: 'request.read',
|
|
2106
2804
|
update_request: 'request.update',
|
|
2805
|
+
bulk_update_requests: 'request.update',
|
|
2107
2806
|
assign_request: 'request.update',
|
|
2108
2807
|
get_request_board: 'request.read',
|
|
2808
|
+
create_request_board: 'request.update',
|
|
2109
2809
|
manage_request_card: 'request.update',
|
|
2110
2810
|
move_request_card: 'request.update',
|
|
2111
2811
|
request_approval: 'request.approval.create',
|
|
2112
2812
|
respond_to_approval: 'request.approval.respond',
|
|
2113
2813
|
list_pending_approvals: 'request.approval.view',
|
|
2114
2814
|
create_project: 'project.create',
|
|
2815
|
+
duplicate_project: 'project.create',
|
|
2115
2816
|
get_project: 'project.read',
|
|
2116
2817
|
update_project: 'project.update',
|
|
2117
2818
|
get_project_health: 'project.read',
|
|
@@ -2121,7 +2822,10 @@ const TOOL_REQUIRED_PERMISSION = {
|
|
|
2121
2822
|
achieve_milestone: 'project.update',
|
|
2122
2823
|
manage_project_stakeholders: 'project.update',
|
|
2123
2824
|
manage_project_notes: 'project.update',
|
|
2825
|
+
create_plan_mode: 'request.update',
|
|
2124
2826
|
create_plan_baseline: 'request.update',
|
|
2827
|
+
get_plan_baseline: 'request.read',
|
|
2828
|
+
compare_plan_baseline: 'request.read',
|
|
2125
2829
|
submit_plan_for_approval: 'plan.approval.submit',
|
|
2126
2830
|
respond_to_plan_approval: 'plan.approval.review',
|
|
2127
2831
|
get_plan_critical_path: 'request.read',
|