@cortexkit/opencode-openai-auth 0.11.0 → 1.0.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 (204) hide show
  1. package/README.md +15 -26
  2. package/dist/index-766sfrh5.js +1 -0
  3. package/dist/index-8kj7sa1p.js +12 -0
  4. package/dist/index-8rqfbhfw.js +221 -0
  5. package/dist/index-b3q3j39x.js +2 -0
  6. package/dist/index-bmd7x5aq.js +2 -0
  7. package/dist/index-eqh53k8v.js +2 -0
  8. package/dist/{index-sdt2d2ra.js → index-hy9030jh.js} +1 -1
  9. package/dist/index-jyr8y8k4.js +7 -0
  10. package/dist/index-rv08jmd4.js +2 -0
  11. package/dist/index.d.ts +578 -145
  12. package/dist/index.js +1 -110
  13. package/dist/pool-lifecycle-DbDmgabd.d.ts +2430 -0
  14. package/dist/port-file-Do3lQIEk.d.ts +29 -0
  15. package/dist/protocol-BOBdkqqm.d.ts +198 -0
  16. package/dist/rpc/port-file.d.ts +4 -17
  17. package/dist/rpc/port-file.js +1 -1
  18. package/dist/rpc/protocol.d.ts +2 -2
  19. package/dist/rpc/protocol.js +1 -0
  20. package/dist/rpc/rpc-client.d.ts +5 -4
  21. package/dist/rpc/rpc-client.js +1 -1
  22. package/dist/rpc/rpc-dir.d.ts +2 -0
  23. package/dist/rpc/rpc-dir.js +1 -1
  24. package/dist/sidebar-state-D6cUKgkm.d.ts +385 -0
  25. package/dist/sidebar-state.d.ts +2 -2
  26. package/dist/sidebar-state.js +1 -1
  27. package/dist/token-util-wdza526f.js +1 -0
  28. package/dist/{token-9823y0pc.js → token-ypk61xj0.js} +1 -1
  29. package/dist/tui-preferences.js +5 -2
  30. package/dist/tui.d.ts +9 -4
  31. package/dist/v2/server.d.ts +54 -0
  32. package/dist/v2/server.js +4 -0
  33. package/package.json +21 -9
  34. package/src/tui/entry.mjs +26 -25
  35. package/src/tui-compiled/raw/logger.js +24 -0
  36. package/src/tui-compiled/raw/rpc/protocol.js +3 -0
  37. package/src/tui-compiled/raw/rpc/rpc-client.js +7 -0
  38. package/src/tui-compiled/raw/rpc/rpc-dir.js +27 -0
  39. package/src/tui-compiled/raw/selector.js +21 -0
  40. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +138 -0
  41. package/src/tui-compiled/raw/shared/04a6357627de5ed0-errors.js +38 -0
  42. package/src/tui-compiled/raw/shared/053cc17b5419261e-codec.js +12 -0
  43. package/src/tui-compiled/raw/shared/0766d78c6f481f18-watcher.js +157 -0
  44. package/src/tui-compiled/raw/shared/08eef45626a94992-reserved.js +13 -0
  45. package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +134 -0
  46. package/src/tui-compiled/raw/shared/3c2d4169d00f907e-tui-support.js +5 -0
  47. package/src/tui-compiled/raw/shared/3e7d45a77c6ad4d5-index.js +4 -0
  48. package/src/tui-compiled/raw/shared/415792e85d80819e-capture-sink.js +13 -0
  49. package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +28 -0
  50. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
  51. package/src/tui-compiled/raw/shared/458b934e92490331-mutate.js +500 -0
  52. package/src/tui-compiled/raw/shared/45bf5343bbbf9add-admission.js +142 -0
  53. package/src/tui-compiled/raw/shared/46f3dff1266160e8-client.js +2 -0
  54. package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +48 -0
  55. package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +162 -0
  56. package/src/tui-compiled/raw/shared/54eba94fd64c8c3c-index.js +7 -0
  57. package/src/tui-compiled/raw/shared/7211cfec06a298bc-atomic-write.js +80 -0
  58. package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +534 -0
  59. package/src/tui-compiled/raw/shared/75bd8c3bd63b315f-identity.js +79 -0
  60. package/src/tui-compiled/raw/shared/7a91bb630e6f4804-index.js +1 -0
  61. package/src/tui-compiled/raw/shared/826342a4b6a8b89b-model.js +16 -0
  62. package/src/tui-compiled/raw/shared/83068c0dbe8a6f13-publication.js +304 -0
  63. package/src/tui-compiled/raw/shared/836fc615de6afbd9-settings.js +120 -0
  64. package/src/tui-compiled/raw/shared/83bc3ba9d0fe2761-runtime.js +68 -0
  65. package/src/tui-compiled/raw/shared/8a18abe4fa2977c8-menu.js +300 -0
  66. package/src/tui-compiled/raw/shared/901e149c5a0dbbff-sticky.js +339 -0
  67. package/src/tui-compiled/raw/shared/9020c407ce64f672-ordered.js +58 -0
  68. package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +105 -0
  69. package/src/tui-compiled/raw/shared/94820e324f2b4c09-seam.js +301 -0
  70. package/src/tui-compiled/raw/shared/97484d9bb98c705b-provider-state.js +226 -0
  71. package/src/tui-compiled/raw/shared/9968e3cade6969a1-index.js +2 -0
  72. package/src/tui-compiled/raw/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
  73. package/src/tui-compiled/raw/shared/9d3fc9e37456504e-refresh.js +225 -0
  74. package/src/tui-compiled/raw/shared/9deae3c4da4fd63b-hooks.js +34 -0
  75. package/src/tui-compiled/raw/shared/a09b6a533c11858f-tui-preferences.js +77 -0
  76. package/src/tui-compiled/raw/shared/a62a27a204266acd-attribution.js +60 -0
  77. package/src/tui-compiled/raw/shared/a910277ac43a8626-pull.js +143 -0
  78. package/src/tui-compiled/raw/shared/b85c5301fddcb98a-paths.js +5 -0
  79. package/src/tui-compiled/raw/shared/c14988621dc60a75-port-file.js +217 -0
  80. package/src/tui-compiled/raw/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
  81. package/src/tui-compiled/raw/shared/c6b176708c446d6d-pins.js +34 -0
  82. package/src/tui-compiled/raw/shared/c7d252bedee01600-pi.js +206 -0
  83. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +120 -0
  84. package/src/tui-compiled/raw/shared/cc9e0702a752cea7-index.js +3 -0
  85. package/src/tui-compiled/raw/shared/cd94316a888b0a8e-schema.js +754 -0
  86. package/src/tui-compiled/raw/shared/d319517e3ea6ad70-redact.js +83 -0
  87. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +5 -0
  88. package/src/tui-compiled/raw/shared/d669d72c40e9e116-logger.js +19 -0
  89. package/src/tui-compiled/raw/shared/d7dcba96fea1685c-engine.js +246 -0
  90. package/src/tui-compiled/raw/shared/dbef697d1df1cd84-index.js +5 -0
  91. package/src/tui-compiled/raw/shared/e4acc4cb713f300c-protocol.js +3 -0
  92. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
  93. package/src/tui-compiled/raw/shared/ec971142548df90e-index.js +6 -0
  94. package/src/tui-compiled/raw/shared/f37fcefc7aedbf25-error.js +11 -0
  95. package/src/tui-compiled/raw/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
  96. package/src/tui-compiled/raw/shared/fd45e82f2f9c0a0d-torn.js +294 -0
  97. package/src/tui-compiled/raw/shared/fe02abeadc535f77-rows.js +1258 -0
  98. package/src/tui-compiled/raw/sidebar-state.js +960 -0
  99. package/src/tui-compiled/raw/tui/command-dialogs.tsx +424 -0
  100. package/src/tui-compiled/raw/tui-preferences.js +135 -0
  101. package/src/tui-compiled/raw/tui.tsx +135 -143
  102. package/src/tui-compiled/runtime/logger.js +24 -0
  103. package/src/tui-compiled/runtime/rpc/protocol.js +3 -0
  104. package/src/tui-compiled/runtime/rpc/rpc-client.js +7 -0
  105. package/src/tui-compiled/runtime/rpc/rpc-dir.js +27 -0
  106. package/src/tui-compiled/runtime/selector.js +21 -0
  107. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +138 -0
  108. package/src/tui-compiled/runtime/shared/04a6357627de5ed0-errors.js +38 -0
  109. package/src/tui-compiled/runtime/shared/053cc17b5419261e-codec.js +12 -0
  110. package/src/tui-compiled/runtime/shared/0766d78c6f481f18-watcher.js +157 -0
  111. package/src/tui-compiled/runtime/shared/08eef45626a94992-reserved.js +13 -0
  112. package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +134 -0
  113. package/src/tui-compiled/runtime/shared/3c2d4169d00f907e-tui-support.js +5 -0
  114. package/src/tui-compiled/runtime/shared/3e7d45a77c6ad4d5-index.js +4 -0
  115. package/src/tui-compiled/runtime/shared/415792e85d80819e-capture-sink.js +13 -0
  116. package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +28 -0
  117. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
  118. package/src/tui-compiled/runtime/shared/458b934e92490331-mutate.js +500 -0
  119. package/src/tui-compiled/runtime/shared/45bf5343bbbf9add-admission.js +142 -0
  120. package/src/tui-compiled/runtime/shared/46f3dff1266160e8-client.js +2 -0
  121. package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +48 -0
  122. package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +162 -0
  123. package/src/tui-compiled/runtime/shared/54eba94fd64c8c3c-index.js +7 -0
  124. package/src/tui-compiled/runtime/shared/7211cfec06a298bc-atomic-write.js +80 -0
  125. package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +534 -0
  126. package/src/tui-compiled/runtime/shared/75bd8c3bd63b315f-identity.js +79 -0
  127. package/src/tui-compiled/runtime/shared/7a91bb630e6f4804-index.js +1 -0
  128. package/src/tui-compiled/runtime/shared/826342a4b6a8b89b-model.js +16 -0
  129. package/src/tui-compiled/runtime/shared/83068c0dbe8a6f13-publication.js +304 -0
  130. package/src/tui-compiled/runtime/shared/836fc615de6afbd9-settings.js +120 -0
  131. package/src/tui-compiled/runtime/shared/83bc3ba9d0fe2761-runtime.js +68 -0
  132. package/src/tui-compiled/runtime/shared/8a18abe4fa2977c8-menu.js +300 -0
  133. package/src/tui-compiled/runtime/shared/901e149c5a0dbbff-sticky.js +339 -0
  134. package/src/tui-compiled/runtime/shared/9020c407ce64f672-ordered.js +58 -0
  135. package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +105 -0
  136. package/src/tui-compiled/runtime/shared/94820e324f2b4c09-seam.js +301 -0
  137. package/src/tui-compiled/runtime/shared/97484d9bb98c705b-provider-state.js +226 -0
  138. package/src/tui-compiled/runtime/shared/9968e3cade6969a1-index.js +2 -0
  139. package/src/tui-compiled/runtime/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
  140. package/src/tui-compiled/runtime/shared/9d3fc9e37456504e-refresh.js +225 -0
  141. package/src/tui-compiled/runtime/shared/9deae3c4da4fd63b-hooks.js +34 -0
  142. package/src/tui-compiled/runtime/shared/a09b6a533c11858f-tui-preferences.js +77 -0
  143. package/src/tui-compiled/runtime/shared/a62a27a204266acd-attribution.js +60 -0
  144. package/src/tui-compiled/runtime/shared/a910277ac43a8626-pull.js +143 -0
  145. package/src/tui-compiled/runtime/shared/b85c5301fddcb98a-paths.js +5 -0
  146. package/src/tui-compiled/runtime/shared/c14988621dc60a75-port-file.js +217 -0
  147. package/src/tui-compiled/runtime/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
  148. package/src/tui-compiled/runtime/shared/c6b176708c446d6d-pins.js +34 -0
  149. package/src/tui-compiled/runtime/shared/c7d252bedee01600-pi.js +206 -0
  150. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +120 -0
  151. package/src/tui-compiled/runtime/shared/cc9e0702a752cea7-index.js +3 -0
  152. package/src/tui-compiled/runtime/shared/cd94316a888b0a8e-schema.js +754 -0
  153. package/src/tui-compiled/runtime/shared/d319517e3ea6ad70-redact.js +83 -0
  154. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +5 -0
  155. package/src/tui-compiled/runtime/shared/d669d72c40e9e116-logger.js +19 -0
  156. package/src/tui-compiled/runtime/shared/d7dcba96fea1685c-engine.js +246 -0
  157. package/src/tui-compiled/runtime/shared/dbef697d1df1cd84-index.js +5 -0
  158. package/src/tui-compiled/runtime/shared/e4acc4cb713f300c-protocol.js +3 -0
  159. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
  160. package/src/tui-compiled/runtime/shared/ec971142548df90e-index.js +6 -0
  161. package/src/tui-compiled/runtime/shared/f37fcefc7aedbf25-error.js +11 -0
  162. package/src/tui-compiled/runtime/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
  163. package/src/tui-compiled/runtime/shared/fd45e82f2f9c0a0d-torn.js +294 -0
  164. package/src/tui-compiled/runtime/shared/fe02abeadc535f77-rows.js +1258 -0
  165. package/src/tui-compiled/runtime/sidebar-state.js +960 -0
  166. package/src/tui-compiled/runtime/tui/command-dialogs.js +378 -0
  167. package/src/tui-compiled/runtime/tui-preferences.js +135 -0
  168. package/src/tui-compiled/runtime/{tui.tsx → tui.js} +288 -316
  169. package/dist/index-4effjmr6.js +0 -2
  170. package/dist/index-9ehhb051.js +0 -2
  171. package/dist/index-g3hsdjh4.js +0 -2
  172. package/dist/index-hh52v8sb.js +0 -2
  173. package/dist/index-jwn4r3sw.js +0 -1
  174. package/dist/index-sq36b120.js +0 -211
  175. package/dist/internal-BTndPmW2.d.ts +0 -914
  176. package/dist/quota-normalize-08hpp51v.js +0 -1
  177. package/dist/sidebar-state-CoKYKcLn.d.ts +0 -220
  178. package/dist/token-util-ypgz7rge.js +0 -1
  179. package/src/tui-compiled/raw/core-account-paths.ts +0 -114
  180. package/src/tui-compiled/raw/core-custody-state.ts +0 -243
  181. package/src/tui-compiled/raw/logger.ts +0 -53
  182. package/src/tui-compiled/raw/rpc-port-file.ts +0 -177
  183. package/src/tui-compiled/raw/rpc-protocol.ts +0 -12
  184. package/src/tui-compiled/raw/rpc-rpc-client.ts +0 -82
  185. package/src/tui-compiled/raw/rpc-rpc-dir.ts +0 -46
  186. package/src/tui-compiled/raw/sidebar-state.ts +0 -1659
  187. package/src/tui-compiled/raw/tui-command-dialogs.tsx +0 -1096
  188. package/src/tui-compiled/raw/tui-preferences.ts +0 -328
  189. package/src/tui-compiled/runtime/core-account-paths.ts +0 -114
  190. package/src/tui-compiled/runtime/core-custody-state.ts +0 -243
  191. package/src/tui-compiled/runtime/logger.ts +0 -53
  192. package/src/tui-compiled/runtime/rpc-port-file.ts +0 -177
  193. package/src/tui-compiled/runtime/rpc-protocol.ts +0 -12
  194. package/src/tui-compiled/runtime/rpc-rpc-client.ts +0 -82
  195. package/src/tui-compiled/runtime/rpc-rpc-dir.ts +0 -46
  196. package/src/tui-compiled/runtime/sidebar-state.ts +0 -1659
  197. package/src/tui-compiled/runtime/tui-command-dialogs.tsx +0 -855
  198. package/src/tui-compiled/runtime/tui-preferences.ts +0 -328
  199. package/src/tui-compiled/shared/internal.ts +0 -6
  200. package/src/tui-compiled/shared/logger.ts +0 -264
  201. package/src/tui-compiled/shared/paths.ts +0 -33
  202. package/src/tui-compiled/shared/refresh-file-lock.ts +0 -384
  203. package/src/tui-compiled/shared/util/error.ts +0 -9
  204. package/src/tui-compiled/shared/util/open-url.ts +0 -30
@@ -0,0 +1,534 @@
1
+ // The four sections every plugin gets from the library: Accounts, Quota,
2
+ // Routing and Limits. They read the pool and the plugin's settings through
3
+ // the store and write only through store operations, so a menu action takes
4
+ // the same locks as any other writer of those files.
5
+ import { formatQuota, isQuotaMap, projectQuota, quotaWindowName, } from "./d5a8f1061ee2f114-index.js";
6
+ import { DEFAULT_FORMER_MAIN_ID, orderForPlacement, resolveRoutingMode, } from "./3e7d45a77c6ad4d5-index.js";
7
+ import { projectFailure, } from "./94820e324f2b4c09-seam.js";
8
+ /** `disabledReason` recorded when the user disables an account from the menu. */
9
+ export const MENU_DISABLED_REASON = 'disabled from the command menu';
10
+ function isRecord(value) {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+ function record(value) {
14
+ return isRecord(value) ? { ...value } : {};
15
+ }
16
+ /** The rows the menu can name: one per id, in roster order. */
17
+ function distinctRows(load) {
18
+ if (load.status !== 'ready')
19
+ return [];
20
+ const seen = new Set();
21
+ return load.rows.filter((row) => {
22
+ if (seen.has(row.id))
23
+ return false;
24
+ seen.add(row.id);
25
+ return true;
26
+ });
27
+ }
28
+ function rowName(row, identity) {
29
+ return row.label ?? identity ?? row.id;
30
+ }
31
+ function projected(row, scope) {
32
+ return isQuotaMap(row.quota) ? projectQuota(row.quota, scope) : undefined;
33
+ }
34
+ /** Real plurals for counts in summaries: `1 account`, `3 accounts`. */
35
+ function count(n, noun) {
36
+ return `${n} ${noun}${n === 1 ? '' : 's'}`;
37
+ }
38
+ /** The header every section's own actions are listed under. */
39
+ const ACTIONS_GROUP = 'Actions';
40
+ function loadProblem(load) {
41
+ if (load.status === 'pending-migration')
42
+ return 'The account pool has not been migrated yet.';
43
+ if (load.status === 'error')
44
+ return `The ${load.file} file cannot be read: ${load.reason}`;
45
+ return undefined;
46
+ }
47
+ function addedText(name, result) {
48
+ switch (result.outcome) {
49
+ case 'added':
50
+ return `Added ${name}.`;
51
+ case 'added-disabled':
52
+ return `Added ${name}, disabled: another enabled account has the same identity.`;
53
+ case 'completed':
54
+ return `Finished adding ${name}.`;
55
+ case 'rotated':
56
+ return `${name} was already in the pool; its credential was updated.`;
57
+ case 'exists':
58
+ return `${name} was already in the pool; its credential is unchanged.`;
59
+ }
60
+ }
61
+ /**
62
+ * What the user is told about a failed late login: the projected message,
63
+ * never the exception's own text, which can quote the login's request.
64
+ */
65
+ function failureMessage(error) {
66
+ return projectFailure(error).text;
67
+ }
68
+ async function snapshot(store) {
69
+ const load = await store.read();
70
+ const read = await store.readSettings();
71
+ return {
72
+ load,
73
+ rows: distinctRows(load),
74
+ settings: read.status === 'error' ? {} : read.settings,
75
+ };
76
+ }
77
+ function accountView(row, options) {
78
+ const identity = options.accounts?.describeIdentity
79
+ ? options.accounts.describeIdentity(row)
80
+ : row.identity;
81
+ // Copy only the fields the menu shows: the store row also carries its
82
+ // credential and fingerprint, which must never reach a payload.
83
+ const account = {
84
+ id: row.id,
85
+ ...(row.label !== undefined ? { label: row.label } : {}),
86
+ enabled: row.enabled,
87
+ type: row.type,
88
+ ...(identity !== undefined ? { identity } : {}),
89
+ };
90
+ return { account, name: rowName(row, identity) };
91
+ }
92
+ function accountsSection(options, snap) {
93
+ const { store, extraLocks } = options;
94
+ const locks = extraLocks ? { extraLocks } : {};
95
+ const ids = snap.rows.map((row) => row.id);
96
+ const problem = loadProblem(snap.load);
97
+ const enabledCount = snap.rows.filter((row) => row.enabled).length;
98
+ const items = snap.rows.map((row, index) => {
99
+ const { account, name } = accountView(row, options);
100
+ // An Accounts row shows only the credential type, identity and any
101
+ // disabled reason; the account's quota windows are shown in the Quota
102
+ // section instead, so this row fits one line.
103
+ const detail = [
104
+ row.type === 'api' ? 'API key' : 'OAuth',
105
+ ...(account.identity !== undefined ? [account.identity] : []),
106
+ ...(!row.invalid && !row.enabled && row.disabledReason
107
+ ? [`disabled: ${row.disabledReason}`]
108
+ : []),
109
+ ].join(' · ');
110
+ const status = row.invalid
111
+ ? 'invalid'
112
+ : row.enabled
113
+ ? 'enabled'
114
+ : 'disabled';
115
+ const actions = [];
116
+ if (!row.invalid && row.enabled)
117
+ actions.push({
118
+ id: 'disable',
119
+ label: 'Disable',
120
+ run: async () => {
121
+ await store.disable(row.id, MENU_DISABLED_REASON, locks);
122
+ return `Disabled ${name}.`;
123
+ },
124
+ });
125
+ if (!row.invalid && !row.enabled)
126
+ actions.push({
127
+ id: 'enable',
128
+ label: 'Enable',
129
+ run: async () => {
130
+ await store.enable(row.id, locks);
131
+ return `Enabled ${name}.`;
132
+ },
133
+ });
134
+ if (ids.length > 1)
135
+ actions.push({
136
+ id: 'move',
137
+ label: 'Move',
138
+ knobs: [
139
+ {
140
+ kind: 'choice',
141
+ id: 'position',
142
+ label: 'Position',
143
+ choices: ids.map((_, at) => ({
144
+ value: String(at + 1),
145
+ label: String(at + 1),
146
+ })),
147
+ value: String(index + 1),
148
+ },
149
+ ],
150
+ run: async ({ values }) => {
151
+ const position = Number(values.position);
152
+ const order = ids.filter((id) => id !== row.id);
153
+ order.splice(position - 1, 0, row.id);
154
+ const result = await store.reorder(order, locks);
155
+ return result.outcome === 'unchanged'
156
+ ? `${name} is already at position ${position}.`
157
+ : `Moved ${name} to position ${position}.`;
158
+ },
159
+ });
160
+ actions.push({
161
+ id: 'remove',
162
+ label: 'Remove',
163
+ irreversible: true,
164
+ confirm: `Remove ${name}? Its stored credential is deleted.`,
165
+ run: async () => {
166
+ await store.remove(row.id, {
167
+ ...locks,
168
+ ...(options.accounts?.protect
169
+ ? { protect: options.accounts.protect }
170
+ : {}),
171
+ });
172
+ return `Removed ${name}.`;
173
+ },
174
+ });
175
+ return {
176
+ id: row.id,
177
+ label: name,
178
+ detail,
179
+ group: 'Accounts',
180
+ status,
181
+ account,
182
+ actions,
183
+ };
184
+ });
185
+ const login = options.accounts?.login;
186
+ const actions = [];
187
+ if (login && !problem)
188
+ actions.push({
189
+ id: 'add',
190
+ label: login.label ?? 'Add account',
191
+ group: ACTIONS_GROUP,
192
+ knobs: login.knobs ?? [],
193
+ run: async ({ values, invocation }) => {
194
+ const outcome = await login.run(values, invocation);
195
+ if (outcome.status === 'cancelled')
196
+ return { ok: false, text: outcome.message ?? 'Login cancelled.' };
197
+ if (outcome.status === 'ready') {
198
+ const result = await store.add(outcome.account, locks);
199
+ return addedText(outcome.account.label ?? result.id, result);
200
+ }
201
+ // The login finishes after this call returns: add the account then,
202
+ // and send the outcome to the invocation that started the login.
203
+ void outcome.completion
204
+ .then(async (account) => {
205
+ if (!account) {
206
+ invocation.notify('Login cancelled.', 'warning');
207
+ return;
208
+ }
209
+ const result = await store.add(account, locks);
210
+ invocation.notify(addedText(account.label ?? result.id, result));
211
+ }, (error) => {
212
+ invocation.notify(`Adding the account failed: ${failureMessage(error)}`, 'error');
213
+ })
214
+ .catch((error) => {
215
+ invocation.notify(`Adding the account failed: ${failureMessage(error)}`, 'error');
216
+ });
217
+ return outcome.message;
218
+ },
219
+ });
220
+ return {
221
+ id: 'accounts',
222
+ slot: 'accounts',
223
+ title: 'Accounts',
224
+ content: {
225
+ lines: problem
226
+ ? [problem]
227
+ : [
228
+ snap.rows.length === 0
229
+ ? 'No accounts yet'
230
+ : `${count(snap.rows.length, 'account')}, ${enabledCount} enabled`,
231
+ ],
232
+ items: problem ? [] : items,
233
+ actions,
234
+ },
235
+ };
236
+ }
237
+ function quotaSection(options, snap) {
238
+ const scope = options.quota?.scope ?? 'all';
239
+ const now = options.now();
240
+ // A scoped menu names its model family in the header, since the quota
241
+ // shown is that family's view of each account.
242
+ const group = scope === 'all' ? 'Accounts' : `Accounts · ${scope}`;
243
+ // Checking quota asks the provider for a fresh reading. A plugin that
244
+ // polls its accounts itself (its own HTTP quota call) supplies
245
+ // `quota.check`; otherwise each row's reading is requested from the store,
246
+ // which runs the plugin's pull for that row. Checking one account and
247
+ // checking all use the same path.
248
+ const check = async (ids, invocation) => {
249
+ if (options.quota?.check)
250
+ await options.quota.check(ids, invocation);
251
+ else {
252
+ for (const id of ids)
253
+ options.store.requestReading(id);
254
+ await options.store.pullsSettled();
255
+ }
256
+ };
257
+ // Only a routing candidate (enabled, valid and holding a credential) is a
258
+ // row, because a row must do something when chosen and its action checks
259
+ // that account's quota. Any other account becomes a read-only line naming
260
+ // why it can't be checked (disabled, invalid, no credential), followed by
261
+ // its last stored quota reading.
262
+ const candidates = snap.rows.filter((row) => row.candidate);
263
+ const items = candidates.map((row) => {
264
+ const { account, name } = accountView(row, options);
265
+ const quota = projected(row, scope);
266
+ return {
267
+ id: row.id,
268
+ label: name,
269
+ group,
270
+ status: formatQuota(quota, { now, form: 'compact' }),
271
+ detail: formatQuota(quota, { now, form: 'full' }),
272
+ account,
273
+ actions: [
274
+ {
275
+ id: 'check',
276
+ label: 'Check this account',
277
+ run: async ({ invocation }) => {
278
+ await check([row.id], invocation);
279
+ return `Checked quota for ${name}.`;
280
+ },
281
+ },
282
+ ],
283
+ };
284
+ });
285
+ const unchecked = snap.rows
286
+ .filter((row) => !row.candidate)
287
+ .map((row) => {
288
+ const { name } = accountView(row, options);
289
+ const reason = row.invalid
290
+ ? 'invalid'
291
+ : !row.enabled
292
+ ? 'disabled'
293
+ : row.credential === undefined
294
+ ? 'no credential'
295
+ : 'unavailable';
296
+ const quota = formatQuota(projected(row, scope), {
297
+ now,
298
+ form: 'compact',
299
+ });
300
+ return `${name} (${reason}): ${quota}`;
301
+ });
302
+ const actions = [];
303
+ if (candidates.length > 0)
304
+ actions.push({
305
+ id: 'check',
306
+ label: 'Check now',
307
+ description: 'Checks every account that can be checked.',
308
+ group: ACTIONS_GROUP,
309
+ run: async ({ invocation }) => {
310
+ const ids = candidates.map((row) => row.id);
311
+ await check(ids, invocation);
312
+ return `Checked quota for ${count(ids.length, 'account')}.`;
313
+ },
314
+ });
315
+ return {
316
+ id: 'quota',
317
+ slot: 'quota',
318
+ title: 'Quota',
319
+ content: {
320
+ lines: snap.rows.length === 0 ? ['No accounts yet'] : unchecked,
321
+ items,
322
+ actions,
323
+ },
324
+ };
325
+ }
326
+ function routingSection(options, snap) {
327
+ const { store, extraLocks } = options;
328
+ const locks = extraLocks ? { extraLocks } : {};
329
+ const choices = [
330
+ { value: 'ordered', label: 'Ordered (roster order)' },
331
+ ...(options.routing?.orderedVariants ?? []),
332
+ { value: 'sticky-balanced', label: 'Sticky balanced' },
333
+ ];
334
+ const raw = record(snap.settings.routing).mode;
335
+ const current = typeof raw === 'string' && choices.some((choice) => choice.value === raw)
336
+ ? raw
337
+ : 'ordered';
338
+ const resolved = resolveRoutingMode(current);
339
+ const ids = snap.rows.map((row) => row.id);
340
+ const tried = orderForPlacement(ids, resolved.placement, options.routing?.formerMainId ?? DEFAULT_FORMER_MAIN_ID);
341
+ const label = choices.find((choice) => choice.value === current)?.label ?? current;
342
+ const lines = [`Mode: ${label}`];
343
+ if (ids.length > 0)
344
+ lines.push(`Roster order: ${ids.join(', ')}`);
345
+ if (resolved.mode === 'ordered' && tried.join() !== ids.join())
346
+ lines.push(`Tried in order: ${tried.join(', ')}`);
347
+ const actions = [
348
+ {
349
+ id: 'mode',
350
+ label: 'Change mode',
351
+ group: ACTIONS_GROUP,
352
+ knobs: [
353
+ { kind: 'choice', id: 'mode', label: 'Mode', choices, value: current },
354
+ ],
355
+ run: async ({ values }) => {
356
+ const mode = String(values.mode);
357
+ await store.updateSettings((settings) => {
358
+ settings.routing = { ...record(settings.routing), mode };
359
+ return undefined;
360
+ }, locks);
361
+ return `Routing mode set to ${choices.find((choice) => choice.value === mode)?.label ?? mode}.`;
362
+ },
363
+ },
364
+ ];
365
+ if (ids.length > 1)
366
+ actions.push({
367
+ id: 'order',
368
+ label: 'Set order',
369
+ group: ACTIONS_GROUP,
370
+ knobs: [
371
+ {
372
+ kind: 'text',
373
+ id: 'order',
374
+ label: 'Account ids, first to last',
375
+ value: ids.join(', '),
376
+ required: true,
377
+ },
378
+ ],
379
+ run: async ({ values }) => {
380
+ const order = String(values.order)
381
+ .split(/[\s,]+/)
382
+ .filter((id) => id.length > 0);
383
+ const result = await store.reorder(order, locks);
384
+ return result.outcome === 'unchanged'
385
+ ? 'The order is unchanged.'
386
+ : `Order set to ${result.ids.join(', ')}.`;
387
+ },
388
+ });
389
+ return {
390
+ id: 'routing',
391
+ slot: 'routing',
392
+ title: 'Routing',
393
+ content: { lines, actions },
394
+ };
395
+ }
396
+ function floorLabels(options, snap) {
397
+ if (options.limits?.labels)
398
+ return [...options.limits.labels];
399
+ const scope = options.quota?.scope ?? 'all';
400
+ const labels = [];
401
+ for (const row of snap.rows)
402
+ for (const limit of projected(row, scope)?.limits ?? [])
403
+ if (!labels.includes(limit.label))
404
+ labels.push(limit.label);
405
+ return labels.length > 0 ? labels : ['primary'];
406
+ }
407
+ /**
408
+ * The display name for each floor. A floor is keyed by the quota window
409
+ * label the plugin stores readings under (`primary`, `five_hour`). When some
410
+ * account's stored quota reading records that window's length, the floor is
411
+ * shown under the same name `formatQuota` gives the window (`5h`, `7d`);
412
+ * otherwise under the label itself. The settings file keeps the label.
413
+ */
414
+ function floorNames(options, snap) {
415
+ const scope = options.quota?.scope ?? 'all';
416
+ const names = new Map();
417
+ for (const row of snap.rows)
418
+ for (const limit of projected(row, scope)?.limits ?? [])
419
+ if (limit.windowMinutes !== undefined && !names.has(limit.label))
420
+ names.set(limit.label, quotaWindowName(limit));
421
+ return (label) => names.get(label) ?? label;
422
+ }
423
+ function limitsSection(options, snap) {
424
+ const { store, extraLocks } = options;
425
+ const locks = extraLocks ? { extraLocks } : {};
426
+ const killswitch = record(snap.settings.killswitch);
427
+ const enabled = killswitch.enabled === true;
428
+ const floors = record(killswitch.accounts);
429
+ const labels = floorLabels(options, snap);
430
+ const nameOf = floorNames(options, snap);
431
+ const state = enabled ? 'on' : 'off';
432
+ const items = snap.rows.map((row) => {
433
+ const { account, name } = accountView(row, options);
434
+ const own = record(floors[row.id]);
435
+ const set = labels.filter((label) => typeof own[label] === 'number');
436
+ return {
437
+ id: row.id,
438
+ label: name,
439
+ group: `Floors · killswitch ${state}`,
440
+ // A floor is the least "% left" the account may fall to.
441
+ status: set.length === 0
442
+ ? 'no floors'
443
+ : set.map((label) => `${nameOf(label)} ≥${own[label]}%`).join(' · '),
444
+ account,
445
+ actions: [
446
+ {
447
+ id: 'floors',
448
+ label: 'Set floors',
449
+ knobs: labels.map((label) => ({
450
+ kind: 'number',
451
+ id: label,
452
+ label: `Minimum % left for ${nameOf(label)}`,
453
+ min: 0,
454
+ max: 100,
455
+ ...(typeof own[label] === 'number'
456
+ ? { value: own[label] }
457
+ : {}),
458
+ })),
459
+ run: async ({ values }) => {
460
+ await store.updateSettings((settings) => {
461
+ const next = record(settings.killswitch);
462
+ const accounts = record(next.accounts);
463
+ const mine = record(accounts[row.id]);
464
+ for (const label of labels) {
465
+ const value = values[label];
466
+ if (typeof value === 'number')
467
+ mine[label] = value;
468
+ else if (value === null)
469
+ delete mine[label];
470
+ }
471
+ if (Object.keys(mine).length > 0)
472
+ accounts[row.id] = mine;
473
+ else
474
+ delete accounts[row.id];
475
+ if (Object.keys(accounts).length > 0)
476
+ next.accounts = accounts;
477
+ else
478
+ delete next.accounts;
479
+ settings.killswitch = next;
480
+ return undefined;
481
+ }, locks);
482
+ return `Floors updated for ${name}.`;
483
+ },
484
+ },
485
+ ],
486
+ };
487
+ });
488
+ return {
489
+ id: 'limits',
490
+ slot: 'limits',
491
+ title: 'Limits',
492
+ content: {
493
+ lines: [`Killswitch ${state}`],
494
+ items,
495
+ actions: [
496
+ {
497
+ id: 'killswitch',
498
+ label: enabled ? 'Turn killswitch off' : 'Turn killswitch on',
499
+ description: 'With the killswitch on, an account whose quota falls below one of its floors is not used.',
500
+ group: ACTIONS_GROUP,
501
+ knobs: [
502
+ {
503
+ kind: 'toggle',
504
+ id: 'enabled',
505
+ label: 'Killswitch',
506
+ value: !enabled,
507
+ },
508
+ ],
509
+ run: async ({ values }) => {
510
+ const on = values.enabled === true;
511
+ await store.updateSettings((settings) => {
512
+ settings.killswitch = {
513
+ ...record(settings.killswitch),
514
+ enabled: on,
515
+ };
516
+ return undefined;
517
+ }, locks);
518
+ return `Killswitch ${on ? 'on' : 'off'}.`;
519
+ },
520
+ },
521
+ ],
522
+ },
523
+ };
524
+ }
525
+ /** The four built-in sections, in their fixed order, from one read of the store. */
526
+ export async function builtinSections(options) {
527
+ const snap = await snapshot(options.store);
528
+ return [
529
+ accountsSection(options, snap),
530
+ quotaSection(options, snap),
531
+ routingSection(options, snap),
532
+ limitsSection(options, snap),
533
+ ];
534
+ }
@@ -0,0 +1,79 @@
1
+ /** Prefix of a refresh quarantine reason, followed by JSON of the expected and returned identities. */
2
+ export const IDENTITY_CONTRADICTED_REASON_PREFIX = 'identity-contradicted: ';
3
+ /** The reason recorded on a row disabled because an earlier row is the same account. */
4
+ export const DUPLICATE_IDENTITY_REASON = 'duplicate-identity';
5
+ /**
6
+ * Enabled OAuth rows holding a credential whose wire identity is not yet
7
+ * known. API-key rows and disabled rows never count.
8
+ */
9
+ export function countUnknownIdentityRows(rows) {
10
+ return rows.filter((row) => row.invalid === undefined &&
11
+ !row.staged &&
12
+ row.type === 'oauth' &&
13
+ row.enabled &&
14
+ row.credential !== undefined &&
15
+ row.identity === undefined).length;
16
+ }
17
+ /**
18
+ * Marks a row disabled with a reason: `enabled: false` in the roster row,
19
+ * which older readers honour, and the reason in the per-row entry. A row
20
+ * without an entry gets one at epoch 1. Nothing is ever deleted.
21
+ */
22
+ export function disableIn(tx, id, reason) {
23
+ const raw = tx.rosterRow(id);
24
+ if (!raw)
25
+ return;
26
+ raw.enabled = false;
27
+ const entry = tx.entry(id) ?? { credentialEpoch: 1, needsFirstReading: true };
28
+ // A routine disable must not erase the evidence needed to refuse enable.
29
+ const quarantined = typeof entry.disabledReason === 'string' &&
30
+ entry.disabledReason.startsWith(IDENTITY_CONTRADICTED_REASON_PREFIX);
31
+ tx.setEntry(id, {
32
+ ...entry,
33
+ disabledReason: quarantined ? entry.disabledReason : reason,
34
+ });
35
+ }
36
+ /**
37
+ * Marks a row enabled: `enabled: true` in the roster row and no
38
+ * `disabledReason` in its entry. A row without an entry is not given one.
39
+ */
40
+ export function enableIn(tx, id) {
41
+ const raw = tx.rosterRow(id);
42
+ if (!raw)
43
+ return;
44
+ raw.enabled = true;
45
+ const entry = tx.entry(id);
46
+ if (entry && 'disabledReason' in entry) {
47
+ const next = { ...entry };
48
+ delete next.disabledReason;
49
+ tx.setEntry(id, next);
50
+ }
51
+ }
52
+ /**
53
+ * Two enabled OAuth rows with one wire identity are the same account: the
54
+ * earlier row in roster order stays enabled and every later one is disabled
55
+ * with a reason. Returns the ids it disabled.
56
+ */
57
+ export function disableIdentityDuplicates(tx, identity) {
58
+ const holders = tx
59
+ .rows()
60
+ .filter((row) => row.invalid === undefined &&
61
+ !row.staged &&
62
+ row.type === 'oauth' &&
63
+ row.enabled &&
64
+ row.identity === identity);
65
+ const disabled = [];
66
+ for (const row of holders.slice(1)) {
67
+ disableIn(tx, row.id, DUPLICATE_IDENTITY_REASON);
68
+ disabled.push(row.id);
69
+ }
70
+ return disabled;
71
+ }
72
+ /** Records a row's wire identity in its roster row, then applies dedupe. */
73
+ export function recordIdentityIn(tx, id, identity) {
74
+ const raw = tx.rosterRow(id);
75
+ if (!raw)
76
+ return [];
77
+ raw.accountId = identity;
78
+ return disableIdentityDuplicates(tx, identity);
79
+ }
@@ -0,0 +1 @@
1
+ export { createSidebarFile } from "./c64d7afbc14ca125-sidebar-file.js";
@@ -0,0 +1,16 @@
1
+ // The command menu: sections of items, each carrying actions with typed
2
+ // inputs. Two kinds of type live here. The `Menu*` types and the request and
3
+ // result types are plain data: they cross the loopback RPC to a host's TUI
4
+ // and are what an in-process renderer walks. The `*Definition` types carry
5
+ // the action bodies a plugin or the library supplies; they never leave the
6
+ // process and are turned into the plain types only by the seam in `seam.ts`.
7
+ /** The section slots, in the one order every renderer shows them. */
8
+ export const SECTION_SLOTS = [
9
+ 'accounts',
10
+ 'quota',
11
+ 'routing',
12
+ 'limits',
13
+ 'cache',
14
+ 'diagnostics',
15
+ 'extra',
16
+ ];