nothumanallowed 8.2.1 → 8.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "8.2.1",
3
+ "version": "8.3.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": {
@@ -787,13 +787,8 @@ export async function cmdUI(args) {
787
787
  if (method === 'GET' && pathname === '/api/github') {
788
788
  try {
789
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 });
790
+ const raw = await gh.listNotificationsRaw(config, 15);
791
+ sendJSON(res, 200, { notifications: raw });
797
792
  } catch (e) {
798
793
  sendJSON(res, 200, { error: e.message, notifications: [] });
799
794
  }
@@ -805,12 +800,8 @@ export async function cmdUI(args) {
805
800
  try {
806
801
  const gh = await import('../services/github.mjs');
807
802
  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 });
803
+ const raw = await gh.listIssuesRaw(config, repo, 'open', 15);
804
+ sendJSON(res, 200, { issues: raw, repo });
814
805
  } catch (e) {
815
806
  sendJSON(res, 200, { issues: [], error: e.message });
816
807
  }
@@ -822,12 +813,8 @@ export async function cmdUI(args) {
822
813
  try {
823
814
  const gh = await import('../services/github.mjs');
824
815
  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 });
816
+ const raw = await gh.listPRsRaw(config, repo, 'open', 15);
817
+ sendJSON(res, 200, { prs: raw, repo });
831
818
  } catch (e) {
832
819
  sendJSON(res, 200, { prs: [], error: e.message });
833
820
  }
@@ -835,6 +822,19 @@ export async function cmdUI(args) {
835
822
  return;
836
823
  }
837
824
 
825
+ // POST /api/github/mark-read — mark all notifications as read
826
+ if (method === 'POST' && pathname === '/api/github/mark-read') {
827
+ try {
828
+ const gh = await import('../services/github.mjs');
829
+ await gh.markNotificationsRead(config);
830
+ sendJSON(res, 200, { ok: true });
831
+ } catch (e) {
832
+ sendJSON(res, 200, { error: e.message });
833
+ }
834
+ logRequest(method, pathname, 200, Date.now() - start);
835
+ return;
836
+ }
837
+
838
838
  // ── Notion ──────────────────────────────────────────────────────────
839
839
  if (method === 'GET' && pathname === '/api/notion/search') {
840
840
  try {
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.2.1';
8
+ export const VERSION = '8.3.1';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -107,3 +107,80 @@ export async function createIssue(config, repo, title, body = '', labels = []) {
107
107
 
108
108
  return `Issue #${issue.number} created: "${issue.title}" — ${issue.html_url}`;
109
109
  }
110
+
111
+ // ── Raw JSON functions for UI (structured data, not text) ──────────────
112
+
113
+ /**
114
+ * List notifications as structured JSON for the web UI.
115
+ */
116
+ export async function listNotificationsRaw(config, maxResults = 15) {
117
+ const data = await ghFetch(config, `/notifications?per_page=${maxResults}`);
118
+ return (Array.isArray(data) ? data : []).map(n => ({
119
+ id: n.id,
120
+ repo: n.repository?.full_name || '',
121
+ type: n.subject?.type || '',
122
+ title: n.subject?.title || '',
123
+ reason: n.reason || '',
124
+ url: n.subject?.url ? buildHtmlUrl(n.subject.url, n.subject.type) : '',
125
+ updated: n.updated_at?.split('T')[0] || '',
126
+ }));
127
+ }
128
+
129
+ /**
130
+ * List issues as structured JSON for the web UI.
131
+ */
132
+ export async function listIssuesRaw(config, repo, state = 'open', maxResults = 15) {
133
+ if (!repo) return [];
134
+ const data = await ghFetch(config, `/repos/${repo}/issues?state=${state}&per_page=${maxResults}&sort=updated&direction=desc`);
135
+ return (Array.isArray(data) ? data : []).filter(i => !i.pull_request).map(i => ({
136
+ number: i.number,
137
+ title: i.title,
138
+ state: i.state,
139
+ labels: i.labels.map(l => l.name).join(', '),
140
+ assignee: i.assignee?.login || '',
141
+ updated: i.updated_at?.split('T')[0] || '',
142
+ url: i.html_url,
143
+ }));
144
+ }
145
+
146
+ /**
147
+ * List PRs as structured JSON for the web UI.
148
+ */
149
+ export async function listPRsRaw(config, repo, state = 'open', maxResults = 15) {
150
+ if (!repo) return [];
151
+ const data = await ghFetch(config, `/repos/${repo}/pulls?state=${state}&per_page=${maxResults}&sort=updated&direction=desc`);
152
+ return (Array.isArray(data) ? data : []).map(pr => ({
153
+ number: pr.number,
154
+ title: pr.title,
155
+ state: pr.state,
156
+ draft: pr.draft || false,
157
+ author: pr.user?.login || '',
158
+ reviewers: pr.requested_reviewers?.map(r => r.login).join(', ') || '',
159
+ updated: pr.updated_at?.split('T')[0] || '',
160
+ url: pr.html_url,
161
+ additions: pr.additions || 0,
162
+ deletions: pr.deletions || 0,
163
+ }));
164
+ }
165
+
166
+ /**
167
+ * Mark all notifications as read.
168
+ */
169
+ export async function markNotificationsRead(config) {
170
+ await ghFetch(config, '/notifications', {
171
+ method: 'PUT',
172
+ headers: { 'Content-Type': 'application/json' },
173
+ body: JSON.stringify({ last_read_at: new Date().toISOString() }),
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Convert API URL to browser HTML URL.
179
+ */
180
+ function buildHtmlUrl(apiUrl, type) {
181
+ if (!apiUrl) return '';
182
+ // api.github.com/repos/owner/repo/issues/1 → github.com/owner/repo/issues/1
183
+ let htmlUrl = apiUrl.replace('https://api.github.com/repos/', 'https://github.com/');
184
+ if (type === 'PullRequest') htmlUrl = htmlUrl.replace('/pulls/', '/pull/');
185
+ return htmlUrl;
186
+ }
@@ -384,16 +384,24 @@ function addTaskUI(){
384
384
  });
385
385
  }
386
386
  function toggleTask(id){
387
- apiPatch('/api/tasks/'+id+'/done').then(function(){loadDash().then(function(){if(currentView==='tasks'||currentView==='dashboard')render()})});
387
+ // Optimistic UI — update instantly, sync in background
388
+ var t=dash.tasks.find(function(x){return x.id===id});
389
+ if(t){t.status=t.status==='done'?'pending':'done';t.completedAt=t.status==='done'?new Date().toISOString():null}
390
+ render();
391
+ apiPatch('/api/tasks/'+id+'/done').then(function(){loadDash()});
388
392
  }
389
393
  function deleteTaskUI(id){
390
394
  if(!confirm('Delete task #'+id+'?'))return;
391
- apiPost('/api/tasks/'+id+'/delete',{}).then(function(){loadDash().then(function(){render()})});
395
+ dash.tasks=dash.tasks.filter(function(x){return x.id!==id});
396
+ render();
397
+ apiPost('/api/tasks/'+id+'/delete',{}).then(function(){loadDash()});
392
398
  }
393
399
  function clearTasksUI(mode){
394
400
  var msg=mode==='all'?'Delete ALL tasks? This cannot be undone.':'Remove all completed tasks?';
395
401
  if(!confirm(msg))return;
396
- apiPost('/api/tasks/clear',{mode:mode}).then(function(){loadDash().then(function(){render()})});
402
+ if(mode==='all'){dash.tasks=[]}else{dash.tasks=dash.tasks.filter(function(x){return x.status!=='done'})}
403
+ render();
404
+ apiPost('/api/tasks/clear',{mode:mode}).then(function(){loadDash()});
397
405
  }
398
406
 
399
407
  // ---- PLAN ----
@@ -606,31 +614,33 @@ function openDayDetail(dateStr){
606
614
  }
607
615
 
608
616
  // ---- GITHUB ----
609
- var ghData=null;
617
+ var ghData=null;var ghRepo='';
610
618
  function renderGitHub(el){
611
619
  el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading GitHub...</div></div>';
612
620
  apiGet('/api/github').then(function(r){
613
621
  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
622
  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>'});
623
+ var h='<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap"><input type="text" id="ghRepo" placeholder="owner/repo" value="'+esc(ghRepo)+'" style="flex:1;min-width:180px;font-size:13px;padding:10px 14px" onkeydown="if(event.key===\\x27Enter\\x27)loadGhIssues()"><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>';
624
+ var notifs=r.notifications||[];
625
+ if(notifs.length>0){
626
+ h+='<div style="display:flex;align-items:center;justify-content:space-between"><div class="section-title">Notifications ('+notifs.length+')</div><button onclick="ghMarkRead()" style="background:var(--bg3);color:var(--dim);border:1px solid var(--border);padding:4px 10px;border-radius:var(--r);font-size:10px;cursor:pointer">Mark all read</button></div>';
627
+ notifs.forEach(function(n){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="window.open(\\x27'+esc(n.url)+'\\x27,\\x27_blank\\x27)"><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)+' &middot; '+esc(n.updated)+'</div></div>'});
619
628
  }
620
629
  if(r.issues&&r.issues.length>0){
621
630
  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>'});
631
+ r.issues.forEach(function(i){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="window.open(\\x27'+esc(i.url)+'\\x27,\\x27_blank\\x27)"><span style="color:var(--green);font-weight:700">#'+i.number+'</span> '+esc(i.title)+(i.assignee?' <span style="font-size:10px;color:var(--cyan)">\\u2192 '+esc(i.assignee)+'</span>':'')+(i.labels?'<span style="font-size:9px;color:var(--amber);margin-left:6px">['+esc(i.labels)+']</span>':'')+'<div style="font-size:10px;color:var(--dim)">'+esc(i.updated)+'</div></div>'});
623
632
  }
624
633
  if(r.prs&&r.prs.length>0){
625
634
  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>'});
635
+ r.prs.forEach(function(p){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="window.open(\\x27'+esc(p.url)+'\\x27,\\x27_blank\\x27)"><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 style="font-size:10px;color:var(--dim)">'+esc(p.updated)+'</div></div>'});
627
636
  }
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>'}
637
+ if(!notifs.length&&!r.issues?.length&&!r.prs?.length){h+='<div class="card" style="text-align:center;color:var(--dim);padding:20px">Enter a repo above (e.g. owner/repo) and click Issues or PRs.<br>Notifications load automatically.</div>'}
629
638
  el.innerHTML=h;
630
639
  });
631
640
  }
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()})}
641
+ function loadGhIssues(){var inp=document.getElementById('ghRepo');if(!inp||!inp.value.trim())return;ghRepo=inp.value.trim();var el=document.getElementById('content');el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div></div>';apiGet('/api/github/issues?repo='+encodeURIComponent(ghRepo)).then(function(r){if(ghData){ghData.issues=r.issues||[];ghData.repo=r.repo}render()})}
642
+ function loadGhPRs(){var inp=document.getElementById('ghRepo');if(!inp||!inp.value.trim())return;ghRepo=inp.value.trim();var el=document.getElementById('content');el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div></div>';apiGet('/api/github/prs?repo='+encodeURIComponent(ghRepo)).then(function(r){if(ghData){ghData.prs=r.prs||[];ghData.repo=r.repo}render()})}
643
+ function ghMarkRead(){apiPost('/api/github/mark-read',{}).then(function(){if(ghData)ghData.notifications=[];render()})}
634
644
 
635
645
  // ---- NOTION ----
636
646
  function renderNotion(el){