nothumanallowed 8.2.0 → 8.3.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 +19 -19
- package/src/constants.mjs +1 -1
- package/src/services/github.mjs +77 -0
- package/src/services/google-contacts.mjs +10 -0
- package/src/services/tool-executor.mjs +31 -0
- package/src/services/web-ui.mjs +12 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.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
|
@@ -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
|
|
791
|
-
|
|
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
|
|
809
|
-
|
|
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
|
|
826
|
-
|
|
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.
|
|
8
|
+
export const VERSION = '8.3.0';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/services/github.mjs
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -606,31 +606,33 @@ function openDayDetail(dateStr){
|
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
// ---- GITHUB ----
|
|
609
|
-
var ghData=null;
|
|
609
|
+
var ghData=null;var ghRepo='';
|
|
610
610
|
function renderGitHub(el){
|
|
611
611
|
el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading GitHub...</div></div>';
|
|
612
612
|
apiGet('/api/github').then(function(r){
|
|
613
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
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(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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(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>';
|
|
616
|
+
var notifs=r.notifications||[];
|
|
617
|
+
if(notifs.length>0){
|
|
618
|
+
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>';
|
|
619
|
+
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)+' · '+esc(n.updated)+'</div></div>'});
|
|
619
620
|
}
|
|
620
621
|
if(r.issues&&r.issues.length>0){
|
|
621
622
|
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)+'<
|
|
623
|
+
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
624
|
}
|
|
624
625
|
if(r.prs&&r.prs.length>0){
|
|
625
626
|
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
|
+
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
628
|
}
|
|
628
|
-
if(!
|
|
629
|
+
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
630
|
el.innerHTML=h;
|
|
630
631
|
});
|
|
631
632
|
}
|
|
632
|
-
function loadGhIssues(){var
|
|
633
|
-
function loadGhPRs(){var
|
|
633
|
+
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()})}
|
|
634
|
+
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()})}
|
|
635
|
+
function ghMarkRead(){apiPost('/api/github/mark-read',{}).then(function(){if(ghData)ghData.notifications=[];render()})}
|
|
634
636
|
|
|
635
637
|
// ---- NOTION ----
|
|
636
638
|
function renderNotion(el){
|