toga-ai 1.0.1016 → 1.0.1017

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.
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
10
- owners: ["bala", "tcox", "dfranks", "apeterson", "jcardinal"]
9
+ updated: 2026-10-14
10
+ owners: ["bala", "tcox", "dfranks", "apeterson", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - _underscore/Email.php
13
13
  - _underscore/String.php
@@ -102,6 +102,14 @@ derive one from the other).
102
102
  inbox.** Whatever the caller rendered (placeholders resolved, built tables, escaping applied) is
103
103
  in the `body` column the moment `send()` returns. This is the cheapest way to verify a new email:
104
104
  query the row, not your mail client.
105
+ - **⚠ client-sandbox does not deliver email (seen 2026-10-13).** Two separate faults:
106
+ 1. Some sandbox `Logs_<Client>` DBs lack the `Email` / `EmailAttachment` tables, so `_Email::send()`
107
+ fails with 500 `Table 'Logs_Elite.Email' doesn't exist`. Fix: create them from the
108
+ `dbchanges2/Logs_Client` blank DB plus the `2026-09-23a` EmailReview columns.
109
+ 2. Even with the tables, the `Infrastructure/Email/Send/Run` cron is configured but `PENDING` rows
110
+ never go out on client-sandbox (`Logs_Compass` had 95 stuck). In non-prod, mail is rewritten
111
+ to `devteam@togatech.com` ("DEVELOPMENT MODE") anyway — so read the link/body from the
112
+ `Logs_<Client>.Email` row instead of waiting for mail.
105
113
  - **⚠ `debug_mode` / `send_debug_emails_to` redirects at QUEUE time ONLY — never at send time.**
106
114
  The redirect happens inside `_Email::send()` as it writes the `PENDING` row, so the row is
107
115
  **already addressed** by the time the Send worker reads it. The worker does no redirect of its
@@ -202,6 +210,7 @@ derive one from the other).
202
210
  for a hotfix.
203
211
 
204
212
  ## Change history
213
+ - 2026-10-14 — Gotcha: client-sandbox missing `Logs_<Client>.Email` tables and a Send cron that never drains `PENDING` rows (rgirish)
205
214
  - 2026-09-23 — Review hold: `HELD`/`CANCELLED` statuses, `emailTemplateId`/`reviewOfEmailId`/
206
215
  `dtReviewed`/`reviewedBy` columns, `setIsHeld`/`setEmailTemplateId`/`setReviewOfEmailId`;
207
216
  `send()` now returns the log id; stale "enqueues" docblock fixed. (jcardinal)
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [bala]
9
+ updated: 2026-10-14
10
+ owners: [bala, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/SalesOrder.php
13
13
  - _underscore/Model/Quad/SalesOrder.php
@@ -114,16 +114,30 @@ fulfillment statuses — filter widened to `['_picked','_packed','_shipped']`:
114
114
  |---|---|---|
115
115
  | Base, Quad (non-SA) | Pending → Picked → Packed → Partially Fulfilled → Fulfilled | all IF stages |
116
116
  | Compass USA + Canada | Pending → Partially Fulfilled → Fulfilled | **shipped IF stage only**; fulfilled-vs-partial rule differs per region (`_fulfillmentStatus()`) |
117
- | Elite | pendingFulfillment → partiallyFulfilled → fulfilled → shipped → delivered (+ canceled) | fulfillment + shipment state; **no approval stage** |
117
+ | Elite | pendingFulfillment → picked → packed → partiallyFulfilled → shipped / fulfilled → delivered (+ canceled) | all IF stages + shipment state; **no approval stage** |
118
118
 
119
119
  Quad `SA%` orders delegate to the base machine; an explicit `salesOrderStageId` overrides the
120
120
  calculated status everywhere.
121
121
 
122
122
  **Elite is a third policy, not a variant of Compass.** `_Model_Elite_SalesOrder::_status`
123
- (`FIELD_SQL` override, with four private helper builders, plus `_Model_Elite_SalesOrderStatus`) walks
124
- a **six-status ladder**: `pendingFulfillment`, `partiallyFulfilled`, `fulfilled`, `shipped`,
125
- `delivered`, `canceled`. The two new statuses (`shipped`, `delivered`) were seeded into
126
- `Client_Elite.SalesOrderStatuses`. **Elite has no approval model at all**, so the Compass status
123
+ (`FIELD_SQL` override, with private helper builders, plus `_Model_Elite_SalesOrderStatus`) walks
124
+ an **eight-status ladder**: Pending Fulfillment → Picked → Packed → Partially Fulfilled →
125
+ Shipped / Fulfilled → Delivered, plus Canceled (covers NetSuite *Cancelled* and *Closed*).
126
+ `shipped`/`delivered` were added first; **`picked`/`packed` were added 2026-10-08**:
127
+ - When shipped qty = 0 and a **picked** NetSuite IF exists → `picked`; else if a **packed** IF
128
+ exists → `packed`. Picked wins when both exist (same as the base `_Model_Client_SalesOrder`).
129
+ The counts come from the private helper `buildCountOfFulfillmentsWithStatus()`.
130
+ - No import change was needed: the 1.0 sync (`sync_togasupply_elite.php` →
131
+ `syncItemFulfillmentFromNetsuite`) already imported `_picked`/`_packed` IFs into
132
+ `ItemFulfillmentStages`. `_status` only counted shipped, so orders jumped Pending Fulfillment →
133
+ Delivered.
134
+ - Data: `dbchanges2/Client_Elite/2026-10-08a - SalesOrderStatusPickedPacked.sql` (picked `F5A623`,
135
+ packed `4DBCC4`) and `dbchanges2/Core/2026-10-08a - SalesOrderStatusVocabularyFulfillmentTerms.sql`
136
+ (Core `Messages` + `sales-order-status` `VocabularyTerms` for picked/packed/shipped/delivered).
137
+ - **Decision:** NetSuite order-header billing statuses (*Pending Billing*, *Billed*) are **not**
138
+ shown as their own Elite status.
139
+ - Prod-verified 2026-10-14: SR-14847 / SO 292745 shows Picked.
140
+ **Elite has no approval model at all**, so the Compass status
127
141
  logic — which is written around the MR/MA approval flow — does not apply and must not be copied
128
142
  across. The override also fixed a **`billed` → `pendingFulfillment` mislabel affecting 13 orders**.
129
143
 
@@ -197,6 +211,17 @@ Fan-out (`Client/`, every client) + a Compass-Canada-specific set. Execution is
197
211
  `_Model_Client_ItemFulfillmentStatus`); only the shipped check uses a constant. Adding a base IF
198
212
  picked/packed constant would let the literal be removed.
199
213
 
214
+ - **A new order-status slug needs TWO data rows, or the toga25-supply UI shows a blank badge.**
215
+ The table pill reads `Client_<Tenant>.SalesOrderStatuses` (name + `colorHex`); the record-modal
216
+ header badge reads the **Core** `sales-order-status` vocabulary terms (`VocabularyTerms` +
217
+ `Messages`). Elite's header badge was blank for `shipped`/`delivered` until the Core terms were
218
+ added (2026-10-08). No frontend code change is needed for a new slug.
219
+
220
+ ## Change history
221
+ - 2026-10-14 — Elite `_status` now shows Picked / Packed from NetSuite IF stages; Core vocabulary
222
+ terms added for picked/packed/shipped/delivered; billing statuses kept out by decision (rgirish)
223
+ - 2026-09-16 — Compass per-region `_fulfillmentStatus()` seam; Elite six-status ladder documented (bala)
224
+
200
225
  ## Related
201
226
  - [Recursive Item Fulfillments](recursive-item-fulfillments.md) — the engine that mirrors IFs (and
202
227
  now their stages) up the chain.
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [ajean]
9
+ updated: 2026-10-14
10
+ owners: [ajean, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/User.php
13
13
  - api2/Component/Api/V2/V2.php
@@ -16,6 +16,8 @@ related:
16
16
  - ./record-change-audit-log.md
17
17
  - ../../../1.0/apps/tools/features/compass-user-persona-admin.md
18
18
  - ../../../clients/compass-usa/features/people-file-user-lifecycle.md
19
+ - ../../toga25-supply/features/forgot-password-and-create-password.md
20
+ - ./email-send-pipeline.md
19
21
  ---
20
22
 
21
23
  Why `Users.email` in 2.0 is a login identity + tenant-routing key (forgot-password takeover primitive, domain routing); open before building any UI/job that edits an email address.
@@ -35,13 +37,13 @@ Verified on code 2026-09-04 while adding email editing to the [Tools Compass adm
35
37
 
36
38
  Key entry points:
37
39
 
38
- - **`_underscore/Model/Client/User.php:95-105`** — the forgot-password lookup and token mint.
40
+ - **`_underscore/Model/Client/User.php` `sendForgotPasswordEmail()`** — the forgot-password lookup and token mint; `updatePassword()` checks the token and sets the password.
39
41
  - **`api2/Component/Api/V2/V2.php:762-765`** — binds the authenticated session to the row carrying the address.
40
42
  - **`api2/Component/Api/V2/V2.php:611-621`** — resolves `clientId` from the email **domain** via `Core.ClientEmailDomains` when the request host does not identify the client.
41
43
 
42
44
  ### The forgot-password chain (the takeover primitive)
43
45
 
44
- `_underscore/Model/Client/User.php:95-105`:
46
+ `_underscore/Model/Client/User.php` `sendForgotPasswordEmail()`:
45
47
 
46
48
  1. Selects `Users` **by email** with **no `isActive` filter** and **no duplicate handling**.
47
49
  2. `fetchRow()` takes the **FIRST** row returned — order is not defined.
@@ -53,6 +55,20 @@ So whoever controls an address that appears on a `Users` row can request, receiv
53
55
  - A **dead (inactive) row** can be password-reset but **cannot be logged into**.
54
56
  - The chain works end-to-end on an **ACTIVE** user.
55
57
 
58
+ ### The two scripted APIs and the reset token
59
+
60
+ - **`GET /users/sendForgotPasswordEmail?userEmail=`** and **`GET /users/updatePassword?password=&token=`**
61
+ — `Core.RecordScripts` 8 and 9, **GET only**. The `Public` role needs `AclRecordScripts` rows for
62
+ both, or a logged-out user cannot call them.
63
+ - The emailed link is `<request Origin>/create-password?token=…`. The host comes from the JWT `aud`
64
+ (the request Origin), so it **must be a `Core.Domains` row** for that client.
65
+ - Email body comes from the `EmailTemplates` row `623c8869-4c59-409d-a6ff-c5d8818c9161` (per client
66
+ DB; Talos uses its own template).
67
+ - **Token lifetime is 30 minutes** (`const PASSWORD_RESET_TOKEN_SECONDS = 1800`, was 24h until
68
+ 2026-10-13). `updatePassword()` returns `false` when the signature is bad **or** `exp` has passed.
69
+ The token is **still reusable inside the 30 minutes** — single-use would need a new `Users` column.
70
+ - Applies to every client and every app that uses this flow (toga2-supply, toga25-supply, Talos).
71
+
56
72
  ### Tenant routing from the email domain
57
73
 
58
74
  `V2.php:611-621` derives `clientId` from the address's domain through `Core.ClientEmailDomains` whenever the request host does not identify the client. An editable email is therefore also a **tenant-routing control**, not just a credential hint. The related cross-client login branch (a home user authenticating against another tenant) is documented in [/auth/login resolves the client from the email domain](../../api2/features/login-cross-client-user-resolution.md).
@@ -63,9 +79,24 @@ So whoever controls an address that appears on a `Users` row can request, receiv
63
79
  - **⚠ Do not assume the database enforces one email per user.** The 2.0 blank-client template declares `UNIQUE KEY email (email)`, but **live tenants have diverged** — both Compass tenants have no unique key and no index on `email` (prod-verified 2026-09-04). Any code that writes an email must check for a cross-row collision itself. Evidence and the full index list: [PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
64
80
  - **⚠ Treat an email write as a security change, not a data change.** Minimum rails for any UI or job that sets `Users.email`: confirm the value twice, validate the domain against `Core.ClientEmailDomains`, refuse when another row in the tenant already holds the address, and write an audit row **before** the update. That is the shape the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) shipped.
65
81
  - **An email is also a worker matching key.** worker2's `ensureCrossRegionUsersActive()` matches Compass users on **email**, so changing an address can silently remove a user from a hardcoded protected set — a second reason email edits need an explicit block list.
82
+ - **⚠ `_Http::validateJsonWebToken()` checks the signature only, never `exp`.** Any code that trusts
83
+ a JWT must check `exp` itself. Before 2026-10-13, `updatePassword()` did not, so a reset link
84
+ never expired.
85
+ - **Follow-up owed:** the "Forgot Password" `EmailTemplates` text still says "Link Valid for 1 Hour";
86
+ it should say 30 minutes (per client DB).
87
+ - **⚠ Known security debt (accepted, not fixed):** `sendForgotPasswordEmail` BCCs
88
+ `devteam@togatech.com` with the **live** reset link, and because `updatePassword` is GET-only the
89
+ new password travels in the query string and lands in `Logs_<Client>.Api`.
90
+ - **Testing tip:** in non-prod, mail is redirected to the dev team, so pull the reset link from the
91
+ `Logs_<Client>.Email` row body instead of an inbox (see [Email Send Pipeline](./email-send-pipeline.md)).
66
92
  - **Never paste real addresses into tickets or the KB.** Reference the mechanism and the column, not the person.
67
93
 
94
+ ## Change history
95
+ - 2026-10-14 — Reset token now 30 min and `updatePassword()` rejects expired tokens; documented the two scripted APIs, Public ACL, `Core.Domains` link host, and the BCC / query-string exposure (rgirish)
96
+ - 2026-09-16 — Created: email as login identity and tenant-routing key (ajean)
97
+
68
98
  ## Related
99
+ - [toga25-supply Forgot / Create Password pages](../../toga25-supply/features/forgot-password-and-create-password.md)
69
100
 
70
101
  - [/auth/login resolves the client from the email domain](../../api2/features/login-cross-client-user-resolution.md)
71
102
  - [Record change audit log](./record-change-audit-log.md)
@@ -9,6 +9,7 @@
9
9
  | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | Config that varies by client (or client × role) — field overrides, filter buttons, group-by, column pickers, layout toggles — resolved through `src/fieldsConfig |
10
10
  | [Column State (URL-driven show/hide, order & pinning)](features/column-visibility.md) | URL-driven "Columns" button + modal (`cols` param) for show/hide + order + pinning of table columns, client-configurable then Surface-driven; open for column-vi |
11
11
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | Force-signs-out every user when a deploy bumps a Core parameter, so session + caches reset to the new build; open when debugging surprise logouts or `useDeploym |
12
+ | [Forgot password & Create password pages (toga25-supply)](features/forgot-password-and-create-password.md) | The toga25-supply forgot-password and create-password pages: routes, page states, router-state notices, and the two backend scripted APIs they call. |
12
13
  | [Loading & save-error states (table skeletons, shimmers, Refresh busy pill, save-error toaster)](features/loading-and-error-states.md) | Owning doc for how supply shows loading (table skeleton, sub-section shimmer, Refresh busy pill, sales-order modal skeletons) and failed saves (error toaster, n |
13
14
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page is meta-driven end to end — page meta + table meta + table data, each keyed by slug via toga-blox hooks; pagination-vs-infinite is decided solely by meta |
14
15
  | [Persisted React Query cache (localStorage `supply-chain-query-cache`)](features/persisted-query-cache.md) | `localStorage["supply-chain-query-cache"]` is the whole React Query cache dehydrated to disk (survives reloads AND sessions); the owning doc for staleTime rules |
@@ -0,0 +1,83 @@
1
+ ---
2
+ title: Forgot password & Create password pages (toga25-supply)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-14
10
+ owners: [rgirish]
11
+ files:
12
+ - toga25-supply/src/routes.tsx
13
+ - toga25-supply/src/pages/Login/LoginPage.tsx
14
+ - toga25-supply/src/pages/Login/view/AuthNotice.tsx
15
+ - toga25-supply/src/pages/ForgotPassword/ForgotPasswordPage.tsx
16
+ - toga25-supply/src/pages/ForgotPassword/viewModel/useForgotPasswordViewModel.ts
17
+ - toga25-supply/src/pages/CreatePassword/CreatePasswordPage.tsx
18
+ - toga25-supply/src/pages/CreatePassword/viewModel/useCreatePasswordViewModel.ts
19
+ - toga25-supply/src/pages/CreatePassword/helpers/passwordRules.ts
20
+ related:
21
+ - ./sso-redirect-and-session-gating.md
22
+ - ../workflows/porting-a-page-from-toga2-supply.md
23
+ - ../../_underscore/features/user-email-as-identity-and-password-reset.md
24
+ - ../../_underscore/features/email-send-pipeline.md
25
+ ---
26
+
27
+ The toga25-supply forgot-password and create-password pages: routes, page states, router-state notices, and the two backend scripted APIs they call.
28
+
29
+ ## Summary
30
+
31
+ toga25-supply has its own **Forgot password** and **Create password** pages (2026-10). The flow is a
32
+ copy of toga2-supply's, redesigned to a provided design (light mode). The backend is **unchanged** —
33
+ the same two `_underscore` scripted APIs every app uses.
34
+
35
+ ## How it works
36
+
37
+ ### Routes
38
+
39
+ `/forgot-password` and `/create-password` are **public** routes in `src/routes.tsx`, next to `/login`.
40
+
41
+ ### Login page
42
+
43
+ - Centered **"Forgot password?"** link.
44
+ - Wrong credentials → red `AuthNotice` with a reset link. Shown **only** when
45
+ `useLoginPageViewModel` throws `"Invalid Credentials"` (not for other errors).
46
+ - Router state `passwordUpdated` → green "Password updated." notice.
47
+ - `AuthNotice.tsx` is the shared notice component for all three pages.
48
+
49
+ ### Forgot password page
50
+
51
+ 1. User enters the email → calls `GET /users/sendForgotPasswordEmail?userEmail=`.
52
+ 2. Switches to a **"Check your email"** view with **Change** (edit the address), **Back to Sign In**,
53
+ and a **30-second resend countdown**.
54
+ 3. Router state `linkExpired` → amber "link expired" notice.
55
+
56
+ The API always returns `true` (it does not reveal whether the address exists).
57
+
58
+ ### Create password page
59
+
60
+ 1. Reads `token` from the URL **once**, then strips it from the URL.
61
+ 2. Missing token, or the API returns `false` → navigate to `/forgot-password` with `linkExpired`.
62
+ 3. Calls `GET /users/updatePassword?password=&token=`.
63
+ 4. Success → navigate to `/` with `passwordUpdated`.
64
+ 5. Rules (in `helpers/passwordRules.ts`, with `passwordRules.test.ts`): 12+ characters, an uppercase
65
+ letter, a number, a special character.
66
+
67
+ Backend details (token lifetime 30 min, Public ACL, `Core.Domains` link host) live in
68
+ [Users.email & password reset](../../_underscore/features/user-email-as-identity-and-password-reset.md).
69
+
70
+ ## Gotchas
71
+
72
+ - **Send the password with axios `params`, never by building the URL.** `updatePassword` is a
73
+ GET-only scripted API, so the password goes in the query string; `params` encodes `&`, `#`, `+`.
74
+ A hand-built URL breaks on those characters.
75
+ - **Use `font-medium`, not `font-semibold`.** The app font loads only weights 300/400/500/700, so
76
+ Tailwind `font-semibold` (600) falls back to the heavy 700.
77
+ - **The reset link host is the request Origin.** A new app host must be a `Core.Domains` row for the
78
+ client, or the emailed link points to the wrong place / the call fails.
79
+ - **Testing on non-prod:** mail is redirected to the dev team and client-sandbox does not send at
80
+ all — read the token from the `Logs_<Client>.Email` row body.
81
+
82
+ ## Change history
83
+ - 2026-10-14 — Created: forgot-password + create-password pages ported from toga2-supply and redesigned (rgirish)
@@ -31,7 +31,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
31
31
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
32
32
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
33
33
  - **toga2-commerce** (TOGa Commerce) — 30 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
34
- - **toga25-supply** (TOGa 2.5 Supply) — 23 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga25-supply** (TOGa 2.5 Supply) — 24 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga25-iq** (TOGa IQ) — 1 doc(s) → [2.0/apps/toga25-iq/INDEX.md](2.0/apps/toga25-iq/INDEX.md)
36
36
  - **toga25-desk** (TOGa Desk 2.5) — 1 doc(s) → [2.0/apps/toga25-desk/INDEX.md](2.0/apps/toga25-desk/INDEX.md)
37
37
  - **toga25-blox** (TOGa Blox 2.5) — 3 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
@@ -18,7 +18,7 @@ project: Worker
18
18
  client: elite
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-24
21
+ updated: 2026-10-14
22
22
  owners: [snaredla, apeterson, tcox, bala, rgirish, jcardinal]
23
23
  files:
24
24
  - worker2/Worker/Elite.php
@@ -85,9 +85,10 @@ method still carries GroWrk-specific rules. TO↔PO links: [TO↔PO link reconci
85
85
  so it is **prod-only drift owed a `dbchanges2` catch-up**. Full write-up in
86
86
  [netsuite-togasupply-sync](features/netsuite-togasupply-sync.md).
87
87
 
88
- **Elite order status is its own six-status ladder.** `_Model_Elite_SalesOrder::_status` emits
89
- pendingFulfillment / partiallyFulfilled / fulfilled / shipped / delivered / canceled. **Elite has no
90
- approval model**, so the Compass status logic does not transfer - see
88
+ **Elite order status is its own ladder.** `_Model_Elite_SalesOrder::_status` emits
89
+ pendingFulfillment / picked / packed / partiallyFulfilled / shipped / fulfilled / delivered / canceled
90
+ (picked/packed added 2026-10-08 from NetSuite IF stages; billing statuses are deliberately not shown).
91
+ **Elite has no approval model**, so the Compass status logic does not transfer - see
91
92
  [IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
92
93
 
93
94
  ## Supply Inventory grouping — "Units by Items" is two levels, not three (2026-09-21)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1016",
3
+ "version": "1.0.1017",
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",