cohvu 2.13.0 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/App.js CHANGED
@@ -18,7 +18,9 @@ import { KnowledgeTab } from './tabs/KnowledgeTab.js';
18
18
  import { TeamTab } from './tabs/TeamTab.js';
19
19
  import { BillingTab } from './tabs/BillingTab.js';
20
20
  import { ProjectTab } from './tabs/ProjectTab.js';
21
+ import { KeysTab } from './tabs/KeysTab.js';
21
22
  import { YouTab } from './tabs/YouTab.js';
23
+ import { KNOWN_TOOL_NAMES } from './state.js';
22
24
  import { daysUntil, timeUntil } from './utils.js';
23
25
  import { exec, execFile } from 'child_process';
24
26
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, watch } from 'fs';
@@ -219,6 +221,20 @@ export default function App() {
219
221
  }
220
222
  break;
221
223
  }
224
+ case 'project':
225
+ break;
226
+ case 'keys': {
227
+ dispatch({ type: 'SET_API_KEYS_LOADING', loading: true });
228
+ // Integration keys require admin on the active project — skip the
229
+ // call if not, since the API will 403 anyway.
230
+ const integrationKeys = (s.userRole === 'admin')
231
+ ? await api.listIntegrationKeys(projectId).catch(() => [])
232
+ : [];
233
+ if (stateRef.current.activeProjectId !== projectId)
234
+ break;
235
+ dispatch({ type: 'SET_API_KEYS', keys: integrationKeys });
236
+ break;
237
+ }
222
238
  }
223
239
  }
224
240
  catch {
@@ -501,7 +517,7 @@ export default function App() {
501
517
  }
502
518
  // Number keys switch tabs (but not when typing in search mode)
503
519
  const tabIdx = parseInt(input, 10);
504
- if (tabIdx >= 1 && tabIdx <= 5 && !key.ctrl && !key.meta && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
520
+ if (tabIdx >= 1 && tabIdx <= TABS.length && !key.ctrl && !key.meta && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
505
521
  dispatch({ type: 'SWITCH_TAB', tab: TABS[tabIdx - 1] });
506
522
  setTimeout(() => loadTabData(), 0);
507
523
  return;
@@ -525,11 +541,50 @@ export default function App() {
525
541
  case 'project':
526
542
  await handleProjectKey(input, key);
527
543
  break;
544
+ case 'keys':
545
+ await handleKeysKey(input, key);
546
+ break;
528
547
  case 'you':
529
548
  await handleYouKey(input, key);
530
549
  break;
531
550
  }
532
551
  }
552
+ // ---- Keys tab keys ----
553
+ async function handleKeysKey(input, key) {
554
+ const s = stateRef.current;
555
+ if (key.upArrow) {
556
+ dispatch({ type: 'SET_API_KEYS_SELECTED', index: Math.max(0, s.apiKeysSelected - 1) });
557
+ return;
558
+ }
559
+ if (key.downArrow) {
560
+ const max = Math.max(0, s.apiKeys.length - 1);
561
+ dispatch({ type: 'SET_API_KEYS_SELECTED', index: Math.min(max, s.apiKeysSelected + 1) });
562
+ return;
563
+ }
564
+ if (input === 'n' && s.userRole === 'admin') {
565
+ const adminProjects = adminScopableProjects(s);
566
+ dispatch({ type: 'OPEN_MODAL', modal: {
567
+ kind: 'create-integration-key',
568
+ step: 'name',
569
+ input: '',
570
+ projectId: adminProjects[0]?.project_id ?? '',
571
+ projectSelectedIdx: 0,
572
+ role: 'member',
573
+ roleSelectedIdx: 1,
574
+ allowedTools: [],
575
+ toolsSelectedIdx: 0,
576
+ agentName: '',
577
+ } });
578
+ return;
579
+ }
580
+ if (input === 'x' && s.apiKeys.length > 0) {
581
+ const target = s.apiKeys[s.apiKeysSelected];
582
+ if (target) {
583
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-revoke-key', keyId: target.id, keyName: target.name } });
584
+ }
585
+ return;
586
+ }
587
+ }
533
588
  // ---- Knowledge keys ----
534
589
  async function handleKnowledgeKey(input, key) {
535
590
  const s = stateRef.current;
@@ -579,7 +634,7 @@ export default function App() {
579
634
  const projectId = s.activeProjectId;
580
635
  if (projectId) {
581
636
  let failures = 0;
582
- dispatch({ type: 'SET_OPERATION', operation: 'Removing memories' });
637
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing contributions' });
583
638
  await Promise.all([...s.forgetSelected].map(async (id) => {
584
639
  try {
585
640
  await api.deleteMemory(projectId, id);
@@ -838,7 +893,7 @@ export default function App() {
838
893
  }
839
894
  }
840
895
  // ---- Project keys ----
841
- async function handleProjectKey(input, _key) {
896
+ async function handleProjectKey(input, key) {
842
897
  const s = stateRef.current;
843
898
  if (input === 't') {
844
899
  dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-team', input: '' } });
@@ -955,7 +1010,7 @@ export default function App() {
955
1010
  if (modal.kind === 'confirm-forget' || modal.kind === 'confirm-remove-member' ||
956
1011
  modal.kind === 'confirm-leave' || modal.kind === 'confirm-logout' ||
957
1012
  modal.kind === 'confirm-regen-link' || modal.kind === 'initiate-consensus' ||
958
- modal.kind === 'approve-action') {
1013
+ modal.kind === 'approve-action' || modal.kind === 'confirm-revoke-key') {
959
1014
  if (input === 'y') {
960
1015
  const willExit = modal.kind === 'confirm-logout';
961
1016
  await confirmModal(modal);
@@ -967,6 +1022,116 @@ export default function App() {
967
1022
  }
968
1023
  return;
969
1024
  }
1025
+ // Integration key creation wizard
1026
+ if (modal.kind === 'create-integration-key') {
1027
+ const adminProjects = adminScopableProjects(s);
1028
+ if (modal.step === 'name') {
1029
+ if (key.return && modal.input.trim().length > 0) {
1030
+ if (adminProjects.length === 0) {
1031
+ showInlineError('no projects you can scope an integration key to', 3000);
1032
+ return;
1033
+ }
1034
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'project' } });
1035
+ }
1036
+ else if (key.backspace || key.delete) {
1037
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input.slice(0, -1) } });
1038
+ }
1039
+ else if (input === ' ') {
1040
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input + ' ' } });
1041
+ }
1042
+ else if (input.length === 1 && !key.ctrl && !key.meta) {
1043
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input + input } });
1044
+ }
1045
+ return;
1046
+ }
1047
+ if (modal.step === 'project') {
1048
+ if (adminProjects.length === 0)
1049
+ return;
1050
+ if (key.upArrow) {
1051
+ const i = Math.max(0, modal.projectSelectedIdx - 1);
1052
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: i, projectId: adminProjects[i].project_id } });
1053
+ }
1054
+ else if (key.downArrow) {
1055
+ const i = Math.min(adminProjects.length - 1, modal.projectSelectedIdx + 1);
1056
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: i, projectId: adminProjects[i].project_id } });
1057
+ }
1058
+ else if (key.return && modal.projectId) {
1059
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'role' } });
1060
+ }
1061
+ return;
1062
+ }
1063
+ if (modal.step === 'role') {
1064
+ const roles = ['admin', 'member', 'viewer'];
1065
+ if (key.upArrow) {
1066
+ const i = Math.max(0, modal.roleSelectedIdx - 1);
1067
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, roleSelectedIdx: i, role: roles[i] } });
1068
+ }
1069
+ else if (key.downArrow) {
1070
+ const i = Math.min(2, modal.roleSelectedIdx + 1);
1071
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, roleSelectedIdx: i, role: roles[i] } });
1072
+ }
1073
+ else if (key.return) {
1074
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'tools' } });
1075
+ }
1076
+ return;
1077
+ }
1078
+ if (modal.step === 'tools') {
1079
+ if (key.upArrow) {
1080
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, toolsSelectedIdx: Math.max(0, modal.toolsSelectedIdx - 1) } });
1081
+ }
1082
+ else if (key.downArrow) {
1083
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, toolsSelectedIdx: Math.min(KNOWN_TOOL_NAMES.length - 1, modal.toolsSelectedIdx + 1) } });
1084
+ }
1085
+ else if (input === ' ') {
1086
+ const t = KNOWN_TOOL_NAMES[modal.toolsSelectedIdx];
1087
+ const next = modal.allowedTools.includes(t)
1088
+ ? modal.allowedTools.filter(x => x !== t)
1089
+ : [...modal.allowedTools, t];
1090
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, allowedTools: next } });
1091
+ }
1092
+ else if (key.return) {
1093
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'agent' } });
1094
+ }
1095
+ return;
1096
+ }
1097
+ if (modal.step === 'agent') {
1098
+ if (key.return) {
1099
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'confirm' } });
1100
+ }
1101
+ else if (key.backspace || key.delete) {
1102
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName.slice(0, -1) } });
1103
+ }
1104
+ else if (input === ' ') {
1105
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName + ' ' } });
1106
+ }
1107
+ else if (input.length === 1 && !key.ctrl && !key.meta) {
1108
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName + input } });
1109
+ }
1110
+ return;
1111
+ }
1112
+ if (modal.step === 'confirm') {
1113
+ if (input === 'y') {
1114
+ await confirmModal(modal);
1115
+ // confirmModal handles the modal transition (closes on error, opens key-created on success)
1116
+ }
1117
+ else if (input === 'n') {
1118
+ dispatch({ type: 'CLOSE_MODAL' });
1119
+ }
1120
+ return;
1121
+ }
1122
+ return;
1123
+ }
1124
+ // Key created — copy or dismiss (esc handled at top)
1125
+ if (modal.kind === 'key-created') {
1126
+ if (input === 'c') {
1127
+ copyToClipboard(modal.keyValue);
1128
+ dispatch({ type: 'SET_COPIED_FEEDBACK', active: true });
1129
+ if (copiedTimerRef.current)
1130
+ clearTimeout(copiedTimerRef.current);
1131
+ copiedTimerRef.current = setTimeout(() => dispatch({ type: 'SET_COPIED_FEEDBACK', active: false }), 1500);
1132
+ }
1133
+ return;
1134
+ }
970
1135
  // Text input modals
971
1136
  if ('input' in modal) {
972
1137
  if (key.return) {
@@ -1338,6 +1503,55 @@ export default function App() {
1338
1503
  }
1339
1504
  return false;
1340
1505
  }
1506
+ if (modal.kind === 'create-integration-key') {
1507
+ // Only the confirm step submits — earlier steps just advance via OPEN_MODAL.
1508
+ if (modal.step !== 'confirm')
1509
+ return false;
1510
+ if (!modal.input.trim() || !modal.projectId)
1511
+ return false;
1512
+ try {
1513
+ dispatch({ type: 'SET_OPERATION', operation: 'Creating key' });
1514
+ const result = await api.createApiKey({
1515
+ kind: 'integration',
1516
+ name: modal.input.trim(),
1517
+ projects: [{ project_id: modal.projectId, role: modal.role }],
1518
+ ...(modal.allowedTools.length > 0 ? { allowed_tools: modal.allowedTools } : {}),
1519
+ ...(modal.agentName.trim() ? { default_agent_name: modal.agentName.trim() } : {}),
1520
+ });
1521
+ dispatch({ type: 'SET_OPERATION', operation: null });
1522
+ const integrationKeys = stateRef.current.userRole === 'admin' && stateRef.current.activeProjectId
1523
+ ? await api.listIntegrationKeys(stateRef.current.activeProjectId).catch(() => [])
1524
+ : [];
1525
+ dispatch({ type: 'SET_API_KEYS', keys: integrationKeys });
1526
+ dispatch({ type: 'OPEN_MODAL', modal: {
1527
+ kind: 'key-created',
1528
+ keyId: result.api_key.id,
1529
+ keyValue: result.key,
1530
+ keyName: result.api_key.name,
1531
+ kind2: 'integration',
1532
+ } });
1533
+ return true;
1534
+ }
1535
+ catch {
1536
+ dispatch({ type: 'SET_OPERATION', operation: null });
1537
+ showToast('Failed to create key', 'error');
1538
+ }
1539
+ return false;
1540
+ }
1541
+ if (modal.kind === 'confirm-revoke-key') {
1542
+ try {
1543
+ dispatch({ type: 'SET_OPERATION', operation: 'Revoking key' });
1544
+ await api.deleteApiKey(modal.keyId);
1545
+ dispatch({ type: 'REMOVE_API_KEY', id: modal.keyId });
1546
+ dispatch({ type: 'SET_OPERATION', operation: null });
1547
+ showToast('Key revoked', 'success');
1548
+ }
1549
+ catch {
1550
+ dispatch({ type: 'SET_OPERATION', operation: null });
1551
+ showToast('Failed to revoke key', 'error');
1552
+ }
1553
+ return false;
1554
+ }
1341
1555
  const projectId = s.activeProjectId;
1342
1556
  if (!projectId)
1343
1557
  return false;
@@ -1365,15 +1579,15 @@ export default function App() {
1365
1579
  }
1366
1580
  case 'confirm-forget':
1367
1581
  try {
1368
- dispatch({ type: 'SET_OPERATION', operation: 'Removing memory' });
1582
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing contribution' });
1369
1583
  await api.deleteMemory(projectId, modal.memoryId);
1370
1584
  dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
1371
1585
  dispatch({ type: 'SET_OPERATION', operation: null });
1372
- showToast('Memory removed', 'success');
1586
+ showToast('Contribution removed', 'success');
1373
1587
  }
1374
1588
  catch {
1375
1589
  dispatch({ type: 'SET_OPERATION', operation: null });
1376
- showToast('Failed to remove memory', 'error');
1590
+ showToast('Failed to remove contribution', 'error');
1377
1591
  }
1378
1592
  break;
1379
1593
  case 'confirm-forget-all':
@@ -1384,7 +1598,7 @@ export default function App() {
1384
1598
  const team = getActiveTeam(s);
1385
1599
  if (team) {
1386
1600
  try {
1387
- await api.initiateApproval(team.team_id, 'clear_memories', `clear all memories from "${project.slug}"`, projectId);
1601
+ await api.initiateApproval(team.team_id, 'clear_memories', `clear all contributions from "${project.slug}"`, projectId);
1388
1602
  const approvals = await api.listApprovals(team.team_id);
1389
1603
  dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
1390
1604
  showToast('Approval requested', 'info');
@@ -1396,15 +1610,15 @@ export default function App() {
1396
1610
  }
1397
1611
  }
1398
1612
  try {
1399
- dispatch({ type: 'SET_OPERATION', operation: 'Clearing memories' });
1613
+ dispatch({ type: 'SET_OPERATION', operation: 'Clearing contributions' });
1400
1614
  await api.clearMemories(projectId);
1401
1615
  dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
1402
1616
  dispatch({ type: 'SET_OPERATION', operation: null });
1403
- showToast('All memories cleared', 'success');
1617
+ showToast('All contributions cleared', 'success');
1404
1618
  }
1405
1619
  catch {
1406
1620
  dispatch({ type: 'SET_OPERATION', operation: null });
1407
- showToast('Failed to clear memories', 'error');
1621
+ showToast('Failed to clear contributions', 'error');
1408
1622
  }
1409
1623
  }
1410
1624
  break;
@@ -1641,19 +1855,31 @@ function renderTab(state, height) {
1641
1855
  case 'team': return _jsx(TeamTab, { state: state, height: height });
1642
1856
  case 'billing': return _jsx(BillingTab, { state: state, height: height });
1643
1857
  case 'project': return _jsx(ProjectTab, { state: state, height: height });
1858
+ case 'keys': return _jsx(KeysTab, { state: state, height: height });
1644
1859
  case 'you': return _jsx(YouTab, { state: state, height: height });
1645
1860
  }
1646
1861
  }
1862
+ // Projects the caller can scope an integration key to. Personal projects are
1863
+ // always scopable by their owner; team projects only by team admins.
1864
+ function adminScopableProjects(s) {
1865
+ return s.projects.filter(p => {
1866
+ if (p.owner.kind === 'personal')
1867
+ return true; // user owns it
1868
+ const teamId = p.owner.teamId;
1869
+ const team = s.teams.find(t => t.team_id === teamId);
1870
+ return team?.role === 'admin';
1871
+ });
1872
+ }
1647
1873
  function HelpOverlay({ state }) {
1648
1874
  const lines = [
1649
- ['tab / 1-5', 'switch tabs'],
1875
+ ['tab / 1-6', 'switch tabs'],
1650
1876
  ['?', 'toggle this help'],
1651
1877
  ['q', 'quit'],
1652
1878
  ['', ''],
1653
1879
  ];
1654
1880
  switch (state.tab) {
1655
1881
  case 'knowledge':
1656
- lines.push(['/', 'search'], ['enter', 'expand memory'], ['↑↓', 'navigate'], ['space', 'load more'], ['d', 'forget mode'], ['D', 'forget all (admin)']);
1882
+ lines.push(['/', 'search'], ['enter', 'expand contribution'], ['↑↓', 'navigate'], ['space', 'load more'], ['d', 'forget mode'], ['D', 'forget all (admin)']);
1657
1883
  break;
1658
1884
  case 'team':
1659
1885
  lines.push(['↑↓', 'navigate'], ['i', 'invite'], ['e', 'edit role (admin)'], ['x', 'remove member'], ['c', 'copy invite link'], ['r', 'regenerate link']);
@@ -1662,7 +1888,10 @@ function HelpOverlay({ state }) {
1662
1888
  lines.push(['s', 'subscribe'], ['p', 'billing portal']);
1663
1889
  break;
1664
1890
  case 'project':
1665
- lines.push(['w', 'switch project'], ['n', 'new project'], ['t', 'new team'], ['r', 'rename'], ['c', 'clear knowledge'], ['d', 'delete project']);
1891
+ lines.push(['↑↓', 'navigate links'], ['w', 'switch project'], ['n', 'new project'], ['t', 'new team'], ['r', 'rename'], ['c', 'clear knowledge'], ['d', 'delete project']);
1892
+ break;
1893
+ case 'keys':
1894
+ lines.push(['↑↓', 'navigate'], ['n', 'new key'], ['x', 'revoke selected']);
1666
1895
  break;
1667
1896
  case 'you':
1668
1897
  lines.push(['p', 'pause / resume'], ['r', 're-run setup'], ['l', 'logout']);