@cat-factory/app 0.35.0 → 0.37.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.
Files changed (87) hide show
  1. package/app/components/auth/UserMenu.vue +11 -1
  2. package/app/components/brainstorm/BrainstormWindow.vue +2 -1
  3. package/app/components/clarity/ClarityReviewWindow.vue +2 -1
  4. package/app/components/gates/GateResultView.vue +107 -12
  5. package/app/components/layout/IntegrationBackTitle.vue +12 -7
  6. package/app/components/layout/IntegrationsHub.vue +191 -43
  7. package/app/components/layout/NotificationsInbox.vue +16 -0
  8. package/app/components/layout/PersonalSetupModal.vue +141 -0
  9. package/app/components/pipeline/PipelineBuilder.vue +1 -1
  10. package/app/components/providers/VendorCredentialsModal.vue +7 -2
  11. package/app/components/slack/SlackPanel.vue +1 -0
  12. package/app/composables/api/accounts.ts +36 -51
  13. package/app/composables/api/auth.ts +20 -19
  14. package/app/composables/api/board.ts +60 -40
  15. package/app/composables/api/bootstrap.ts +25 -22
  16. package/app/composables/api/client.ts +102 -0
  17. package/app/composables/api/context.ts +25 -6
  18. package/app/composables/api/documents.ts +36 -34
  19. package/app/composables/api/execution.ts +65 -48
  20. package/app/composables/api/followUps.ts +26 -26
  21. package/app/composables/api/fragments.ts +47 -34
  22. package/app/composables/api/github.ts +65 -45
  23. package/app/composables/api/humanReview.ts +19 -0
  24. package/app/composables/api/humanTest.ts +15 -11
  25. package/app/composables/api/kaizen.ts +8 -6
  26. package/app/composables/api/localSettings.ts +5 -4
  27. package/app/composables/api/models.ts +58 -51
  28. package/app/composables/api/notifications.ts +13 -7
  29. package/app/composables/api/presets.ts +34 -28
  30. package/app/composables/api/providerConnections.ts +68 -26
  31. package/app/composables/api/recurring.ts +40 -30
  32. package/app/composables/api/releaseHealth.ts +28 -26
  33. package/app/composables/api/reviews.ts +136 -114
  34. package/app/composables/api/sandbox.ts +52 -34
  35. package/app/composables/api/slack.ts +22 -25
  36. package/app/composables/api/spec.ts +3 -3
  37. package/app/composables/api/tasks.ts +42 -41
  38. package/app/composables/api/userSecrets.ts +12 -17
  39. package/app/composables/api/workspaces.ts +21 -15
  40. package/app/composables/useApi.ts +11 -1
  41. package/app/composables/useIntegrationBack.ts +9 -3
  42. package/app/pages/index.vue +2 -0
  43. package/app/stores/auth.ts +2 -1
  44. package/app/stores/board.ts +2 -1
  45. package/app/stores/brainstorm.ts +2 -2
  46. package/app/stores/clarity.ts +6 -2
  47. package/app/stores/execution.ts +3 -2
  48. package/app/stores/github.ts +1 -2
  49. package/app/stores/humanReview.ts +41 -0
  50. package/app/stores/mergePresets.ts +2 -6
  51. package/app/stores/pipelines.ts +1 -1
  52. package/app/stores/recurringPipelines.ts +2 -7
  53. package/app/stores/sandbox.ts +1 -2
  54. package/app/stores/ui.ts +62 -19
  55. package/app/types/accountSettings.ts +11 -36
  56. package/app/types/accounts.ts +16 -71
  57. package/app/types/bootstrap.ts +13 -75
  58. package/app/types/brainstorm.ts +13 -38
  59. package/app/types/clarity.ts +12 -43
  60. package/app/types/consensus.ts +16 -89
  61. package/app/types/documents.ts +19 -94
  62. package/app/types/domain.ts +54 -582
  63. package/app/types/execution.ts +48 -499
  64. package/app/types/fragments.ts +15 -83
  65. package/app/types/github.ts +25 -161
  66. package/app/types/incidentEnrichment.ts +10 -25
  67. package/app/types/localModels.ts +11 -61
  68. package/app/types/localSettings.ts +9 -26
  69. package/app/types/merge.ts +10 -68
  70. package/app/types/model-presets.ts +7 -28
  71. package/app/types/models.ts +16 -164
  72. package/app/types/notifications.ts +18 -76
  73. package/app/types/openrouter.ts +8 -34
  74. package/app/types/providerConnections.ts +21 -41
  75. package/app/types/provisioningLogs.ts +9 -29
  76. package/app/types/recurring.ts +10 -63
  77. package/app/types/releaseHealth.ts +15 -39
  78. package/app/types/requirements.ts +14 -84
  79. package/app/types/sandbox.ts +45 -161
  80. package/app/types/services.ts +3 -22
  81. package/app/types/slack.ts +10 -47
  82. package/app/types/spec.ts +15 -68
  83. package/app/types/tasks.ts +15 -111
  84. package/app/types/tracker.ts +9 -24
  85. package/app/types/userSecrets.ts +12 -47
  86. package/app/utils/catalog.ts +12 -0
  87. package/package.json +9 -2
@@ -1,98 +1,115 @@
1
+ import {
2
+ commentGitHubIssueContract,
3
+ commitGitHubFilesContract,
4
+ connectGitHubContract,
5
+ createGitHubBranchContract,
6
+ createGitHubRepoContract,
7
+ disconnectGitHubContract,
8
+ getGitHubConnectionContract,
9
+ getGitHubInstallUrlContract,
10
+ listGitHubAvailableReposContract,
11
+ listGitHubBranchesContract,
12
+ listGitHubInstallationsContract,
13
+ listGitHubIssuesContract,
14
+ listGitHubPullsContract,
15
+ listGitHubReposContract,
16
+ listGitHubRepoTreeContract,
17
+ mergeGitHubPullRequestContract,
18
+ openGitHubPullRequestContract,
19
+ resyncGitHubContract,
20
+ setGitHubLinkedReposContract,
21
+ } from '@cat-factory/contracts'
1
22
  import type {
2
23
  CommitFilesInput,
3
24
  CreateBranchInput,
4
- CreatedRepo,
5
- CreateRepoRequest,
6
- GitHubAvailableRepo,
7
- GitHubBranch,
8
- GitHubConnection,
9
- GitHubInstallationOption,
10
- GitHubIssue,
11
- GitHubPullRequest,
12
- GitHubRepo,
13
25
  MergePullRequestInput,
14
26
  OpenPullRequestInput,
15
- RepoTreeEntry,
16
27
  ResyncRequest,
17
28
  } from '~/types/domain'
29
+ import type { SendParams } from './client'
18
30
  import type { ApiContext } from './context'
19
31
 
32
+ // The create-repo body is typed from the contract's INPUT shape so the
33
+ // valibot-defaulted `private`/`description` stay optional for callers (the exported
34
+ // `CreateRepoRequest` is the post-default OUTPUT shape).
35
+ type CreateRepoBody = NonNullable<SendParams<typeof createGitHubRepoContract>['body']>
36
+
20
37
  /**
21
38
  * GitHub integration: connection management, the D1-served projection reads
22
39
  * (fast, rate-limit-free) and the repo writes (branches/commits/PRs/merges).
23
40
  */
24
- export function githubApi({ http, ws }: ApiContext) {
41
+ export function githubApi({ send, ws }: ApiContext) {
25
42
  return {
26
43
  // ---- github integration ----------------------------------------------
27
44
  // Connection management, projection reads (served from D1 — fast and
28
45
  // rate-limit-free) and repo writes. A 503 from `getGitHubConnection` means
29
46
  // the integration is off (the store hides its UI on any error there).
30
47
  getGitHubInstallUrl: (workspaceId: string) =>
31
- http<{ url: string }>(`${ws(workspaceId)}/github/install-url`),
48
+ send(getGitHubInstallUrlContract, { pathPrefix: ws(workspaceId) }),
32
49
 
33
50
  getGitHubConnection: (workspaceId: string) =>
34
- http<{ connection: GitHubConnection | null }>(`${ws(workspaceId)}/github/connection`),
51
+ send(getGitHubConnectionContract, { pathPrefix: ws(workspaceId) }),
35
52
 
36
53
  listGitHubInstallations: (workspaceId: string) =>
37
- http<{ installations: GitHubInstallationOption[] }>(
38
- `${ws(workspaceId)}/github/installations`,
39
- ),
54
+ send(listGitHubInstallationsContract, { pathPrefix: ws(workspaceId) }),
40
55
 
41
56
  connectGitHub: (workspaceId: string, installationId: number) =>
42
- http<GitHubConnection>(`${ws(workspaceId)}/github/connect`, {
43
- method: 'POST',
44
- body: { installationId },
45
- }),
57
+ send(connectGitHubContract, { pathPrefix: ws(workspaceId), body: { installationId } }),
46
58
 
47
59
  disconnectGitHub: (workspaceId: string) =>
48
- http(`${ws(workspaceId)}/github/connection`, { method: 'DELETE' }),
60
+ send(disconnectGitHubContract, { pathPrefix: ws(workspaceId) }),
49
61
 
50
62
  resyncGitHub: (workspaceId: string, body: ResyncRequest = {}) =>
51
- http<{ status: string }>(`${ws(workspaceId)}/github/resync`, { method: 'POST', body }),
63
+ send(resyncGitHubContract, { pathPrefix: ws(workspaceId), body }),
52
64
 
53
- listGitHubRepos: (workspaceId: string) => http<GitHubRepo[]>(`${ws(workspaceId)}/github/repos`),
65
+ listGitHubRepos: (workspaceId: string) =>
66
+ send(listGitHubReposContract, { pathPrefix: ws(workspaceId) }),
54
67
 
55
68
  // Programmatic repo creation (privileged App tier). Only called when the
56
69
  // connection reports `canCreateRepos`; otherwise the UI opens GitHub directly.
57
- createGitHubRepo: (workspaceId: string, body: CreateRepoRequest) =>
58
- http<CreatedRepo>(`${ws(workspaceId)}/github/repos`, { method: 'POST', body }),
70
+ createGitHubRepo: (workspaceId: string, body: CreateRepoBody) =>
71
+ send(createGitHubRepoContract, { pathPrefix: ws(workspaceId), body }),
59
72
 
60
73
  // Repos the connected installation can access, annotated with whether this
61
74
  // workspace links each (drives the per-workspace repo picker).
62
75
  listGitHubAvailableRepos: (workspaceId: string) =>
63
- http<GitHubAvailableRepo[]>(`${ws(workspaceId)}/github/available-repos`),
76
+ send(listGitHubAvailableReposContract, { pathPrefix: ws(workspaceId) }),
64
77
 
65
78
  // Set the exact set of repos this workspace links.
66
79
  setGitHubLinkedRepos: (workspaceId: string, repoGithubIds: number[]) =>
67
- http<GitHubRepo[]>(`${ws(workspaceId)}/github/repos`, {
68
- method: 'PUT',
69
- body: { repoGithubIds },
70
- }),
80
+ send(setGitHubLinkedReposContract, { pathPrefix: ws(workspaceId), body: { repoGithubIds } }),
71
81
 
72
82
  // Browse one level of a (monorepo) repo's tree to pin a service to a subdirectory.
73
83
  listGitHubRepoTree: (workspaceId: string, repoGithubId: number, path = '') =>
74
- http<RepoTreeEntry[]>(`${ws(workspaceId)}/github/repos/${repoGithubId}/tree`, {
75
- query: { path },
84
+ send(listGitHubRepoTreeContract, {
85
+ pathPrefix: ws(workspaceId),
86
+ pathParams: { repoGithubId: String(repoGithubId) },
87
+ queryParams: { path },
76
88
  }),
77
89
 
78
90
  listGitHubBranches: (workspaceId: string, repoGithubId: number) =>
79
- http<GitHubBranch[]>(`${ws(workspaceId)}/github/repos/${repoGithubId}/branches`),
91
+ send(listGitHubBranchesContract, {
92
+ pathPrefix: ws(workspaceId),
93
+ pathParams: { repoGithubId: String(repoGithubId) },
94
+ }),
80
95
 
81
96
  listGitHubPullRequests: (workspaceId: string) =>
82
- http<GitHubPullRequest[]>(`${ws(workspaceId)}/github/pulls`),
97
+ send(listGitHubPullsContract, { pathPrefix: ws(workspaceId) }),
83
98
 
84
99
  listGitHubIssues: (workspaceId: string) =>
85
- http<GitHubIssue[]>(`${ws(workspaceId)}/github/issues`),
100
+ send(listGitHubIssuesContract, { pathPrefix: ws(workspaceId) }),
86
101
 
87
102
  createGitHubBranch: (workspaceId: string, repoGithubId: number, body: CreateBranchInput) =>
88
- http<GitHubBranch>(`${ws(workspaceId)}/github/repos/${repoGithubId}/branches`, {
89
- method: 'POST',
103
+ send(createGitHubBranchContract, {
104
+ pathPrefix: ws(workspaceId),
105
+ pathParams: { repoGithubId: String(repoGithubId) },
90
106
  body,
91
107
  }),
92
108
 
93
109
  commitGitHubFiles: (workspaceId: string, repoGithubId: number, body: CommitFilesInput) =>
94
- http<{ sha: string }>(`${ws(workspaceId)}/github/repos/${repoGithubId}/commits`, {
95
- method: 'POST',
110
+ send(commitGitHubFilesContract, {
111
+ pathPrefix: ws(workspaceId),
112
+ pathParams: { repoGithubId: String(repoGithubId) },
96
113
  body,
97
114
  }),
98
115
 
@@ -101,8 +118,9 @@ export function githubApi({ http, ws }: ApiContext) {
101
118
  repoGithubId: number,
102
119
  body: OpenPullRequestInput,
103
120
  ) =>
104
- http<GitHubPullRequest>(`${ws(workspaceId)}/github/repos/${repoGithubId}/pulls`, {
105
- method: 'POST',
121
+ send(openGitHubPullRequestContract, {
122
+ pathPrefix: ws(workspaceId),
123
+ pathParams: { repoGithubId: String(repoGithubId) },
106
124
  body,
107
125
  }),
108
126
 
@@ -112,8 +130,9 @@ export function githubApi({ http, ws }: ApiContext) {
112
130
  number: number,
113
131
  body: MergePullRequestInput = {},
114
132
  ) =>
115
- http(`${ws(workspaceId)}/github/repos/${repoGithubId}/pulls/${number}/merge`, {
116
- method: 'PUT',
133
+ send(mergeGitHubPullRequestContract, {
134
+ pathPrefix: ws(workspaceId),
135
+ pathParams: { repoGithubId: String(repoGithubId), number: String(number) },
117
136
  body,
118
137
  }),
119
138
 
@@ -123,8 +142,9 @@ export function githubApi({ http, ws }: ApiContext) {
123
142
  number: number,
124
143
  bodyText: string,
125
144
  ) =>
126
- http(`${ws(workspaceId)}/github/repos/${repoGithubId}/issues/${number}/comments`, {
127
- method: 'POST',
145
+ send(commentGitHubIssueContract, {
146
+ pathPrefix: ws(workspaceId),
147
+ pathParams: { repoGithubId: String(repoGithubId), number: String(number) },
128
148
  body: { body: bodyText },
129
149
  }),
130
150
  }
@@ -0,0 +1,19 @@
1
+ import { requestHumanReviewFixContract } from '@cat-factory/contracts'
2
+ import type { ApiContext } from './context'
3
+
4
+ /**
5
+ * The human-review gate's run-driving action. The gate self-drives off the PR's GitHub review
6
+ * state, but a human can request a freeform fix at any time — dispatched to the `fixer`
7
+ * immediately. Returns the updated execution instance (the gate state rides on its step + the
8
+ * execution stream).
9
+ */
10
+ export function humanReviewApi({ send, ws }: ApiContext) {
11
+ return {
12
+ requestHumanReviewFix: (workspaceId: string, blockId: string, instructions: string) =>
13
+ send(requestHumanReviewFixContract, {
14
+ pathPrefix: ws(workspaceId),
15
+ pathParams: { blockId },
16
+ body: { instructions },
17
+ }),
18
+ }
19
+ }
@@ -1,4 +1,10 @@
1
- import type { ExecutionInstance } from '~/types/domain'
1
+ import {
2
+ confirmHumanTestContract,
3
+ destroyHumanTestEnvContract,
4
+ pullMainHumanTestContract,
5
+ recreateHumanTestEnvContract,
6
+ requestHumanTestFixContract,
7
+ } from '@cat-factory/contracts'
2
8
  import type { ApiContext } from './context'
3
9
 
4
10
  /**
@@ -6,32 +12,30 @@ import type { ApiContext } from './context'
6
12
  * step and returns the updated execution instance (the gate state rides on its current step,
7
13
  * and also arrives live via the execution stream).
8
14
  */
9
- export function humanTestApi({ http, ws }: ApiContext) {
10
- const base = (workspaceId: string, blockId: string) =>
11
- `${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/human-test`
12
-
15
+ export function humanTestApi({ send, ws }: ApiContext) {
13
16
  return {
14
17
  // Confirm the change works: tear the env down and advance the pipeline.
15
18
  confirmHumanTest: (workspaceId: string, blockId: string) =>
16
- http<ExecutionInstance>(`${base(workspaceId, blockId)}/confirm`, { method: 'POST' }),
19
+ send(confirmHumanTestContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
17
20
 
18
21
  // Submit findings and request a fix (dispatches the Tester's fixer, then rebuilds the env).
19
22
  requestHumanTestFix: (workspaceId: string, blockId: string, findings: string) =>
20
- http<ExecutionInstance>(`${base(workspaceId, blockId)}/request-fix`, {
21
- method: 'POST',
23
+ send(requestHumanTestFixContract, {
24
+ pathPrefix: ws(workspaceId),
25
+ pathParams: { blockId },
22
26
  body: { findings },
23
27
  }),
24
28
 
25
29
  // Pull latest main into the PR branch + redeploy (conflict → conflict-resolver).
26
30
  pullMainHumanTest: (workspaceId: string, blockId: string) =>
27
- http<ExecutionInstance>(`${base(workspaceId, blockId)}/pull-main`, { method: 'POST' }),
31
+ send(pullMainHumanTestContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
28
32
 
29
33
  // Rebuild the ephemeral environment on demand.
30
34
  recreateHumanTestEnv: (workspaceId: string, blockId: string) =>
31
- http<ExecutionInstance>(`${base(workspaceId, blockId)}/recreate-env`, { method: 'POST' }),
35
+ send(recreateHumanTestEnvContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
32
36
 
33
37
  // Destroy the ephemeral environment on demand (the run stays parked).
34
38
  destroyHumanTestEnv: (workspaceId: string, blockId: string) =>
35
- http<ExecutionInstance>(`${base(workspaceId, blockId)}/destroy-env`, { method: 'POST' }),
39
+ send(destroyHumanTestEnvContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
36
40
  }
37
41
  }
@@ -1,16 +1,18 @@
1
- import type { KaizenGrading, KaizenOverview } from '~/types/domain'
1
+ import { getKaizenOverviewContract, getKaizenRunGradingsContract } from '@cat-factory/contracts'
2
2
  import type { ApiContext } from './context'
3
3
 
4
4
  /** Kaizen (post-run grading) read endpoints: the screen overview + a run's gradings. */
5
- export function kaizenApi({ http, ws }: ApiContext) {
5
+ export function kaizenApi({ send, ws }: ApiContext) {
6
6
  return {
7
7
  // The Kaizen screen: recent grading history + the verified-combo library.
8
- getKaizenOverview: (workspaceId: string) => http<KaizenOverview>(`${ws(workspaceId)}/kaizen`),
8
+ getKaizenOverview: (workspaceId: string) =>
9
+ send(getKaizenOverviewContract, { pathPrefix: ws(workspaceId) }),
9
10
 
10
11
  // The gradings recorded for one run (the run-window status surface).
11
12
  getKaizenForExecution: (workspaceId: string, executionId: string) =>
12
- http<{ gradings: KaizenGrading[] }>(
13
- `${ws(workspaceId)}/executions/${encodeURIComponent(executionId)}/kaizen`,
14
- ),
13
+ send(getKaizenRunGradingsContract, {
14
+ pathPrefix: ws(workspaceId),
15
+ pathParams: { executionId },
16
+ }),
15
17
  }
16
18
  }
@@ -1,4 +1,5 @@
1
- import type { LocalSettings, UpdateLocalSettingsInput } from '~/types/localSettings'
1
+ import { getLocalSettingsContract, updateLocalSettingsContract } from '@cat-factory/contracts'
2
+ import type { UpdateLocalSettingsInput } from '~/types/localSettings'
2
3
  import type { ApiContext } from './context'
3
4
 
4
5
  /**
@@ -7,11 +8,11 @@ import type { ApiContext } from './context'
7
8
  * Worker / stock Node facades (the store hides the panel then). No secrets, so the read view
8
9
  * is the plain config and the write replaces it wholesale.
9
10
  */
10
- export function localSettingsApi({ http }: ApiContext) {
11
+ export function localSettingsApi({ send }: ApiContext) {
11
12
  return {
12
- getLocalSettings: () => http<LocalSettings>('/local-settings'),
13
+ getLocalSettings: () => send(getLocalSettingsContract, {}),
13
14
 
14
15
  updateLocalSettings: (body: UpdateLocalSettingsInput) =>
15
- http<LocalSettings>('/local-settings', { method: 'PUT', body }),
16
+ send(updateLocalSettingsContract, { body }),
16
17
  }
17
18
  }
@@ -1,25 +1,42 @@
1
+ import {
2
+ addAccountApiKeyContract,
3
+ addUserApiKeyContract,
4
+ addVendorCredentialContract,
5
+ addWorkspaceApiKeyContract,
6
+ getOpenRouterCatalogContract,
7
+ getServiceFragmentDefaultsContract,
8
+ listAccountApiKeysContract,
9
+ listLocalModelEndpointsContract,
10
+ listModelsContract,
11
+ listPersonalSubscriptionsContract,
12
+ listUserApiKeysContract,
13
+ listVendorCredentialsContract,
14
+ listWorkspaceApiKeysContract,
15
+ listWorkspaceModelsContract,
16
+ refreshOpenRouterCatalogContract,
17
+ removeAccountApiKeyContract,
18
+ removeLocalModelEndpointContract,
19
+ removePersonalSubscriptionContract,
20
+ removeUserApiKeyContract,
21
+ removeVendorCredentialContract,
22
+ removeWorkspaceApiKeyContract,
23
+ setServiceFragmentDefaultsContract,
24
+ storePersonalSubscriptionContract,
25
+ testLocalModelEndpointContract,
26
+ upsertLocalModelEndpointContract,
27
+ upsertOpenRouterCatalogContract,
28
+ } from '@cat-factory/contracts'
1
29
  import type {
2
30
  AddApiKeyInput,
3
- ApiKey,
4
- ModelOption,
5
- PersonalSubscriptionStatus,
6
- ServiceFragmentDefaults,
7
31
  StorePersonalSubscriptionInput,
8
32
  SubscriptionVendor,
9
- VendorCredential,
10
33
  } from '~/types/domain'
11
34
  import type {
12
- LocalModelEndpoint,
13
- LocalModelEndpointTestResult,
14
35
  LocalRunner,
15
36
  TestLocalModelEndpointInput,
16
37
  UpsertLocalModelEndpointInput,
17
38
  } from '~/types/localModels'
18
- import type {
19
- OpenRouterCatalog,
20
- OpenRouterRefreshResult,
21
- UpsertOpenRouterCatalogInput,
22
- } from '~/types/openrouter'
39
+ import type { UpsertOpenRouterCatalogInput } from '~/types/openrouter'
23
40
  import type { ApiContext } from './context'
24
41
 
25
42
  /**
@@ -27,104 +44,94 @@ import type { ApiContext } from './context'
27
44
  * API keys, vendor subscription tokens, per-user personal subscriptions + local
28
45
  * runners) + the per-workspace routing/selection defaults.
29
46
  */
30
- export function modelsApi({ http, ws }: ApiContext) {
47
+ export function modelsApi({ send, ws }: ApiContext) {
31
48
  return {
32
49
  // ---- model picker catalog (effective per-deployment flavours) ---------
33
- getModels: () => http<ModelOption[]>('/models'),
50
+ getModels: () => send(listModelsContract, {}),
34
51
  // Per-workspace catalog: selectability reflects the workspace's (+ account's +
35
52
  // caller's) configured API keys and subscription tokens (`available` flag).
36
- getWorkspaceModels: (workspaceId: string) => http<ModelOption[]>(`${ws(workspaceId)}/models`),
53
+ getWorkspaceModels: (workspaceId: string) =>
54
+ send(listWorkspaceModelsContract, { pathParams: { workspaceId } }),
37
55
 
38
56
  // ---- direct-provider API keys (the DB-backed pool) --------------------
39
57
  // Onboarded via UI, stored encrypted, pooled + rotated. Scoped to a workspace,
40
58
  // its owning account, or the signed-in user. Keys are write-only (never returned).
41
59
  listWorkspaceApiKeys: (workspaceId: string) =>
42
- http<{ keys: ApiKey[] }>(`${ws(workspaceId)}/api-keys`),
60
+ send(listWorkspaceApiKeysContract, { pathPrefix: ws(workspaceId) }),
43
61
  addWorkspaceApiKey: (workspaceId: string, body: AddApiKeyInput) =>
44
- http<ApiKey>(`${ws(workspaceId)}/api-keys`, { method: 'POST', body }),
62
+ send(addWorkspaceApiKeyContract, { pathPrefix: ws(workspaceId), body }),
45
63
  removeWorkspaceApiKey: (workspaceId: string, id: string) =>
46
- http(`${ws(workspaceId)}/api-keys/${encodeURIComponent(id)}`, { method: 'DELETE' }),
47
- listMyApiKeys: () => http<{ keys: ApiKey[] }>('/me/api-keys'),
48
- addMyApiKey: (body: AddApiKeyInput) => http<ApiKey>('/me/api-keys', { method: 'POST', body }),
49
- removeMyApiKey: (id: string) =>
50
- http(`/me/api-keys/${encodeURIComponent(id)}`, { method: 'DELETE' }),
64
+ send(removeWorkspaceApiKeyContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
65
+ listMyApiKeys: () => send(listUserApiKeysContract, {}),
66
+ addMyApiKey: (body: AddApiKeyInput) => send(addUserApiKeyContract, { body }),
67
+ removeMyApiKey: (id: string) => send(removeUserApiKeyContract, { pathParams: { id } }),
51
68
  // Account-scoped keys (shared by every workspace in the account); admin-only.
52
69
  listAccountApiKeys: (accountId: string) =>
53
- http<{ keys: ApiKey[] }>(`/accounts/${encodeURIComponent(accountId)}/api-keys`),
70
+ send(listAccountApiKeysContract, { pathParams: { accountId } }),
54
71
  addAccountApiKey: (accountId: string, body: AddApiKeyInput) =>
55
- http<ApiKey>(`/accounts/${encodeURIComponent(accountId)}/api-keys`, { method: 'POST', body }),
72
+ send(addAccountApiKeyContract, { pathParams: { accountId }, body }),
56
73
  removeAccountApiKey: (accountId: string, id: string) =>
57
- http(`/accounts/${encodeURIComponent(accountId)}/api-keys/${encodeURIComponent(id)}`, {
58
- method: 'DELETE',
59
- }),
74
+ send(removeAccountApiKeyContract, { pathParams: { accountId, id } }),
60
75
 
61
76
  // ---- LLM vendor subscription credentials (the token pool) -------------
62
77
  listVendorCredentials: (workspaceId: string) =>
63
- http<{ credentials: VendorCredential[] }>(`${ws(workspaceId)}/vendor-credentials`),
78
+ send(listVendorCredentialsContract, { pathPrefix: ws(workspaceId) }),
64
79
  addVendorCredential: (
65
80
  workspaceId: string,
66
81
  body: { vendor: SubscriptionVendor; label: string; token: string },
67
- ) => http<VendorCredential>(`${ws(workspaceId)}/vendor-credentials`, { method: 'POST', body }),
82
+ ) => send(addVendorCredentialContract, { pathPrefix: ws(workspaceId), body }),
68
83
  removeVendorCredential: (workspaceId: string, id: string) =>
69
- http(`${ws(workspaceId)}/vendor-credentials/${encodeURIComponent(id)}`, { method: 'DELETE' }),
84
+ send(removeVendorCredentialContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
70
85
 
71
86
  // ---- personal (individual-usage) subscriptions (per-user, e.g. Claude) ----
72
87
  // Stored per signed-in user, double-encrypted under their personal password.
73
88
  // Metadata only is returned (never the token). User-scoped (no workspace).
74
- listPersonalSubscriptions: () =>
75
- http<{ subscriptions: PersonalSubscriptionStatus[] }>('/personal-subscriptions'),
89
+ listPersonalSubscriptions: () => send(listPersonalSubscriptionsContract, {}),
76
90
 
77
91
  storePersonalSubscription: (body: StorePersonalSubscriptionInput) =>
78
- http<PersonalSubscriptionStatus>('/personal-subscriptions', { method: 'POST', body }),
92
+ send(storePersonalSubscriptionContract, { body }),
79
93
 
80
94
  removePersonalSubscription: (vendor: SubscriptionVendor) =>
81
- http(`/personal-subscriptions/${encodeURIComponent(vendor)}`, { method: 'DELETE' }),
95
+ send(removePersonalSubscriptionContract, { pathParams: { vendor } }),
82
96
 
83
97
  // ---- local model runners (per-user, e.g. Ollama / LM Studio) ----------
84
98
  // A developer's own-machine LLM endpoints, stored per signed-in user (the API
85
99
  // key is write-only, never returned). User-scoped (no workspace). The enabled
86
100
  // models then surface automatically in the per-workspace `/models` catalog.
87
- listLocalModelEndpoints: () =>
88
- http<{ endpoints: LocalModelEndpoint[] }>('/local-model-endpoints'),
101
+ listLocalModelEndpoints: () => send(listLocalModelEndpointsContract, {}),
89
102
 
90
103
  upsertLocalModelEndpoint: (provider: LocalRunner, body: UpsertLocalModelEndpointInput) =>
91
- http<LocalModelEndpoint>(`/local-model-endpoints/${encodeURIComponent(provider)}`, {
92
- method: 'PUT',
93
- body,
94
- }),
104
+ send(upsertLocalModelEndpointContract, { pathParams: { provider }, body }),
95
105
 
96
106
  deleteLocalModelEndpoint: (provider: LocalRunner) =>
97
- http(`/local-model-endpoints/${encodeURIComponent(provider)}`, { method: 'DELETE' }),
107
+ send(removeLocalModelEndpointContract, { pathParams: { provider } }),
98
108
 
99
109
  // Probe a runner endpoint for reachability + the models it currently serves
100
110
  // (no persistence — drives the "Test connection" model multi-select).
101
111
  testLocalModelEndpoint: (body: TestLocalModelEndpointInput) =>
102
- http<LocalModelEndpointTestResult>('/local-model-endpoints/test', {
103
- method: 'POST',
104
- body,
105
- }),
112
+ send(testLocalModelEndpointContract, { body }),
106
113
 
107
114
  // ---- OpenRouter dynamic catalog (per-workspace gateway models) --------
108
115
  // Browse OpenRouter's live catalog (`refresh`, leasing the workspace's pooled
109
116
  // OpenRouter key server-side) and enable a subset; enabled models then surface
110
117
  // in the per-workspace `/models` catalog with their context + price.
111
118
  getOpenRouterCatalog: (workspaceId: string) =>
112
- http<OpenRouterCatalog>(`${ws(workspaceId)}/openrouter/catalog`),
119
+ send(getOpenRouterCatalogContract, { pathParams: { workspaceId } }),
113
120
 
114
121
  setOpenRouterCatalog: (workspaceId: string, body: UpsertOpenRouterCatalogInput) =>
115
- http<OpenRouterCatalog>(`${ws(workspaceId)}/openrouter/catalog`, { method: 'PUT', body }),
122
+ send(upsertOpenRouterCatalogContract, { pathParams: { workspaceId }, body }),
116
123
 
117
124
  refreshOpenRouterCatalog: (workspaceId: string) =>
118
- http<OpenRouterRefreshResult>(`${ws(workspaceId)}/openrouter/refresh`, { method: 'POST' }),
125
+ send(refreshOpenRouterCatalogContract, { pathParams: { workspaceId } }),
119
126
 
120
127
  // The workspace's default service-fragment selection (the fragment ids new
121
128
  // services inherit). `setServiceFragmentDefaults` replaces the whole list.
122
129
  getServiceFragmentDefaults: (workspaceId: string) =>
123
- http<ServiceFragmentDefaults>(`${ws(workspaceId)}/service-fragment-defaults`),
130
+ send(getServiceFragmentDefaultsContract, { pathPrefix: ws(workspaceId) }),
124
131
 
125
132
  setServiceFragmentDefaults: (workspaceId: string, fragmentIds: string[]) =>
126
- http<ServiceFragmentDefaults>(`${ws(workspaceId)}/service-fragment-defaults`, {
127
- method: 'PUT',
133
+ send(setServiceFragmentDefaultsContract, {
134
+ pathPrefix: ws(workspaceId),
128
135
  body: { fragmentIds },
129
136
  }),
130
137
  }
@@ -1,23 +1,29 @@
1
- import type { Notification } from '~/types/notifications'
1
+ import {
2
+ actNotificationContract,
3
+ dismissNotificationContract,
4
+ listNotificationsContract,
5
+ } from '@cat-factory/contracts'
2
6
  import type { ApiContext } from './context'
3
7
 
4
8
  /** The human-actionable notification inbox (act / dismiss). */
5
- export function notificationsApi({ http, ws }: ApiContext) {
9
+ export function notificationsApi({ send, ws }: ApiContext) {
6
10
  return {
7
11
  // ---- notifications (human-actionable board items) ---------------------
8
12
  listNotifications: (workspaceId: string) =>
9
- http<Notification[]>(`${ws(workspaceId)}/notifications`),
13
+ send(listNotificationsContract, { pathPrefix: ws(workspaceId) }),
10
14
 
11
15
  // Act on a notification (merge the PR / confirm / retry), then resolve it.
12
16
  actNotification: (workspaceId: string, id: string) =>
13
- http<Notification>(`${ws(workspaceId)}/notifications/${encodeURIComponent(id)}/act`, {
14
- method: 'POST',
17
+ send(actNotificationContract, {
18
+ pathPrefix: ws(workspaceId),
19
+ pathParams: { notificationId: id },
15
20
  }),
16
21
 
17
22
  // Dismiss a notification without acting.
18
23
  dismissNotification: (workspaceId: string, id: string) =>
19
- http<Notification>(`${ws(workspaceId)}/notifications/${encodeURIComponent(id)}/dismiss`, {
20
- method: 'POST',
24
+ send(dismissNotificationContract, {
25
+ pathPrefix: ws(workspaceId),
26
+ pathParams: { notificationId: id },
21
27
  }),
22
28
  }
23
29
  }
@@ -1,52 +1,58 @@
1
- import type {
2
- CreateMergePresetInput,
3
- MergeThresholdPreset,
4
- UpdateMergePresetInput,
5
- } from '~/types/merge'
6
- import type {
7
- CreateModelPresetInput,
8
- ModelPreset,
9
- UpdateModelPresetInput,
10
- } from '~/types/model-presets'
1
+ import {
2
+ createMergePresetContract,
3
+ createModelPresetContract,
4
+ deleteMergePresetContract,
5
+ deleteModelPresetContract,
6
+ listMergePresetsContract,
7
+ listModelPresetsContract,
8
+ updateMergePresetContract,
9
+ updateModelPresetContract,
10
+ } from '@cat-factory/contracts'
11
+ import type { UpdateMergePresetInput } from '~/types/merge'
12
+ import type { CreateModelPresetInput, UpdateModelPresetInput } from '~/types/model-presets'
13
+ import type { SendParams } from './client'
11
14
  import type { ApiContext } from './context'
12
15
 
16
+ // The merge-preset create body is typed from the contract's INPUT shape so the
17
+ // valibot-defaulted fields (release/grace windows, isDefault) stay optional for callers
18
+ // (the exported `CreateMergePresetInput` is the post-default OUTPUT shape).
19
+ type CreateMergePresetBody = NonNullable<SendParams<typeof createMergePresetContract>['body']>
20
+
13
21
  /** The per-workspace preset libraries: merge-threshold policy + model->agent mapping. */
14
- export function presetsApi({ http, ws }: ApiContext) {
22
+ export function presetsApi({ send, ws }: ApiContext) {
15
23
  return {
16
24
  // ---- merge threshold presets (per-task auto-merge policy library) -----
17
25
  listMergePresets: (workspaceId: string) =>
18
- http<MergeThresholdPreset[]>(`${ws(workspaceId)}/merge-presets`),
26
+ send(listMergePresetsContract, { pathPrefix: ws(workspaceId) }),
19
27
 
20
- createMergePreset: (workspaceId: string, body: CreateMergePresetInput) =>
21
- http<MergeThresholdPreset>(`${ws(workspaceId)}/merge-presets`, { method: 'POST', body }),
28
+ createMergePreset: (workspaceId: string, body: CreateMergePresetBody) =>
29
+ send(createMergePresetContract, { pathPrefix: ws(workspaceId), body }),
22
30
 
23
31
  updateMergePreset: (workspaceId: string, presetId: string, body: UpdateMergePresetInput) =>
24
- http<MergeThresholdPreset>(
25
- `${ws(workspaceId)}/merge-presets/${encodeURIComponent(presetId)}`,
26
- { method: 'PATCH', body },
27
- ),
32
+ send(updateMergePresetContract, {
33
+ pathPrefix: ws(workspaceId),
34
+ pathParams: { presetId },
35
+ body,
36
+ }),
28
37
 
29
38
  deleteMergePreset: (workspaceId: string, presetId: string) =>
30
- http(`${ws(workspaceId)}/merge-presets/${encodeURIComponent(presetId)}`, {
31
- method: 'DELETE',
32
- }),
39
+ send(deleteMergePresetContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
33
40
 
34
41
  // ---- model presets (per-task model->agent mapping library) ------------
35
42
  listModelPresets: (workspaceId: string) =>
36
- http<ModelPreset[]>(`${ws(workspaceId)}/model-presets`),
43
+ send(listModelPresetsContract, { pathPrefix: ws(workspaceId) }),
37
44
 
38
45
  createModelPreset: (workspaceId: string, body: CreateModelPresetInput) =>
39
- http<ModelPreset>(`${ws(workspaceId)}/model-presets`, { method: 'POST', body }),
46
+ send(createModelPresetContract, { pathPrefix: ws(workspaceId), body }),
40
47
 
41
48
  updateModelPreset: (workspaceId: string, presetId: string, body: UpdateModelPresetInput) =>
42
- http<ModelPreset>(`${ws(workspaceId)}/model-presets/${encodeURIComponent(presetId)}`, {
43
- method: 'PATCH',
49
+ send(updateModelPresetContract, {
50
+ pathPrefix: ws(workspaceId),
51
+ pathParams: { presetId },
44
52
  body,
45
53
  }),
46
54
 
47
55
  deleteModelPreset: (workspaceId: string, presetId: string) =>
48
- http(`${ws(workspaceId)}/model-presets/${encodeURIComponent(presetId)}`, {
49
- method: 'DELETE',
50
- }),
56
+ send(deleteModelPresetContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
51
57
  }
52
58
  }