toga-ai 1.0.34 → 1.0.36

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.
@@ -3,3 +3,4 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
+ | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` contains three private helpers that handle bidirectional file-attachment sync between TOGA 2 and other systems fo | library/app/api/toga2.php |
@@ -0,0 +1,93 @@
1
+ ---
2
+ title: Elite Freshservice Sync (library)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: elite
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-06-10
10
+ owners: []
11
+ files:
12
+ - library/app/api/toga2.php
13
+ related:
14
+ - clients/elite/profile.md
15
+ - 2.0/apps/worker2/features/elite-freshservice-sync.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ `App_Api_Toga2` in `library/app/api/toga2.php` contains three private helpers that handle
21
+ bidirectional file-attachment sync between TOGA 2 and other systems for Elite:
22
+
23
+ | Method | Direction | Purpose |
24
+ |---|---|---|
25
+ | `syncFreshserviceConversationAttachmentsIntoToga2` | Freshservice → TOGA 2 | Download attachments from Freshservice presigned S3 URLs and store in TOGA 2 |
26
+ | `syncToga2NoteFilesIntoTogadesk1` | TOGA 2 → TOGaDesk | Copy TOGA 2 note files to TOGaDesk reply attachments |
27
+ | `syncTogadesk1NoteFilesIntoToga2` | TOGaDesk → TOGA 2 | Copy TOGaDesk reply attachments to TOGA 2 note files |
28
+
29
+ ## Key files / entry points
30
+
31
+ - `library/app/api/toga2.php` — all three methods live here, called from larger
32
+ sync orchestration methods in the same class
33
+ - `App_Api_Toga2::send()` — the HTTP client used by all three
34
+
35
+ ## How dedup works (all three methods)
36
+
37
+ All three methods avoid re-uploading files by checking what is already linked before
38
+ uploading. The pattern uses `depth=5` on the parent ticket-note:
39
+
40
+ ```php
41
+ $togaNote = App_Api_Toga2::send($clientUuid, $apiKey, $apiSecret,
42
+ 'GET', '/ticket-notes/' . $ticketNoteUuid, null, ['depth' => 5], true, $endpoint);
43
+
44
+ $existingFileNames = [];
45
+ foreach (($togaNote->data->ticketNotes->ticketNoteFiles ?? []) as $link) {
46
+ if (!empty($link->file->name)) {
47
+ $existingFileNames[] = $link->file->name;
48
+ }
49
+ }
50
+ ```
51
+
52
+ Dedup is by **filename** — if `$file->name` is already in `$existingFileNames`, skip.
53
+
54
+ ## TOGA 2 API — critical gotchas
55
+
56
+ **Response key is `ticketNoteFiles`, not `ticketNotesFiles`.**
57
+ The extra `s` before `Files` causes a silent empty array — dedup never fires and files
58
+ get re-uploaded on every sync run. Always use `ticketNoteFiles`.
59
+
60
+ **`GET /ticket-notes/{uuid}/ticket-note-files` is NOT a list endpoint.**
61
+ This nested route uses `childPolicy = MATCH`:
62
+ - 0 bridge records → 404 EV-6
63
+ - 1 record → 200 single object
64
+ - 2+ records → 404 EV-14 ("multiple found, expected one")
65
+
66
+ Always use `depth=5` on the parent note for listing — never the nested route.
67
+
68
+ **`throwExceptionOnApiError` (8th param of `App_Api_Toga2::send()`).**
69
+ Pass `true` for the dedup GET — a valid note UUID should always return 200. Passing
70
+ `false` silently swallows errors and the dedup array stays empty.
71
+
72
+ ## Bridge route for linking files to notes
73
+
74
+ ```php
75
+ // POST /ticket-notes/{uuid}/ticket-note-files
76
+ App_Api_Toga2::send($clientUuid, $apiKey, $apiSecret, 'POST',
77
+ '/ticket-notes/' . $ticketNoteUuid . '/ticket-note-files',
78
+ ['file' => ['uuid' => $fileUuid]],
79
+ null, true, $endpoint);
80
+ ```
81
+
82
+ ## Freshservice attachment handling
83
+
84
+ Freshservice attachment URLs are **presigned S3 URLs that expire**. The sync must run
85
+ immediately at webhook time — do not defer or re-queue the download step.
86
+
87
+ ## TOGaDesk file storage
88
+
89
+ `syncToga2NoteFilesIntoTogadesk1` writes files to disk at:
90
+ ```
91
+ /var/www/html/ontrack/desk/uploads/{id}-{filename}
92
+ ```
93
+ Stored as `{id}-{filename}` in the TOGaDesk `files.file` column.
@@ -5,3 +5,4 @@
5
5
  | [Worker (worker2) Architecture](architecture.md) | Worker (repo `worker2`) is an AWS Elastic Beanstalk **Worker Tier** application that processes background jobs. | worker2/Controller/Index.php, worker2/Worker/, worker2/LambdaFunctions/, _underscore/Worker.php |
6
6
  | [ClickUp Project & Opportunity Multi-List Routing](features/clickup-project-routing.md) | Routes ClickUp tasks into the correct **secondary multi-list memberships** based on their custom-field values, via the `clickup` webhook. | worker2/Worker/Clickup/Project.php, worker2/Worker/Clickup.php |
7
7
  | [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
8
+ | [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Elite Freshservice Sync (worker2)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: elite
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-06-10
10
+ owners: []
11
+ files:
12
+ - worker2/Worker/Elite.php
13
+ - worker2/Config/dev-kmaramreddy-laptop.ini
14
+ related:
15
+ - clients/elite/profile.md
16
+ - 1.0/apps/library/features/elite-freshservice-sync.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2.
22
+ Dispatched via action `Elite/Webhook` on the worker queue.
23
+
24
+ ## Key files / entry points
25
+
26
+ - `Worker/Elite.php` — `_Worker_Elite` class, all sync logic lives here
27
+ - `_Component_Api_Elite` (`_underscore`) — authenticated Freshservice HTTP client
28
+ - `_Component_Api_Toga` (`_underscore`) — authenticated TOGA 2 HTTP client
29
+
30
+ ## How the webhook flow works
31
+
32
+ 1. Freshservice fires a webhook to `webhook.togahub.com/elite`
33
+ 2. Lambda inserts a `WorkerJobs` row with action `Elite/Webhook`
34
+ 3. Worker picks up the job → calls `_Worker_Elite::Webhook($payload, $headers)`
35
+ 4. Worker syncs the ticket and all conversations into TOGA 2
36
+
37
+ ## Constants
38
+
39
+ ```php
40
+ const CLIENT_UUID_ELITE = '97627e9d-605f-3ded-9e83-97a4ec7a0c15';
41
+ const API_UUID_ELITE = 'ee08cf3d-caee-462d-adb5-acc5c36b1f01';
42
+ const API_SECRET_ELITE = 'c2701d21-1908-4895-aaed-2e6456710e8d';
43
+ ```
44
+
45
+ ## Freshservice API — gotchas
46
+
47
+ **No single-conversation GET endpoint.**
48
+ `GET /api/v2/conversations/{id}` only supports PATCH/PUT/DELETE — it returns 405 on GET.
49
+ To fetch a specific conversation, retrieve the full list for the ticket:
50
+
51
+ ```php
52
+ $conversations = _Component_Api_Elite::send('GET', '/tickets/' . $ticketId . '/conversations', null);
53
+ foreach ($conversations->conversations as $conv) {
54
+ if ((string) $conv->id === $targetId) { ... }
55
+ }
56
+ ```
57
+
58
+ **Attachment URLs are presigned S3 URLs — they expire.**
59
+ Call the attachment sync method immediately at webhook time. Do not queue the download.
60
+
61
+ **Attachment shape on a conversation object:**
62
+ ```
63
+ $attachment->name
64
+ $attachment->content_type
65
+ $attachment->size
66
+ $attachment->attachment_url // presigned S3 URL
67
+ ```
68
+
69
+ ## TOGA 2 API — gotchas
70
+
71
+ **`childPolicy = MATCH` nested routes are get-ONE, not a list.**
72
+ `GET /ticket-notes/{uuid}/ticket-note-files` returns:
73
+ - 404 EV-6 when 0 bridge records exist
74
+ - 200 object when exactly 1 exists
75
+ - 404 EV-14 ("multiple found, expected one") when 2+ exist
76
+
77
+ **Never use this route for listing or dedup.** Use `depth=5` on the parent note instead:
78
+
79
+ ```php
80
+ $togaNote = _Component_Api_Toga::send(..., 'GET', '/ticket-notes/' . $uuid, null, ['depth' => 5], true, $endpoint);
81
+ $existingFileNames = [];
82
+ foreach (($togaNote->data->ticketNotes->ticketNoteFiles ?? []) as $link) {
83
+ if (!empty($link->file->name)) {
84
+ $existingFileNames[] = $link->file->name;
85
+ }
86
+ }
87
+ ```
88
+
89
+ Response key is `ticketNoteFiles` (not `ticketNotesFiles` — no `s` before `Files`).
90
+
91
+ ## Local dev setup
92
+
93
+ To test worker2 Elite code locally against production TOGA 2 and Freshservice:
94
+
95
+ 1. **Dev config** — `[api]` section needs both keys (`endpoint` for some callers,
96
+ `_` for `_Config::api('_')`):
97
+ ```ini
98
+ [api]
99
+ _ = "https://api.togahub.com/v2"
100
+ endpoint = "https://api.togahub.com/v2"
101
+ ```
102
+
103
+ 2. **`ClientLogs` database** must exist locally — the framework logs every API call there:
104
+ ```sql
105
+ CREATE DATABASE IF NOT EXISTS ClientLogs CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
106
+ USE ClientLogs;
107
+ CREATE TABLE `Api` (
108
+ `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
109
+ `uuid` CHAR(36) NOT NULL,
110
+ `dtStamp` DATETIME NOT NULL,
111
+ `transactionId` VARCHAR(64) NULL,
112
+ `apiId` INT UNSIGNED NULL,
113
+ `userId` INT UNSIGNED NULL,
114
+ `isAuthRequest` TINYINT UNSIGNED NOT NULL DEFAULT 0,
115
+ `direction` ENUM('IN','OUT') NOT NULL,
116
+ `source` VARCHAR(255) NULL,
117
+ `sourceIp` VARCHAR(45) NULL,
118
+ `instanceId` VARCHAR(128) NULL,
119
+ `executionTimeSeconds` DECIMAL(10,4) NULL,
120
+ `method` VARCHAR(10) NOT NULL,
121
+ `hostname` VARCHAR(255) NOT NULL,
122
+ `route` VARCHAR(1024) NOT NULL,
123
+ `responseCode` SMALLINT UNSIGNED NULL,
124
+ `queryString` TEXT NULL,
125
+ `requestHeaders` TEXT NULL,
126
+ `requestPayload` MEDIUMTEXT NULL,
127
+ `responseHeaders` TEXT NULL,
128
+ `responsePayload` MEDIUMTEXT NULL
129
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
130
+ ```
131
+
132
+ 3. **Bootstrap** for a standalone test script:
133
+ ```php
134
+ chdir(__DIR__);
135
+ putenv('ENVIRONMENT=dev-kmaramreddy-laptop');
136
+ $_ENV['ENVIRONMENT'] = 'dev-kmaramreddy-laptop';
137
+ require_once 'vendor/autoload.php';
138
+ require '_underscore.php';
139
+ ```
140
+
141
+ 4. **PHP version** — worker2 vendor targets PHP >= 8.5. Local PHP 8.1 requires a
142
+ temporary bypass in `vendor/composer/platform_check.php` — revert before committing.
@@ -4,13 +4,13 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 1 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 2 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 1 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
 
10
10
  ## 2.0 framework
11
11
 
12
12
  - **_underscore** (_Underscore) _(framework core)_ — 3 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
13
- - **worker2** (Worker) — 3 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
13
+ - **worker2** (Worker) — 4 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
14
14
  - **api2** (API) — 1 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
15
15
  - **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
16
16
  - **toga2-supply** (TOGa Supply) — 2 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -19,4 +19,5 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
 
20
20
  - **compass-canada** → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
21
21
  - **compass-usa** → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
22
+ - **elite** → [clients/elite/INDEX.md](clients/elite/INDEX.md)
22
23
 
@@ -0,0 +1,5 @@
1
+ # Client: elite
2
+
3
+ | Doc | Framework | Summary | Files |
4
+ |-----|-----------|---------|-------|
5
+ | [Elite Client Profile](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, library/app/api/toga2.php |
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Elite Client Profile
3
+ framework: "2.0"
4
+ project: Worker
5
+ client: elite
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-06-10
9
+ owners: []
10
+ files:
11
+ - worker2/Worker/Elite.php
12
+ - library/app/api/toga2.php
13
+ related:
14
+ - 2.0/apps/worker2/features/elite-freshservice-sync.md
15
+ - 1.0/apps/library/features/elite-freshservice-sync.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ Elite is a managed-services client that uses **Freshservice** as their helpdesk platform.
21
+ TOGA 2 is the source of truth for tickets, notes, and file attachments. The integration
22
+ keeps Freshservice and TOGA 2 in sync via webhook-driven worker jobs.
23
+
24
+ ## TOGA 2 API credentials
25
+
26
+ Constants live in `worker2/Worker/Elite.php`:
27
+
28
+ ```php
29
+ const CLIENT_UUID_ELITE = '97627e9d-605f-3ded-9e83-97a4ec7a0c15';
30
+ const API_UUID_ELITE = 'ee08cf3d-caee-462d-adb5-acc5c36b1f01';
31
+ const API_SECRET_ELITE = 'c2701d21-1908-4895-aaed-2e6456710e8d';
32
+ ```
33
+
34
+ The library version of the sync methods (`library/app/api/toga2.php`) passes these in as
35
+ parameters — they are not constants there.
36
+
37
+ ## Freshservice instance
38
+
39
+ - Base URL: `https://eliteservicedesk.freshservice.com`
40
+ - Configured in `_underscore` framework via `_Component_Api_Elite`
41
+ - API logs written to the `ClientLogs` database (`ClientLogs.Api` table) — this DB must
42
+ exist locally when running worker2 scripts against production Freshservice
43
+
44
+ ## Key custom fields
45
+
46
+ | TOGA 2 field | Purpose |
47
+ |---|---|
48
+ | `c_eliteTicketId` | Freshservice ticket numeric ID (on `Tickets`) |
49
+ | `c_eliteConversationId` | Freshservice conversation ID (on `TicketNotes`) |
50
+ | `c_eliteUserId` | Freshservice user ID (on `Contacts`) |
51
+ | `c_eliteCategoryId` | Freshservice category ID (on `TicketCategories`) |
52
+ | `c_eliteGroupId` | Freshservice group ID (on `TicketTeams`) |
53
+
54
+ ## Systems involved
55
+
56
+ | System | Role |
57
+ |---|---|
58
+ | Freshservice | Client helpdesk — source of ticket/conversation events |
59
+ | TOGA 2 (`api2`) | System of record — tickets, notes, contacts, files |
60
+ | worker2 `_Worker_Elite` | Webhook processor — syncs Freshservice events into TOGA 2 |
61
+ | library `App_Api_Toga2` | Shared sync helpers (note-file bidirectional sync) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",
@@ -112,19 +112,45 @@ Ask these in one message. **Present the repos/projects you already know** (read
112
112
 
113
113
  1. **Framework** — 1.0, 2.0, or **both**?
114
114
  2. **Layer** — front-end, back-end, or hybrid?
115
- 3. **Repo(s) / project(s)** — list the registered repos for the chosen framework(s) as
116
- options (show `repo` and `project`), plus **"a new repo not listed"**. **This is a
117
- multi-select (checkbox), not a single choice.** A session often spans more than one
118
- repo/project, so always let the developer pick several. When asking via the question
119
- tool, set `multiSelect: true` — never force a single-select (radio) here. If they pick
120
- the new-repo option, run **New-repo onboarding** (below) for each new repo before
121
- continuing.
122
- 4. **Client** — which client is this for, or "shared / internal"? (List `clients/` folders
123
- you know of, plus "a new client".)
115
+ 3. **Repo(s) / project(s)** — **list EVERY registered repo for the chosen framework(s)**
116
+ do not abbreviate or show only a few. **This is a multi-select (checkbox), not a single
117
+ choice;** a session often spans more than one repo/project, so always let the developer
118
+ pick several. If they pick a repo not listed, run **New-repo onboarding** (below) for
119
+ each new repo before continuing.
120
+ 4. **Client** which client is this for, or "shared / internal"? **List EVERY folder under
121
+ `knowledge/clients/`** plus "shared / internal" and "a new client" — do not show only a
122
+ subset.
124
123
  5. **What are you building or changing?** (a sentence — used to pick relevant feature docs.)
125
124
 
126
125
  If any answer is unclear, ask again. Do not guess the framework, repo, project, or client.
127
126
 
127
+ ### How to present the repo and client choices (questions 3 and 4)
128
+
129
+ **Do NOT use the option-chip / radio question control for questions 3 and 4.** That
130
+ control caps at ~4 options, which silently truncates the list — with 7+ repos or 3+
131
+ clients the developer never sees the full set. Instead, **render the full list as a
132
+ numbered markdown list in a normal message** and ask the developer to reply with the
133
+ numbers they want (e.g. "1, 3, 5"). This shows every repo and every client with no cap
134
+ and naturally supports multi-select.
135
+
136
+ ```
137
+ **Which repo(s)/project(s)?** (reply with the numbers, multiple OK)
138
+ 1. _underscore (_Underscore — 2.0 core)
139
+ 2. worker2 (Worker — 2.0 app)
140
+ 3. api2 (API — 2.0 app)
141
+ ... every registered repo for the chosen framework(s) ...
142
+ N. a new repo not listed
143
+
144
+ **Which client?** (one, or "shared / internal")
145
+ 1. compass-canada
146
+ 2. compass-usa
147
+ ... every folder under knowledge/clients/ ...
148
+ S. shared / internal
149
+ X. a new client
150
+ ```
151
+
152
+ Framework (q1) and layer (q2) have ≤4 options, so the chip control is fine for those.
153
+
128
154
  ## Step 3 — Resolve the repos this work needs (local paths, lazily)
129
155
 
130
156
  Compute the load set with `node "<TEAM_REPO>/knowledge.js" deps --repo=<chosen-repo>`