nothumanallowed 8.1.1 → 8.2.1
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/google-contacts.mjs +10 -0
- package/src/services/tool-executor.mjs +31 -0
- package/src/services/web-ui.mjs +106 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.1",
|
|
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.1';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -171,6 +171,16 @@ export async function updateContact(config, resourceName, fields) {
|
|
|
171
171
|
body.addresses = [{ formattedValue: fields.address }];
|
|
172
172
|
updateFields.push('addresses');
|
|
173
173
|
}
|
|
174
|
+
if (fields.birthday !== undefined) {
|
|
175
|
+
// birthday format: "YYYY-MM-DD" or "MM-DD"
|
|
176
|
+
const parts = fields.birthday.split('-').map(Number);
|
|
177
|
+
if (parts.length === 3) {
|
|
178
|
+
body.birthdays = [{ date: { year: parts[0], month: parts[1], day: parts[2] } }];
|
|
179
|
+
} else if (parts.length === 2) {
|
|
180
|
+
body.birthdays = [{ date: { month: parts[0], day: parts[1] } }];
|
|
181
|
+
}
|
|
182
|
+
updateFields.push('birthdays');
|
|
183
|
+
}
|
|
174
184
|
|
|
175
185
|
const data = await peopleFetch(config, `/${resourceName}:updateContact?updatePersonFields=${updateFields.join(',')}`, {
|
|
176
186
|
method: 'PATCH',
|
|
@@ -260,6 +260,9 @@ TOOLS:
|
|
|
260
260
|
45. birthdays_upcoming(days?: number)
|
|
261
261
|
Check upcoming birthdays from Google Contacts in the next N days (default 30).
|
|
262
262
|
|
|
263
|
+
46. birthday_add(name: string, date: string)
|
|
264
|
+
Add or update a birthday for a contact. Name is the contact name (must exist in Google Contacts — creates one if not found). Date is MM-DD (e.g. "04-06" for April 6) or YYYY-MM-DD.
|
|
265
|
+
|
|
263
266
|
RULES:
|
|
264
267
|
- For search/read operations, execute immediately and present results conversationally.
|
|
265
268
|
- For write/send/delete operations (gmail_send, gmail_reply, gmail_delete, calendar_create, calendar_move, calendar_update, contact_delete, task_done, notify_remind), DESCRIBE what you're about to do and include the JSON block so the system can ask the user for confirmation.
|
|
@@ -991,6 +994,34 @@ export async function executeTool(action, params, config) {
|
|
|
991
994
|
}).join('\n');
|
|
992
995
|
}
|
|
993
996
|
|
|
997
|
+
// ── Birthday Add ──────────────────────────────────────────────────
|
|
998
|
+
case 'birthday_add': {
|
|
999
|
+
const gc = await import('./google-contacts.mjs');
|
|
1000
|
+
const name = params.name;
|
|
1001
|
+
const date = params.date;
|
|
1002
|
+
if (!name || !date) return 'Both name and date are required. Date format: MM-DD or YYYY-MM-DD.';
|
|
1003
|
+
|
|
1004
|
+
// Search for existing contact
|
|
1005
|
+
let contacts = await gc.searchContacts(config, name, 5);
|
|
1006
|
+
let contact = contacts.find(c => c.name.toLowerCase().includes(name.toLowerCase()));
|
|
1007
|
+
|
|
1008
|
+
if (!contact) {
|
|
1009
|
+
// Create the contact first
|
|
1010
|
+
contact = await gc.createContact(config, { name });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Update birthday
|
|
1014
|
+
await gc.updateContact(config, contact.resourceName, { birthday: date });
|
|
1015
|
+
|
|
1016
|
+
// Parse date for display
|
|
1017
|
+
const parts = date.split('-').map(Number);
|
|
1018
|
+
const month = parts.length === 3 ? parts[1] : parts[0];
|
|
1019
|
+
const day = parts.length === 3 ? parts[2] : parts[1];
|
|
1020
|
+
const monthName = new Date(2000, month - 1, 1).toLocaleDateString('en-US', { month: 'long' });
|
|
1021
|
+
|
|
1022
|
+
return `Birthday set for ${contact.name}: ${monthName} ${day}. It will appear in the Birthdays tab.`;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
994
1025
|
default:
|
|
995
1026
|
return `Unknown action: ${action}`;
|
|
996
1027
|
}
|
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
|
}
|
|
@@ -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>
|