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 +1 -1
- package/src/commands/ui.mjs +148 -0
- package/src/constants.mjs +1 -1
- package/src/services/ops-pipeline.mjs +21 -16
- package/src/services/web-ui.mjs +108 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "8.
|
|
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": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
143
|
+
const conductorPrompt = `You are the NHA Daily Planner. Synthesize intelligence from specialist agents into a structured daily plan.
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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==='
|
|
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(
|
|
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">🎂</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">☁</span> OneDrive</div>
|
|
1427
1526
|
<div class="nav-item" data-view="mstodo" onclick="switchView('mstodo')"><span class="nav-item__icon">📋</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">🛠</span> GitHub</div>
|
|
1531
|
+
<div class="nav-item" data-view="notion" onclick="switchView('notion')"><span class="nav-item__icon">📖</span> Notion</div>
|
|
1532
|
+
<div class="nav-item" data-view="slack" onclick="switchView('slack')"><span class="nav-item__icon">🗨</span> Slack</div>
|
|
1533
|
+
<div class="nav-item" data-view="birthdays" onclick="switchView('birthdays')"><span class="nav-item__icon">🎂</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">🤖</span> Agents</div>
|