nothumanallowed 8.1.0 → 8.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
4
4
  "description": "NotHumanAllowed — 38 AI agents + unified productivity suite. Gmail, Calendar, Drive, Contacts, Tasks, GitHub, Notion, Slack, voice chat, smart scheduler. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -783,6 +783,154 @@ export async function cmdUI(args) {
783
783
  return;
784
784
  }
785
785
 
786
+ // ── GitHub ───────────────────────────────────────────────────────
787
+ if (method === 'GET' && pathname === '/api/github') {
788
+ try {
789
+ const gh = await import('../services/github.mjs');
790
+ const notifications = await gh.listNotifications(config, 15).catch(() => '');
791
+ // Parse notifications text into structured data
792
+ const notifLines = notifications ? notifications.split('\n').filter(Boolean).map(l => {
793
+ const m = l.match(/^\d+\.\s+\[([^\]]*)\]\s+(\w+):\s+(.+)\s+\((\w+)\)$/);
794
+ return m ? { repo: m[1], type: m[2], title: m[3], reason: m[4] } : { repo: '', type: '', title: l, reason: '' };
795
+ }) : [];
796
+ sendJSON(res, 200, { notifications: notifLines });
797
+ } catch (e) {
798
+ sendJSON(res, 200, { error: e.message, notifications: [] });
799
+ }
800
+ logRequest(method, pathname, 200, Date.now() - start);
801
+ return;
802
+ }
803
+
804
+ if (method === 'GET' && pathname === '/api/github/issues') {
805
+ try {
806
+ const gh = await import('../services/github.mjs');
807
+ const repo = url.searchParams.get('repo');
808
+ const text = await gh.listIssues(config, repo, 'open', 15);
809
+ const issues = text.split('\n').filter(Boolean).map(l => {
810
+ const m = l.match(/^\d+\.\s+#(\d+)\s+(.+?)(?:\s+\[([^\]]*)\])?\s+\((\d{4}-\d{2}-\d{2})\)$/);
811
+ return m ? { number: parseInt(m[1]), title: m[2].trim(), labels: m[3] || '', updated: m[4] } : { number: 0, title: l, labels: '', updated: '' };
812
+ });
813
+ sendJSON(res, 200, { issues });
814
+ } catch (e) {
815
+ sendJSON(res, 200, { issues: [], error: e.message });
816
+ }
817
+ logRequest(method, pathname, 200, Date.now() - start);
818
+ return;
819
+ }
820
+
821
+ if (method === 'GET' && pathname === '/api/github/prs') {
822
+ try {
823
+ const gh = await import('../services/github.mjs');
824
+ const repo = url.searchParams.get('repo');
825
+ const text = await gh.listPRs(config, repo, 'open', 15);
826
+ const prs = text.split('\n').filter(Boolean).map(l => {
827
+ const m = l.match(/^\d+\.\s+#(\d+)\s+(.+?)(?:\s+\[DRAFT\])?\s+by\s+(\S+)/);
828
+ return m ? { number: parseInt(m[1]), title: m[2].trim(), author: m[3], draft: l.includes('[DRAFT]') } : { number: 0, title: l, author: '', draft: false };
829
+ });
830
+ sendJSON(res, 200, { prs });
831
+ } catch (e) {
832
+ sendJSON(res, 200, { prs: [], error: e.message });
833
+ }
834
+ logRequest(method, pathname, 200, Date.now() - start);
835
+ return;
836
+ }
837
+
838
+ // ── Notion ──────────────────────────────────────────────────────────
839
+ if (method === 'GET' && pathname === '/api/notion/search') {
840
+ try {
841
+ const nt = await import('../services/notion.mjs');
842
+ const q = url.searchParams.get('q') || '';
843
+ const text = await nt.search(config, q, 15);
844
+ const pages = text.split('\n').filter(Boolean).map(l => {
845
+ const m = l.match(/^\d+\.\s+\[(\w+)\]\s+(.?)\s+(.+?)\s+\(edited:\s+(\S+)\)\s+—\s+ID:\s+(\S+)$/);
846
+ return m ? { type: m[1], icon: m[2], title: m[3], edited: m[4], id: m[5] } : { type: 'Page', icon: '', title: l, edited: '', id: '' };
847
+ });
848
+ sendJSON(res, 200, { pages });
849
+ } catch (e) {
850
+ sendJSON(res, 200, { pages: [], error: e.message });
851
+ }
852
+ logRequest(method, pathname, 200, Date.now() - start);
853
+ return;
854
+ }
855
+
856
+ if (method === 'GET' && pathname === '/api/notion/page') {
857
+ try {
858
+ const nt = await import('../services/notion.mjs');
859
+ const id = url.searchParams.get('id') || '';
860
+ const text = await nt.getPage(config, id);
861
+ const titleMatch = text.match(/^Title:\s+(.+?)$/m);
862
+ const content = text.replace(/^Title:.*\n\n?/, '');
863
+ sendJSON(res, 200, { title: titleMatch ? titleMatch[1] : 'Page', content });
864
+ } catch (e) {
865
+ sendJSON(res, 200, { error: e.message });
866
+ }
867
+ logRequest(method, pathname, 200, Date.now() - start);
868
+ return;
869
+ }
870
+
871
+ // ── Slack ───────────────────────────────────────────────────────────
872
+ if (method === 'GET' && pathname === '/api/slack/channels') {
873
+ try {
874
+ const sl = await import('../services/slack.mjs');
875
+ const text = await sl.listChannels(config, 30);
876
+ const channels = text.split('\n').filter(Boolean).map(l => {
877
+ const m = l.match(/^\d+\.\s+#(\S+)\s+\((\d+)\s+members\)(?:\s+—\s+(.+))?$/);
878
+ return m ? { id: m[1], name: m[1], members: parseInt(m[2]), purpose: m[3] || '' } : { id: l, name: l, members: 0, purpose: '' };
879
+ });
880
+ sendJSON(res, 200, { channels });
881
+ } catch (e) {
882
+ sendJSON(res, 200, { channels: [], error: e.message });
883
+ }
884
+ logRequest(method, pathname, 200, Date.now() - start);
885
+ return;
886
+ }
887
+
888
+ if (method === 'GET' && pathname === '/api/slack/messages') {
889
+ try {
890
+ const sl = await import('../services/slack.mjs');
891
+ const channel = url.searchParams.get('channel') || '';
892
+ const text = await sl.listMessages(config, channel, 20);
893
+ const messages = text.split('\n').filter(Boolean).map(l => {
894
+ const m = l.match(/^(\d{1,2}:\d{2}\s*[AP]M)\s+\[([^\]]+)\]:\s+(.+)$/);
895
+ return m ? { time: m[1], user: m[2], text: m[3] } : { time: '', user: '', text: l };
896
+ });
897
+ sendJSON(res, 200, { messages });
898
+ } catch (e) {
899
+ sendJSON(res, 200, { messages: [], error: e.message });
900
+ }
901
+ logRequest(method, pathname, 200, Date.now() - start);
902
+ return;
903
+ }
904
+
905
+ // ── Birthdays ───────────────────────────────────────────────────────
906
+ if (method === 'GET' && pathname === '/api/birthdays') {
907
+ try {
908
+ const gc = await import('../services/google-contacts.mjs');
909
+ const contacts = await gc.getBirthdays(config);
910
+ const today = new Date();
911
+ const upcoming = [];
912
+ for (const c of contacts) {
913
+ if (!c.birthday) continue;
914
+ const parts = c.birthday.split('-');
915
+ const month = parseInt(parts.length === 3 ? parts[1] : parts[0], 10);
916
+ const day = parseInt(parts.length === 3 ? parts[2] : parts[1], 10);
917
+ const thisYear = new Date(today.getFullYear(), month - 1, day);
918
+ if (thisYear < today) thisYear.setFullYear(today.getFullYear() + 1);
919
+ const daysUntil = Math.ceil((thisYear - today) / 86400000);
920
+ if (daysUntil <= 90) {
921
+ const dateStr = thisYear.toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
922
+ upcoming.push({ name: c.name, date: dateStr, daysUntil });
923
+ }
924
+ }
925
+ upcoming.sort((a, b) => a.daysUntil - b.daysUntil);
926
+ sendJSON(res, 200, { birthdays: upcoming });
927
+ } catch (e) {
928
+ sendJSON(res, 200, { birthdays: [], error: e.message });
929
+ }
930
+ logRequest(method, pathname, 200, Date.now() - start);
931
+ return;
932
+ }
933
+
786
934
  // ── 404 ──────────────────────────────────────────────────────────
787
935
  sendJSON(res, 404, { error: 'Not found' });
788
936
  logRequest(method, pathname, 404, Date.now() - start);
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '8.1.0';
8
+ export const VERSION = '8.2.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -140,13 +140,16 @@ export async function runPlanningPipeline(config, opts = {}) {
140
140
  // ── Phase 6: CONDUCTOR — Synthesize daily plan ─────────────────────────
141
141
  info('Phase 6: CONDUCTOR synthesizing daily plan...');
142
142
 
143
- const conductorPrompt = `You are the NHA Daily Planner. Synthesize intelligence from 4 specialist agents into a structured, practical daily plan.
143
+ const conductorPrompt = `You are the NHA Daily Planner. Synthesize intelligence from specialist agents into a structured daily plan.
144
144
 
145
- IMPORTANT GUIDELINES:
146
- - Be PRACTICAL, not alarmist. Routine notifications (Google login alerts from your own devices, npm publish confirmations, GitHub security notices) are NOT security incidents.
147
- - Only escalate to "security_alerts" if there is a GENUINE, actionable threat (unknown logins from strange locations, actual phishing, credential leaks).
148
- - Focus on making the user's day productive, not on creating false urgency.
149
- - Suggest realistic time blocks based on the actual task complexity.
145
+ CRITICAL RULES — READ CAREFULLY:
146
+ 1. ONLY include events that appear in the CALENDAR section below. NEVER invent, hallucinate, or assume meetings/appointments that are not listed.
147
+ 2. If there are 0 events in the calendar, the schedule section must ONLY contain suggested focus blocks and task time — NO invented meetings.
148
+ 3. Be PRACTICAL, not alarmist. Routine notifications (Google login alerts from your own devices, npm publish confirmations, GitHub 2FA, password change emails) are NOT security threats. Mark them as SAFE.
149
+ 4. Only put items in "security_alerts" for GENUINE threats: phishing, unknown device access from unexpected locations, credential leaks, social engineering.
150
+ 5. Security alerts must be simple strings, NOT JSON objects. Example: "Verify Google login from unknown Mac device in Italy"
151
+ 6. The "schedule" section should reflect REAL calendar events + suggested blocks for tasks. Do not fabricate appointments.
152
+ 7. Do not create new_tasks that duplicate existing tasks.
150
153
 
151
154
  AGENT REPORTS:
152
155
  ${agentResults.saber ? `\n[SABER — Security Scan]\n${agentResults.saber}` : ''}
@@ -160,22 +163,22 @@ Events: ${events.length}
160
163
  Unread emails: ${emails.length}
161
164
  Tasks: ${tasks.length}
162
165
 
163
- CALENDAR:
164
- ${calendarContext || 'No events.'}
166
+ CALENDAR (these are the ONLY real events — do NOT add any others):
167
+ ${calendarContext || 'No events scheduled.'}
165
168
 
166
- EXISTING TASKS:
169
+ EXISTING TASKS (do NOT duplicate these):
167
170
  ${taskContext || 'No tasks.'}
168
171
 
169
- Create a comprehensive daily plan. Output strict JSON:
172
+ Output strict JSON:
170
173
  {
171
174
  "date": "${dateStr}",
172
- "executive_summary": "2-3 sentence overview",
175
+ "executive_summary": "2-3 sentence overview of the ACTUAL day based on real data",
173
176
  "priority_actions": [{ "time": "HH:MM", "action": "...", "source": "email|calendar|task", "priority": "critical|high|medium|low" }],
174
177
  "schedule": [{ "time_start": "HH:MM", "time_end": "HH:MM", "type": "meeting|focus|break|task", "title": "...", "notes": "...", "preparation": "..." }],
175
178
  "email_actions": [{ "from": "...", "subject": "...", "action": "reply|archive|flag|defer", "suggested_reply": "..." }],
176
- "security_alerts": [],
177
- "new_tasks": [{ "description": "...", "priority": "high|medium|low", "estimated_minutes": N, "suggested_slot": "HH:MM" }],
178
- "insights": []
179
+ "security_alerts": ["simple string descriptions only"],
180
+ "new_tasks": [{ "description": "...", "priority": "high|medium|low", "estimated_minutes": 30, "suggested_slot": "HH:MM" }],
181
+ "insights": ["simple string insights only"]
179
182
  }`;
180
183
 
181
184
  let plan;
@@ -266,7 +269,8 @@ function displayPlan(plan) {
266
269
  if (plan.security_alerts?.length > 0) {
267
270
  console.log(` ${BOLD}\x1b[0;31mSecurity Alerts${NC}`);
268
271
  for (const a of plan.security_alerts) {
269
- console.log(` \x1b[0;31m!\x1b[0m ${typeof a === 'string' ? a : a.message || JSON.stringify(a)}`);
272
+ const text = typeof a === 'string' ? a : a.description || a.message || a.action_required || `[${a.type || 'alert'}] ${a.severity || ''} — ${JSON.stringify(a)}`;
273
+ console.log(` \x1b[0;31m!\x1b[0m ${text}`);
270
274
  }
271
275
  console.log('');
272
276
  }
@@ -274,7 +278,8 @@ function displayPlan(plan) {
274
278
  if (plan.insights?.length > 0) {
275
279
  console.log(` ${BOLD}${D}Insights${NC}`);
276
280
  for (const i of plan.insights) {
277
- console.log(` ${D}→ ${typeof i === 'string' ? i : i.message || JSON.stringify(i)}${NC}`);
281
+ const text = typeof i === 'string' ? i : i.message || i.insight || JSON.stringify(i);
282
+ console.log(` ${D}→ ${text}${NC}`);
278
283
  }
279
284
  console.log('');
280
285
  }
@@ -285,6 +285,10 @@ function render(){
285
285
  case 'notes':renderNotes(el);break;
286
286
  case 'onedrive':renderOneDrive(el);break;
287
287
  case 'mstodo':renderMsTodo(el);break;
288
+ case 'github':renderGitHub(el);break;
289
+ case 'notion':renderNotion(el);break;
290
+ case 'slack':renderSlack(el);break;
291
+ case 'birthdays':renderBirthdays(el);break;
288
292
  case 'agents':renderAgents(el);break;
289
293
  case 'settings':renderSettings(el);break;
290
294
  }
@@ -401,8 +405,8 @@ function renderPlan(el){
401
405
  var h='<div class="plan-summary">'+esc(p.executive_summary||'No summary')+'</div>';
402
406
  if(p.priority_actions&&p.priority_actions.length>0){h+='<div class="section-title">Priority Actions</div>';p.priority_actions.forEach(function(a){h+='<div class="card plan-action"><span class="plan-action__time">'+esc(a.time||'')+'</span><span class="plan-action__text">'+esc(a.action)+'</span></div>'})}
403
407
  if(p.schedule&&p.schedule.length>0){h+='<div class="section-title">Schedule</div>';p.schedule.forEach(function(s){h+='<div class="card event"><span class="event__time">'+esc(s.time_start)+'-'+esc(s.time_end)+'</span><span class="event__title">'+esc(s.title)+'</span></div>'})}
404
- if(p.security_alerts&&p.security_alerts.length>0){h+='<div class="section-title" style="color:var(--red)">Security Alerts</div>';p.security_alerts.forEach(function(a){h+='<div class="card" style="border-color:var(--red)"><span style="color:var(--red)">'+esc(typeof a==='string'?a:a.message||JSON.stringify(a))+'</span></div>'})}
405
- if(p.insights&&p.insights.length>0){h+='<div class="section-title">Insights</div>';p.insights.forEach(function(i){h+='<div style="color:var(--dim);padding:4px 0;font-size:12px">\\u2192 '+esc(typeof i==='string'?i:i.message||'')+'</div>'})}
408
+ if(p.security_alerts&&p.security_alerts.length>0){h+='<div class="section-title" style="color:var(--red)">Security Alerts</div>';p.security_alerts.forEach(function(a){var txt=typeof a==='string'?a:(a.description||a.message||a.action_required||'Alert');var sev=typeof a==='object'&&a.severity?' ['+a.severity.toUpperCase()+']':'';h+='<div class="card" style="border-color:var(--red);padding:14px"><span style="color:var(--red);font-weight:700">!'+esc(sev)+'</span> '+esc(txt)+(typeof a==='object'&&a.action_required&&a.action_required!==txt?'<div style="color:var(--amber);font-size:11px;margin-top:6px">Action: '+esc(a.action_required)+'</div>':'')+'</div>'})}
409
+ if(p.insights&&p.insights.length>0){h+='<div class="section-title">Insights</div>';p.insights.forEach(function(i){var txt=typeof i==='string'?i:(i.message||i.insight||'');h+='<div style="color:var(--dim);padding:4px 0;font-size:12px">\\u2192 '+esc(txt)+'</div>'})}
406
410
  h+='<div style="margin-top:16px;text-align:center"><button class="btn btn--secondary" onclick="refreshPlan()">Regenerate</button></div>';
407
411
  el.innerHTML=h;
408
412
  });
@@ -601,6 +605,101 @@ function openDayDetail(dateStr){
601
605
  if(sendBtn)sendBtn.style.display='none';
602
606
  }
603
607
 
608
+ // ---- GITHUB ----
609
+ var ghData=null;
610
+ function renderGitHub(el){
611
+ el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading GitHub...</div></div>';
612
+ apiGet('/api/github').then(function(r){
613
+ if(r&&r.error){el.innerHTML='<div class="card" style="text-align:center;padding:30px"><div style="color:var(--dim);margin-bottom:8px">'+esc(r.error)+'</div><div style="font-size:11px;color:var(--dim)">Run: nha config set github-token YOUR_PAT</div></div>';return}
614
+ ghData=r;
615
+ var h='<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap"><input type="text" id="ghRepo" placeholder="owner/repo" value="'+esc(r.defaultRepo||'')+'" style="flex:1;min-width:180px;font-size:13px;padding:10px 14px"><button onclick="loadGhIssues()" style="background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px">Issues</button><button onclick="loadGhPRs()" style="background:var(--cyan);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px">PRs</button></div>';
616
+ if(r.notifications&&r.notifications.length>0){
617
+ h+='<div class="section-title">Notifications ('+r.notifications.length+')</div>';
618
+ r.notifications.forEach(function(n){h+='<div class="card" style="padding:10px 14px"><span style="color:var(--cyan);font-size:11px">'+esc(n.repo)+'</span> <span style="color:var(--dim);font-size:10px">['+esc(n.type)+']</span><div style="font-size:13px;margin-top:2px">'+esc(n.title)+'</div><div style="font-size:10px;color:var(--dim)">'+esc(n.reason)+'</div></div>'});
619
+ }
620
+ if(r.issues&&r.issues.length>0){
621
+ h+='<div class="section-title">Issues</div>';
622
+ r.issues.forEach(function(i){h+='<div class="card" style="padding:10px 14px"><span style="color:var(--green);font-weight:700">#'+i.number+'</span> '+esc(i.title)+'<div style="font-size:10px;color:var(--dim)">'+esc(i.labels||'')+' '+esc(i.updated||'')+'</div></div>'});
623
+ }
624
+ if(r.prs&&r.prs.length>0){
625
+ h+='<div class="section-title">Pull Requests</div>';
626
+ r.prs.forEach(function(p){h+='<div class="card" style="padding:10px 14px"><span style="color:var(--cyan);font-weight:700">#'+p.number+'</span> '+esc(p.title)+' <span style="font-size:10px;color:var(--dim)">by '+esc(p.author)+'</span>'+(p.draft?'<span style="font-size:9px;color:var(--amber)"> DRAFT</span>':'')+'</div>'});
627
+ }
628
+ if(!r.notifications?.length&&!r.issues?.length&&!r.prs?.length){h+='<div class="card" style="text-align:center;color:var(--dim);padding:20px">Enter a repo above and click Issues or PRs. Notifications are loaded automatically.</div>'}
629
+ el.innerHTML=h;
630
+ });
631
+ }
632
+ function loadGhIssues(){var repo=document.getElementById('ghRepo');if(!repo||!repo.value.trim())return;apiGet('/api/github/issues?repo='+encodeURIComponent(repo.value.trim())).then(function(r){if(ghData)ghData.issues=r.issues||[];render()})}
633
+ function loadGhPRs(){var repo=document.getElementById('ghRepo');if(!repo||!repo.value.trim())return;apiGet('/api/github/prs?repo='+encodeURIComponent(repo.value.trim())).then(function(r){if(ghData)ghData.prs=r.prs||[];render()})}
634
+
635
+ // ---- NOTION ----
636
+ function renderNotion(el){
637
+ el.innerHTML='<div style="display:flex;gap:8px;margin-bottom:16px"><input type="text" id="notionQuery" placeholder="Search Notion pages..." style="flex:1;font-size:13px;padding:10px 14px" onkeydown="if(event.key===\\x27Enter\\x27)searchNotion()"><button onclick="searchNotion()" style="background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px">Search</button></div><div id="notionResults"><div class="card" style="text-align:center;color:var(--dim);padding:20px">Search your Notion workspace. Requires: nha config set notion-token YOUR_TOKEN</div></div>';
638
+ }
639
+ function searchNotion(){
640
+ var q=document.getElementById('notionQuery');if(!q||!q.value.trim())return;
641
+ var res=document.getElementById('notionResults');res.innerHTML='<div style="text-align:center;padding:20px"><div class="spinner"></div></div>';
642
+ apiGet('/api/notion/search?q='+encodeURIComponent(q.value.trim())).then(function(r){
643
+ if(r&&r.error){res.innerHTML='<div class="card" style="color:var(--red);padding:14px">'+esc(r.error)+'</div>';return}
644
+ var pages=r.pages||[];
645
+ if(pages.length===0){res.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:20px">No results for "'+esc(q.value)+'"</div>';return}
646
+ var h='';pages.forEach(function(p){h+='<div class="card" style="padding:12px 14px;cursor:pointer" onclick="loadNotionPage(\\x27'+esc(p.id)+'\\x27)"><span style="font-size:14px">'+esc(p.icon||'')+'</span> <span style="font-weight:700">'+esc(p.title)+'</span> <span style="font-size:10px;color:var(--dim)">['+esc(p.type)+'] edited '+esc(p.edited)+'</span></div>'});
647
+ res.innerHTML=h;
648
+ });
649
+ }
650
+ function loadNotionPage(id){
651
+ var res=document.getElementById('notionResults');res.innerHTML='<div style="text-align:center;padding:20px"><div class="spinner"></div></div>';
652
+ apiGet('/api/notion/page?id='+encodeURIComponent(id)).then(function(r){
653
+ if(r&&r.error){res.innerHTML='<div class="card" style="color:var(--red);padding:14px">'+esc(r.error)+'</div>';return}
654
+ res.innerHTML='<div class="card" style="padding:16px"><div style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--green)">'+esc(r.title||'Page')+'</div><div style="white-space:pre-wrap;font-size:13px;line-height:1.6">'+esc(r.content||'(empty)')+'</div></div><button onclick="searchNotion()" style="margin-top:8px;background:var(--bg3);color:var(--dim);border:1px solid var(--border);padding:6px 12px;border-radius:var(--r);font-size:11px;cursor:pointer">Back to results</button>';
655
+ });
656
+ }
657
+
658
+ // ---- SLACK ----
659
+ var slackData=null;
660
+ function renderSlack(el){
661
+ el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading Slack channels...</div></div>';
662
+ apiGet('/api/slack/channels').then(function(r){
663
+ if(r&&r.error){el.innerHTML='<div class="card" style="text-align:center;padding:30px"><div style="color:var(--dim);margin-bottom:8px">'+esc(r.error)+'</div><div style="font-size:11px;color:var(--dim)">Run: nha config set slack-token xoxb-YOUR_TOKEN</div></div>';return}
664
+ slackData=r;
665
+ var channels=r.channels||[];
666
+ var h='<div class="section-title">Channels ('+channels.length+')</div>';
667
+ if(channels.length===0){h+='<div class="card" style="text-align:center;color:var(--dim);padding:20px">No channels found</div>'}
668
+ channels.forEach(function(c){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="loadSlackChannel(\\x27'+esc(c.id)+'\\x27,\\x27'+esc(c.name)+'\\x27)"><span style="color:var(--green);font-weight:700">#'+esc(c.name)+'</span> <span style="font-size:10px;color:var(--dim)">'+c.members+' members</span>'+(c.purpose?'<div style="font-size:11px;color:var(--dim);margin-top:2px">'+esc(c.purpose)+'</div>':'')+'</div>'});
669
+ h+='<div id="slackMessages"></div>';
670
+ el.innerHTML=h;
671
+ });
672
+ }
673
+ function loadSlackChannel(id,name){
674
+ var res=document.getElementById('slackMessages');if(!res)return;
675
+ res.innerHTML='<div style="text-align:center;padding:20px"><div class="spinner"></div><div style="color:var(--dim)">Loading #'+esc(name)+'...</div></div>';
676
+ apiGet('/api/slack/messages?channel='+encodeURIComponent(id)).then(function(r){
677
+ if(r&&r.error){res.innerHTML='<div class="card" style="color:var(--red);padding:14px">'+esc(r.error)+'</div>';return}
678
+ var msgs=r.messages||[];
679
+ var h='<div class="section-title" style="margin-top:16px">#'+esc(name)+' ('+msgs.length+' messages)</div>';
680
+ msgs.forEach(function(m){h+='<div style="padding:6px 0;border-bottom:1px solid var(--border)"><span style="color:var(--cyan);font-size:11px;font-weight:700">'+esc(m.user)+'</span> <span style="font-size:10px;color:var(--dim)">'+esc(m.time)+'</span><div style="font-size:13px;margin-top:2px">'+esc(m.text)+'</div></div>'});
681
+ if(msgs.length===0)h+='<div style="color:var(--dim);padding:16px;text-align:center">No recent messages</div>';
682
+ res.innerHTML=h;
683
+ });
684
+ }
685
+
686
+ // ---- BIRTHDAYS ----
687
+ function renderBirthdays(el){
688
+ el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading birthdays...</div></div>';
689
+ apiGet('/api/birthdays').then(function(r){
690
+ if(r&&r.error){el.innerHTML='<div class="card" style="text-align:center;padding:30px"><div style="color:var(--dim)">'+esc(r.error)+'</div></div>';return}
691
+ var bdays=r.birthdays||[];
692
+ if(bdays.length===0){el.innerHTML='<div class="card" style="text-align:center;padding:30px;color:var(--dim)">No upcoming birthdays found. Make sure your Google Contacts have birthday info.</div>';return}
693
+ var h='<div class="section-title">Upcoming Birthdays</div>';
694
+ bdays.forEach(function(b){
695
+ var isToday=b.daysUntil===0;
696
+ var label=isToday?'<span style="color:var(--red);font-weight:700">TODAY!</span>':b.daysUntil===1?'<span style="color:var(--amber)">Tomorrow</span>':'<span style="color:var(--dim)">in '+b.daysUntil+' days</span>';
697
+ h+='<div class="card" style="padding:12px 14px'+(isToday?';border-color:var(--red)':'')+'"><span style="font-size:16px">&#127874;</span> <span style="font-weight:700">'+esc(b.name)+'</span> — '+esc(b.date)+' '+label+'</div>';
698
+ });
699
+ el.innerHTML=h;
700
+ });
701
+ }
702
+
604
703
  // ---- AGENTS ----
605
704
  var AGENT_DESCRIPTIONS = {
606
705
  saber:'Security audits, OWASP Top 10, threat modeling, pentest planning',
@@ -1426,6 +1525,13 @@ init();
1426
1525
  <div class="nav-item" data-view="onedrive" onclick="switchView('onedrive')"><span class="nav-item__icon">&#9729;</span> OneDrive</div>
1427
1526
  <div class="nav-item" data-view="mstodo" onclick="switchView('mstodo')"><span class="nav-item__icon">&#128203;</span> To Do</div>
1428
1527
  </div>
1528
+ <div class="sidebar__section">
1529
+ <div class="sidebar__label">Integrations</div>
1530
+ <div class="nav-item" data-view="github" onclick="switchView('github')"><span class="nav-item__icon">&#128736;</span> GitHub</div>
1531
+ <div class="nav-item" data-view="notion" onclick="switchView('notion')"><span class="nav-item__icon">&#128214;</span> Notion</div>
1532
+ <div class="nav-item" data-view="slack" onclick="switchView('slack')"><span class="nav-item__icon">&#128488;</span> Slack</div>
1533
+ <div class="nav-item" data-view="birthdays" onclick="switchView('birthdays')"><span class="nav-item__icon">&#127874;</span> Birthdays</div>
1534
+ </div>
1429
1535
  <div class="sidebar__section">
1430
1536
  <div class="sidebar__label">AI</div>
1431
1537
  <div class="nav-item" data-view="agents" onclick="switchView('agents')"><span class="nav-item__icon">&#129302;</span> Agents</div>