nothumanallowed 13.2.81 → 13.2.83
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 +191 -31
- package/src/constants.mjs +1 -1
- package/src/services/github.mjs +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.2.
|
|
3
|
+
"version": "13.2.83",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -2958,12 +2958,30 @@ ${rawText.slice(0, 18000)}`;
|
|
|
2958
2958
|
// If there is document context from a previous step, ask the LLM to derive
|
|
2959
2959
|
// the optimal search queries. This is generic and works for any document/task.
|
|
2960
2960
|
let searchQueries = [stepPrompt.slice(0, 120)];
|
|
2961
|
-
|
|
2961
|
+
// Only use LLM query generation when context is a PDF/document (not previous agent text output).
|
|
2962
|
+
// When context is email/github output from a prior step, ignore it — use task + stepPrompt directly.
|
|
2963
|
+
const contextIsPdf = context && context.length > 50 && context.startsWith('## ATTACHED PDF');
|
|
2964
|
+
if (contextIsPdf) {
|
|
2962
2965
|
sendToken('[Building search queries from document...] ');
|
|
2963
2966
|
try {
|
|
2964
|
-
// Document context goes in system prompt — SENTINEL only scans user message
|
|
2965
2967
|
const queryPlanSys = `You are a search query generator. Given a document summary and a user task, output a JSON array of 1-3 concise web search queries (strings, max 80 chars each) that will find the best results. Output ONLY the JSON array, no explanation.\n\nDocument content:\n${context.slice(0, 3000)}`;
|
|
2966
|
-
const queryPlanUser = `User task: "${task.slice(0, 200)}". Generate search queries. If task asks for similar/alternative products use technical specs.
|
|
2968
|
+
const queryPlanUser = `User task: "${task.slice(0, 200)}". Generate search queries. If task asks for similar/alternative products use technical specs. Output: ["query1","query2",...]`;
|
|
2969
|
+
const planConfig2 = Object.assign({}, config, { thinking: 'off' });
|
|
2970
|
+
const queryRaw = await withTimeout(callLLM(planConfig2, queryPlanSys, queryPlanUser, { max_tokens: 200 }), 15000);
|
|
2971
|
+
const jsonMatch = queryRaw.match(/\[[\s\S]*?\]/);
|
|
2972
|
+
if (jsonMatch) {
|
|
2973
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2974
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
2975
|
+
searchQueries = parsed.filter(q => typeof q === 'string' && q.length > 2).slice(0, 3);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
} catch {}
|
|
2979
|
+
} else {
|
|
2980
|
+
// No PDF — derive queries from task + stepPrompt using LLM for better queries
|
|
2981
|
+
sendToken('[Building search queries...] ');
|
|
2982
|
+
try {
|
|
2983
|
+
const queryPlanSys = `You are a search query generator. Given a user task and a search instruction, output a JSON array of 2-3 concise web search queries (strings, max 80 chars each). Focus on the specific topics in the task. Output ONLY the JSON array, no explanation.`;
|
|
2984
|
+
const queryPlanUser = `Task: "${task.slice(0, 300)}"\nSearch instruction: "${stepPrompt.slice(0, 200)}"\nOutput: ["query1","query2","query3"]`;
|
|
2967
2985
|
const planConfig2 = Object.assign({}, config, { thinking: 'off' });
|
|
2968
2986
|
const queryRaw = await withTimeout(callLLM(planConfig2, queryPlanSys, queryPlanUser, { max_tokens: 200 }), 15000);
|
|
2969
2987
|
const jsonMatch = queryRaw.match(/\[[\s\S]*?\]/);
|
|
@@ -2974,8 +2992,8 @@ ${rawText.slice(0, 18000)}`;
|
|
|
2974
2992
|
}
|
|
2975
2993
|
}
|
|
2976
2994
|
} catch {}
|
|
2977
|
-
sendToken(`[Queries: ${searchQueries.map(q => '"' + q + '"').join(', ')}] `);
|
|
2978
2995
|
}
|
|
2996
|
+
sendToken(`[Queries: ${searchQueries.map(q => '"' + q + '"').join(', ')}] `);
|
|
2979
2997
|
|
|
2980
2998
|
// Run all queries sequentially, accumulate results
|
|
2981
2999
|
for (let qi = 0; qi < searchQueries.length; qi++) {
|
|
@@ -3012,24 +3030,64 @@ ${rawText.slice(0, 18000)}`;
|
|
|
3012
3030
|
toolData = 'GitHub token not configured. Run: nha config set github-token YOUR_PAT';
|
|
3013
3031
|
} else {
|
|
3014
3032
|
const parts = [];
|
|
3015
|
-
//
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3033
|
+
// Extract repo from prompt or task (e.g. "owner/repo" pattern)
|
|
3034
|
+
const repoMatch = (stepPrompt + ' ' + task).match(/([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)/);
|
|
3035
|
+
const targetRepo = repoMatch ? repoMatch[1].replace(/[`'"]/g, '') : (config.github?.defaultRepo || '');
|
|
3036
|
+
|
|
3037
|
+
if (targetRepo) {
|
|
3038
|
+
sendToken(`[Analyzing ${targetRepo}...] `);
|
|
3039
|
+
// Repo metadata
|
|
3040
|
+
try {
|
|
3041
|
+
const info = await withTimeout(gh.getRepoInfo(config, targetRepo), 20000);
|
|
3042
|
+
parts.push(`## Repository: ${info.full_name}\n` +
|
|
3043
|
+
`- Description: ${info.description || 'none'}\n` +
|
|
3044
|
+
`- Stars: ${info.stars} | Forks: ${info.forks} | Watchers: ${info.watchers}\n` +
|
|
3045
|
+
`- Open issues: ${info.open_issues}\n` +
|
|
3046
|
+
`- Primary language: ${info.language}\n` +
|
|
3047
|
+
`- Topics: ${info.topics}\n` +
|
|
3048
|
+
`- License: ${info.license}\n` +
|
|
3049
|
+
`- Last push: ${info.pushed_at} | Created: ${info.created_at}\n` +
|
|
3050
|
+
`- Homepage: ${info.homepage || 'none'}\n` +
|
|
3051
|
+
`- Archived: ${info.archived}`);
|
|
3052
|
+
} catch (e) { parts.push(`## Repository ${targetRepo}\nCould not fetch repo info: ${e.message}`); }
|
|
3053
|
+
// Languages
|
|
3054
|
+
try {
|
|
3055
|
+
const langs = await withTimeout(gh.getRepoLanguages(config, targetRepo), 10000);
|
|
3056
|
+
if (langs) parts.push('## Languages\n' + langs);
|
|
3057
|
+
} catch {}
|
|
3058
|
+
// README
|
|
3059
|
+
try {
|
|
3060
|
+
const readme = await withTimeout(gh.getReadme(config, targetRepo), 15000);
|
|
3061
|
+
if (readme) parts.push('## README\n' + readme.slice(0, 3000));
|
|
3062
|
+
} catch {}
|
|
3063
|
+
// Recent commits
|
|
3064
|
+
try {
|
|
3065
|
+
const commits = await withTimeout(gh.getRecentCommits(config, targetRepo, 10), 15000);
|
|
3066
|
+
if (commits) parts.push('## Recent Commits\n' + commits);
|
|
3067
|
+
} catch {}
|
|
3068
|
+
// Open issues
|
|
3023
3069
|
try {
|
|
3024
|
-
const issues = await withTimeout(gh.listIssues(config,
|
|
3025
|
-
if (issues) parts.push('## Open Issues
|
|
3026
|
-
} catch
|
|
3070
|
+
const issues = await withTimeout(gh.listIssues(config, targetRepo, 'open', 10), 15000);
|
|
3071
|
+
if (issues) parts.push('## Open Issues\n' + issues);
|
|
3072
|
+
} catch {}
|
|
3073
|
+
// Open PRs
|
|
3027
3074
|
try {
|
|
3028
|
-
const prs = await withTimeout(gh.listPRs(config,
|
|
3029
|
-
if (prs) parts.push('## Open
|
|
3030
|
-
} catch
|
|
3075
|
+
const prs = await withTimeout(gh.listPRs(config, targetRepo, 'open', 10), 15000);
|
|
3076
|
+
if (prs) parts.push('## Open Pull Requests\n' + prs);
|
|
3077
|
+
} catch {}
|
|
3078
|
+
// Contributors
|
|
3079
|
+
try {
|
|
3080
|
+
const contributors = await withTimeout(gh.getContributors(config, targetRepo, 10), 10000);
|
|
3081
|
+
if (contributors) parts.push('## Contributors\n' + contributors);
|
|
3082
|
+
} catch {}
|
|
3083
|
+
} else {
|
|
3084
|
+
// No specific repo — read notifications + user repos
|
|
3085
|
+
try {
|
|
3086
|
+
const notifs = await withTimeout(gh.listNotifications(config, 15), 15000);
|
|
3087
|
+
if (notifs) parts.push('## GitHub Notifications\n' + notifs);
|
|
3088
|
+
} catch {}
|
|
3031
3089
|
}
|
|
3032
|
-
toolData = parts.length > 0 ? parts.join('\n\n') : 'No GitHub data
|
|
3090
|
+
toolData = parts.length > 0 ? parts.join('\n\n') : 'No GitHub data could be retrieved.';
|
|
3033
3091
|
}
|
|
3034
3092
|
} catch (e) { toolData = `GitHub read failed: ${e.message}`; }
|
|
3035
3093
|
|
|
@@ -3188,14 +3246,15 @@ ${task}
|
|
|
3188
3246
|
CRITICAL RULES:
|
|
3189
3247
|
- Do NOT output JSON, tool calls, function calls, or code blocks
|
|
3190
3248
|
- NEVER invent, fabricate, or hallucinate data, events, emails, meetings, or news
|
|
3191
|
-
- ONLY use
|
|
3192
|
-
-
|
|
3249
|
+
- ONLY use data from the DATA sections that is RELEVANT to your specific domain and the WORKFLOW GOAL
|
|
3250
|
+
- If the previous agents' output contains irrelevant personal data (e.g. unrelated emails, purchases, subscriptions) — IGNORE it entirely
|
|
3251
|
+
- ONLY reference data that directly relates to the subject of the WORKFLOW GOAL
|
|
3252
|
+
- If genuinely no relevant data exists for your domain, say so clearly — do NOT invent analysis
|
|
3193
3253
|
- Write in plain prose, structured with markdown headers (##) and bullet points (-)
|
|
3194
3254
|
- Be thorough and specific — this is for an executive briefing based on REAL data only
|
|
3195
|
-
- Always keep the OVERALL WORKFLOW GOAL in mind — apply your analysis specifically to the subject mentioned
|
|
3196
3255
|
|
|
3197
3256
|
${attachmentText ? `## ATTACHED FILE CONTENT:\n${attachmentText}\n` : ''}${toolData ? `## LIVE DATA FROM TOOLS:\n${toolData}\n` : '## LIVE DATA: No tool data was fetched for this step.\n'}
|
|
3198
|
-
${context ? `## OUTPUT FROM PREVIOUS AGENTS:\n${context}\n` : ''}`;
|
|
3257
|
+
${context ? `## OUTPUT FROM PREVIOUS AGENTS (use only what is RELEVANT to the workflow goal):\n${context}\n` : ''}`;
|
|
3199
3258
|
userMsg = hasRealData
|
|
3200
3259
|
? `Based ONLY on the real data above, complete this task specifically for the subject in the WORKFLOW GOAL: ${stepPrompt}`
|
|
3201
3260
|
: `No real data is available for "${task}". State this clearly and explain what data would be needed to complete: ${stepPrompt}`;
|
|
@@ -3299,16 +3358,117 @@ ${context ? `## OUTPUT FROM PREVIOUS AGENTS:\n${context}\n` : ''}`;
|
|
|
3299
3358
|
const stopWords = new Set(['di','la','il','lo','le','gli','un','una','dei','del','della','per','che','con','su','in','e','a','da','è','come','analizza','analisi','ricerca','crea','genera','fai','fammi','dammi','the','of','for','and','a','an','in','with','on','about','analyze','analysis','research','create','generate','make','find','search']);
|
|
3300
3359
|
const titleWords = task.replace(/[.,;:!?]/g,'').split(/\s+/).filter(w => w.length > 2 && !stopWords.has(w.toLowerCase())).slice(0, 6);
|
|
3301
3360
|
const reportTitle = titleWords.length > 0 ? titleWords.map(w => w.charAt(0).toUpperCase()+w.slice(1)).join(' ') : 'Studio Report';
|
|
3302
|
-
//
|
|
3361
|
+
// Convert markdown to NHA-classed HTML — handles the case where Liara/Qwen3
|
|
3362
|
+
// returns markdown instead of HTML despite instructions.
|
|
3363
|
+
const mdToNhaHtml = (md) => {
|
|
3364
|
+
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
3365
|
+
// inline: bold, italic, inline-code, links
|
|
3366
|
+
const inl = s => s
|
|
3367
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
3368
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
3369
|
+
.replace(/`([^`]+)`/g, '<code style="background:#1c1c28;padding:1px 5px;border-radius:3px;font-size:12px">$1</code>')
|
|
3370
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
3371
|
+
|
|
3372
|
+
const lines = md.split('\n');
|
|
3373
|
+
let out = [];
|
|
3374
|
+
let i = 0;
|
|
3375
|
+
let currentSection = null; // accumulates section content
|
|
3376
|
+
|
|
3377
|
+
const flushSection = () => {
|
|
3378
|
+
if (currentSection) { out.push(currentSection + '</div>'); currentSection = null; }
|
|
3379
|
+
};
|
|
3380
|
+
|
|
3381
|
+
while (i < lines.length) {
|
|
3382
|
+
const l = lines[i];
|
|
3383
|
+
// H1 — treat as sub-header inside a new section
|
|
3384
|
+
if (/^# /.test(l)) {
|
|
3385
|
+
flushSection();
|
|
3386
|
+
const title = esc(l.replace(/^# /, '').replace(/\*\*/g,'').replace(/\*/g,''));
|
|
3387
|
+
currentSection = `<div class="section"><div class="section-title">${title}</div>`;
|
|
3388
|
+
i++; continue;
|
|
3389
|
+
}
|
|
3390
|
+
// H2 / H3 — new section
|
|
3391
|
+
if (/^#{2,3} /.test(l)) {
|
|
3392
|
+
flushSection();
|
|
3393
|
+
const title = esc(l.replace(/^#{2,3} /, '').replace(/\*\*/g,'').replace(/\*/g,''));
|
|
3394
|
+
currentSection = `<div class="section"><div class="section-title">${title}</div>`;
|
|
3395
|
+
i++; continue;
|
|
3396
|
+
}
|
|
3397
|
+
// H4 — sub-heading inside current section
|
|
3398
|
+
if (/^#### /.test(l)) {
|
|
3399
|
+
const h = esc(l.replace(/^#### /, '').replace(/\*\*/g,'').replace(/\*/g,''));
|
|
3400
|
+
const frag = `<h3>${h}</h3>`;
|
|
3401
|
+
if (currentSection) currentSection += frag; else out.push(frag);
|
|
3402
|
+
i++; continue;
|
|
3403
|
+
}
|
|
3404
|
+
// Horizontal rule — divider
|
|
3405
|
+
if (/^---+$/.test(l.trim())) {
|
|
3406
|
+
const frag = '<div class="divider"></div>';
|
|
3407
|
+
if (currentSection) currentSection += frag; else out.push(frag);
|
|
3408
|
+
i++; continue;
|
|
3409
|
+
}
|
|
3410
|
+
// Table row
|
|
3411
|
+
if (l.trim().startsWith('|') && l.includes('|', 1)) {
|
|
3412
|
+
// Collect all table lines
|
|
3413
|
+
const tableLines = [];
|
|
3414
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) {
|
|
3415
|
+
if (!/^\|[\s:|-]+\|$/.test(lines[i].trim())) tableLines.push(lines[i]);
|
|
3416
|
+
i++;
|
|
3417
|
+
}
|
|
3418
|
+
if (tableLines.length > 0) {
|
|
3419
|
+
const isHeader = tableLines.length > 1;
|
|
3420
|
+
let tHtml = '<table style="width:100%;border-collapse:collapse;margin:10px 0;font-size:12px">';
|
|
3421
|
+
tableLines.forEach((tl, ti) => {
|
|
3422
|
+
const cells = tl.split('|').slice(1,-1).map(c => c.trim());
|
|
3423
|
+
const tag = (ti === 0 && isHeader) ? 'th' : 'td';
|
|
3424
|
+
const bg = ti === 0 && isHeader ? 'background:#1c1c28;color:#a5b4fc;font-weight:700' : (ti % 2 === 0 ? 'background:#15151f' : 'background:#1a1a28');
|
|
3425
|
+
tHtml += '<tr>' + cells.map(c => `<${tag} style="${bg};padding:6px 10px;border:1px solid #2a2a38;text-align:left">${inl(esc(c))}</${tag}>`).join('') + '</tr>';
|
|
3426
|
+
});
|
|
3427
|
+
tHtml += '</table>';
|
|
3428
|
+
if (currentSection) currentSection += tHtml; else out.push(tHtml);
|
|
3429
|
+
}
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
// Unordered list block
|
|
3433
|
+
if (/^(\s*[-*+] )/.test(l)) {
|
|
3434
|
+
const items = [];
|
|
3435
|
+
while (i < lines.length && /^(\s*[-*+] )/.test(lines[i])) {
|
|
3436
|
+
items.push(inl(esc(lines[i].replace(/^\s*[-*+] /, ''))));
|
|
3437
|
+
i++;
|
|
3438
|
+
}
|
|
3439
|
+
const frag = '<ul>' + items.map(it => `<li>${it}</li>`).join('') + '</ul>';
|
|
3440
|
+
if (currentSection) currentSection += frag; else out.push(frag);
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
// Ordered list block
|
|
3444
|
+
if (/^\d+\. /.test(l)) {
|
|
3445
|
+
const items = [];
|
|
3446
|
+
while (i < lines.length && /^\d+\. /.test(lines[i])) {
|
|
3447
|
+
items.push(inl(esc(lines[i].replace(/^\d+\. /, ''))));
|
|
3448
|
+
i++;
|
|
3449
|
+
}
|
|
3450
|
+
const frag = '<ol>' + items.map(it => `<li>${it}</li>`).join('') + '</ol>';
|
|
3451
|
+
if (currentSection) currentSection += frag; else out.push(frag);
|
|
3452
|
+
continue;
|
|
3453
|
+
}
|
|
3454
|
+
// Blank line — skip
|
|
3455
|
+
if (!l.trim()) { i++; continue; }
|
|
3456
|
+
// Regular paragraph
|
|
3457
|
+
const frag = `<p>${inl(esc(l))}</p>`;
|
|
3458
|
+
if (currentSection) currentSection += frag; else out.push(frag);
|
|
3459
|
+
i++;
|
|
3460
|
+
}
|
|
3461
|
+
flushSection();
|
|
3462
|
+
return out.join('');
|
|
3463
|
+
};
|
|
3464
|
+
|
|
3465
|
+
// If LLM output has no HTML tags → it's markdown → convert
|
|
3303
3466
|
if (!bodyHtml || !bodyHtml.includes('<')) {
|
|
3304
|
-
const
|
|
3467
|
+
const source = bodyHtml || context;
|
|
3468
|
+
const converted = mdToNhaHtml(source);
|
|
3469
|
+
const agentNames = (stepDef && Array.isArray(stepDef)) ? '' : '';
|
|
3305
3470
|
bodyHtml = `<div class="header"><h1>${reportTitle.replace(/</g,'<')}</h1><p>NHA Studio Report \u00b7 ${today}</p><div class="meta"><span>${today}</span></div></div>` +
|
|
3306
|
-
|
|
3307
|
-
const lines = s.replace(/\*\*/g,'').replace(/\*/g,'').trim().split('\n').filter(Boolean);
|
|
3308
|
-
const stitle = lines[0] || '';
|
|
3309
|
-
const body = lines.slice(1).map(l => `<p>${l.replace(/</g,'<')}</p>`).join('');
|
|
3310
|
-
return `<div class="section"><div class="section-title">${stitle.replace(/</g,'<')}</div>${body}</div>`;
|
|
3311
|
-
}).join('') +
|
|
3471
|
+
converted +
|
|
3312
3472
|
`<div class="footer">NHA Studio \u00b7 ${today}</div>`;
|
|
3313
3473
|
} else {
|
|
3314
3474
|
// Replace the h1 inside existing header div if the model included the full prompt as title
|
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 = '13.2.
|
|
8
|
+
export const VERSION = '13.2.83';
|
|
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
|
@@ -185,6 +185,85 @@ export async function listUserRepos(config, maxResults = 30) {
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Get repository metadata: description, stars, forks, language, topics, license, last push.
|
|
190
|
+
*/
|
|
191
|
+
export async function getRepoInfo(config, repo) {
|
|
192
|
+
const data = await ghFetch(config, `/repos/${repo}`);
|
|
193
|
+
return {
|
|
194
|
+
full_name: data.full_name,
|
|
195
|
+
description: data.description || '',
|
|
196
|
+
stars: data.stargazers_count || 0,
|
|
197
|
+
forks: data.forks_count || 0,
|
|
198
|
+
watchers: data.watchers_count || 0,
|
|
199
|
+
open_issues: data.open_issues_count || 0,
|
|
200
|
+
language: data.language || 'N/A',
|
|
201
|
+
topics: (data.topics || []).join(', ') || 'none',
|
|
202
|
+
license: data.license?.name || 'none',
|
|
203
|
+
default_branch: data.default_branch || 'main',
|
|
204
|
+
pushed_at: data.pushed_at ? data.pushed_at.slice(0, 10) : 'unknown',
|
|
205
|
+
created_at: data.created_at ? data.created_at.slice(0, 10) : 'unknown',
|
|
206
|
+
homepage: data.homepage || '',
|
|
207
|
+
private: data.private,
|
|
208
|
+
archived: data.archived,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get programming languages breakdown for a repo.
|
|
214
|
+
*/
|
|
215
|
+
export async function getRepoLanguages(config, repo) {
|
|
216
|
+
const data = await ghFetch(config, `/repos/${repo}/languages`);
|
|
217
|
+
const total = Object.values(data).reduce((a, b) => a + b, 0) || 1;
|
|
218
|
+
return Object.entries(data)
|
|
219
|
+
.map(([lang, bytes]) => `${lang}: ${((bytes / total) * 100).toFixed(1)}%`)
|
|
220
|
+
.join(', ');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get recent commits for a repo (last N).
|
|
225
|
+
*/
|
|
226
|
+
export async function getRecentCommits(config, repo, maxResults = 10) {
|
|
227
|
+
const data = await ghFetch(config, `/repos/${repo}/commits?per_page=${maxResults}`);
|
|
228
|
+
if (!Array.isArray(data) || data.length === 0) return `No commits found in ${repo}.`;
|
|
229
|
+
return data.map((c, i) => {
|
|
230
|
+
const sha = c.sha ? c.sha.slice(0, 7) : '?';
|
|
231
|
+
const msg = (c.commit?.message || '').split('\n')[0].slice(0, 100);
|
|
232
|
+
const author = c.commit?.author?.name || c.author?.login || 'unknown';
|
|
233
|
+
const date = c.commit?.author?.date ? c.commit.author.date.slice(0, 10) : '';
|
|
234
|
+
return `${i + 1}. [${sha}] ${date} — ${author}: ${msg}`;
|
|
235
|
+
}).join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get README content for a repo.
|
|
240
|
+
*/
|
|
241
|
+
export async function getReadme(config, repo) {
|
|
242
|
+
try {
|
|
243
|
+
const data = await ghFetch(config, `/repos/${repo}/readme`);
|
|
244
|
+
if (data.content) {
|
|
245
|
+
const text = Buffer.from(data.content, 'base64').toString('utf-8');
|
|
246
|
+
return text.slice(0, 6000); // cap at 6KB
|
|
247
|
+
}
|
|
248
|
+
return '';
|
|
249
|
+
} catch {
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get contributors for a repo.
|
|
256
|
+
*/
|
|
257
|
+
export async function getContributors(config, repo, maxResults = 10) {
|
|
258
|
+
try {
|
|
259
|
+
const data = await ghFetch(config, `/repos/${repo}/contributors?per_page=${maxResults}`);
|
|
260
|
+
if (!Array.isArray(data) || data.length === 0) return 'No contributors data.';
|
|
261
|
+
return data.map((c, i) => `${i + 1}. @${c.login} — ${c.contributions} commits`).join('\n');
|
|
262
|
+
} catch {
|
|
263
|
+
return 'Contributors data unavailable.';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
188
267
|
export async function markNotificationsRead(config) {
|
|
189
268
|
await ghFetch(config, '/notifications', {
|
|
190
269
|
method: 'PUT',
|