agent-nuvira 1.75.2 → 1.76.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/README.md +2 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +25 -24
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/gateway.d.ts +1 -0
- package/dist/cli/gateway.d.ts.map +1 -1
- package/dist/cli/gateway.js +205 -7
- package/dist/cli/gateway.js.map +1 -1
- package/dist/gateway/contacts.d.ts +7 -0
- package/dist/gateway/contacts.d.ts.map +1 -1
- package/dist/gateway/contacts.js +18 -0
- package/dist/gateway/contacts.js.map +1 -1
- package/dist/gateway/registry.d.ts +7 -0
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +53 -0
- package/dist/gateway/registry.js.map +1 -1
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +185 -18
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +6 -0
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/web-dashboard/public/assets/{index-ej9RS97z.css → index-BMXmCrQF.css} +1 -1
- package/src/web-dashboard/public/assets/index-TbDN1H0u.js +178 -0
- package/src/web-dashboard/public/assets/index-TbDN1H0u.js.map +1 -0
- package/src/web-dashboard/public/index.html +2 -2
- package/src/web-dashboard/public/assets/index-BomwXhCt.js +0 -177
- package/src/web-dashboard/public/assets/index-BomwXhCt.js.map +0 -1
|
@@ -1854,7 +1854,7 @@ let chatConsole = new ChatConsole({ persistPath: join(MEMORY_DIR, 'chat-sessions
|
|
|
1854
1854
|
// the picker; both are in-memory (the workspace store records repo ids, not
|
|
1855
1855
|
// local paths — this is the dashboard's own recency).
|
|
1856
1856
|
const projectBundleCache = new Map();
|
|
1857
|
-
const recentProjects = new Set(
|
|
1857
|
+
const recentProjects = new Set();
|
|
1858
1858
|
/** Get (or build) the cached bundle for a project path; null when invalid. */
|
|
1859
1859
|
function getProjectBundle(path) {
|
|
1860
1860
|
try {
|
|
@@ -2654,8 +2654,10 @@ function handleRequest(req, res) {
|
|
|
2654
2654
|
// WhatsApp contacts are synced into the bridge contacts file for
|
|
2655
2655
|
// send-by-name parity with `buff whatsapp contact add <Name> <no>`.
|
|
2656
2656
|
let savedContacts;
|
|
2657
|
+
const contactErrors = [];
|
|
2657
2658
|
if (Array.isArray(body?.contacts)) {
|
|
2658
2659
|
const { writeGatewayContacts, syncWhatsAppContactName } = await import('../gateway/contacts.js');
|
|
2660
|
+
const { validateContactId } = await import('../gateway/contacts.js');
|
|
2659
2661
|
const valid = body.contacts
|
|
2660
2662
|
.filter((c) => !!c && typeof c === 'object' &&
|
|
2661
2663
|
typeof c.name === 'string' &&
|
|
@@ -2667,7 +2669,16 @@ function handleRequest(req, res) {
|
|
|
2667
2669
|
id: (c.id ?? '').trim(),
|
|
2668
2670
|
addedAt: typeof c.addedAt === 'number' ? c.addedAt : Date.now(),
|
|
2669
2671
|
}))
|
|
2670
|
-
.filter((c) =>
|
|
2672
|
+
.filter((c) => {
|
|
2673
|
+
if (!c.name || !c.id || !(c.platform in PLATFORM_ENV_VARS))
|
|
2674
|
+
return false;
|
|
2675
|
+
const err = validateContactId(c.platform, c.id);
|
|
2676
|
+
if (err) {
|
|
2677
|
+
contactErrors.push(`${c.name || c.id} (${c.platform}): ${err}`);
|
|
2678
|
+
return false;
|
|
2679
|
+
}
|
|
2680
|
+
return true;
|
|
2681
|
+
});
|
|
2671
2682
|
savedContacts = valid;
|
|
2672
2683
|
writeGatewayContacts(savedContacts);
|
|
2673
2684
|
// Send-by-name parity: named whatsapp contacts land in the bridge
|
|
@@ -2677,7 +2688,13 @@ function handleRequest(req, res) {
|
|
|
2677
2688
|
syncWhatsAppContactName(c.name, c.id);
|
|
2678
2689
|
}
|
|
2679
2690
|
}
|
|
2680
|
-
writeJson(res, 200, {
|
|
2691
|
+
writeJson(res, 200, {
|
|
2692
|
+
ok: true,
|
|
2693
|
+
policies: merged,
|
|
2694
|
+
statusRecipients: gatewayPatch.statusRecipients ?? [],
|
|
2695
|
+
contacts: savedContacts,
|
|
2696
|
+
...(contactErrors.length > 0 ? { contactErrors } : {}),
|
|
2697
|
+
});
|
|
2681
2698
|
return;
|
|
2682
2699
|
}
|
|
2683
2700
|
writeJson(res, 405, { ok: false, error: 'Method not allowed — use GET or PUT.' });
|
|
@@ -3362,9 +3379,34 @@ function handleRequest(req, res) {
|
|
|
3362
3379
|
return;
|
|
3363
3380
|
}
|
|
3364
3381
|
const canWrite = roleCan(session.role, 'routing.operate');
|
|
3382
|
+
// Per-platform setup documentation links and one-line hints.
|
|
3383
|
+
const SETUP_META = {
|
|
3384
|
+
telegram: { url: 'https://core.telegram.org/bots#how-do-i-create-a-bot', hint: 'Create a bot via @BotFather on Telegram, copy the token, paste it here, then run `buff gateway start`. Note: contacts need the Telegram chat ID (numeric), not a phone number — message the bot first to get the ID.' },
|
|
3385
|
+
discord: { url: 'https://discord.com/developers/applications', hint: 'Create an application + bot in the Discord Developer Portal, copy the bot token, paste it here, then run `buff gateway start`.' },
|
|
3386
|
+
slack: { url: 'https://api.slack.com/apps', hint: 'Create a Slack app with Bot Token Scopes, install to workspace, copy the Bot Token, paste it here, then run `buff gateway start`.' },
|
|
3387
|
+
whatsapp_cloud: { url: 'https://developers.facebook.com/docs/whatsapp/cloud-api/get-started', hint: 'Set up a Meta Cloud API phone number, paste the token + Phone ID here, then run `buff gateway start`.' },
|
|
3388
|
+
email: { url: '', hint: 'Enter your SMTP relay host and auth credentials. The agent replies to incoming emails.' },
|
|
3389
|
+
signal: { url: 'https://bbernhard.github.io/signal-cli-rest-api/', hint: 'Run signal-cli-rest-api, register an account, paste the account number here, then run `buff gateway start`.' },
|
|
3390
|
+
dingtalk: { url: 'https://open.dingtalk.com/', hint: 'Create a custom robot in a DingTalk group, copy the webhook URL, paste it here, then run `buff gateway start`.' },
|
|
3391
|
+
feishu: { url: 'https://open.feishu.cn/', hint: 'Create a bot in Feishu, copy the webhook URL, paste it here, then run `buff gateway start`.' },
|
|
3392
|
+
wecom: { url: 'https://open.work.weixin.qq.com/', hint: 'Create a group bot in WeCom, copy the webhook URL, paste it here, then run `buff gateway start`.' },
|
|
3393
|
+
mattermost: { url: 'https://developers.mattermost.com/', hint: 'Create an incoming webhook in Mattermost, copy the URL, paste it here, then run `buff gateway start`.' },
|
|
3394
|
+
matrix: { url: 'https://spec.matrix.org/', hint: 'Enter your Matrix homeserver URL and an access token (from a bot user), then run `buff gateway start`.' },
|
|
3395
|
+
webhook: { url: '', hint: 'Enter a generic webhook URL. The agent sends outbound webhooks to this URL.' },
|
|
3396
|
+
bluebubbles: { url: 'https://docs.bluebubbles.io/', hint: 'Set up BlueBubbles server, paste the URL + password here, then run `buff gateway start`.' },
|
|
3397
|
+
ntfy: { url: 'https://ntfy.sh/', hint: 'Enter an ntfy topic. The agent publishes to this topic (default server: ntfy.sh).' },
|
|
3398
|
+
teams: { url: 'https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook', hint: 'Create an incoming webhook in Microsoft Teams, paste the URL here, then run `buff gateway start`.' },
|
|
3399
|
+
google_chat: { url: 'https://developers.google.com/workspace/chat/quickstart/webhooks', hint: 'Create a webhook in a Google Chat space, paste the URL here, then run `buff gateway start`.' },
|
|
3400
|
+
weixin: { url: '', hint: 'Enter your Weixin iLink bot token, then run `buff gateway start`.' },
|
|
3401
|
+
sms: { url: 'https://www.twilio.com/docs/messaging/quickstart/node', hint: 'Enter your Twilio Account SID, Auth Token, and sender phone number, then run `buff gateway start`.' },
|
|
3402
|
+
irc: { url: '', hint: 'Enter the IRC server host and optional port/nickname/channel settings, then run `buff gateway start`.' },
|
|
3403
|
+
simplex: { url: '', hint: 'Run the local simplex-chat daemon (ws://127.0.0.1:5225), paste the WebSocket URL here, then run `buff gateway start`.' },
|
|
3404
|
+
homeassistant: { url: 'https://developers.home-assistant.io/docs/auth_api/', hint: 'Create a long-lived access token in Home Assistant, paste it here, then run `buff gateway start`.' },
|
|
3405
|
+
};
|
|
3365
3406
|
const platforms = configurablePlatforms().map((p) => {
|
|
3366
3407
|
const st = platformConfigStatus(p);
|
|
3367
3408
|
const meta = platformEnvVarMeta(p);
|
|
3409
|
+
const setup = SETUP_META[p];
|
|
3368
3410
|
return {
|
|
3369
3411
|
platform: p,
|
|
3370
3412
|
label: st.label,
|
|
@@ -3379,6 +3421,7 @@ function handleRequest(req, res) {
|
|
|
3379
3421
|
secret: m?.secret ?? false,
|
|
3380
3422
|
};
|
|
3381
3423
|
}),
|
|
3424
|
+
...(setup ? { setupUrl: setup.url, setupHint: setup.hint } : {}),
|
|
3382
3425
|
};
|
|
3383
3426
|
});
|
|
3384
3427
|
writeJson(res, 200, { ok: true, platforms });
|
|
@@ -3471,7 +3514,7 @@ function handleRequest(req, res) {
|
|
|
3471
3514
|
return;
|
|
3472
3515
|
}
|
|
3473
3516
|
const projects = [...recentProjects]
|
|
3474
|
-
.map((p) => ({ path: p, name: basename(p) || p, kind:
|
|
3517
|
+
.map((p) => ({ path: p, name: basename(p) || p, kind: 'recent' }))
|
|
3475
3518
|
.filter((p) => existsSync(p.path) && statSync(p.path).isDirectory());
|
|
3476
3519
|
writeJson(res, 200, { ok: true, projects });
|
|
3477
3520
|
return;
|
|
@@ -3516,9 +3559,8 @@ function handleRequest(req, res) {
|
|
|
3516
3559
|
})();
|
|
3517
3560
|
return;
|
|
3518
3561
|
}
|
|
3519
|
-
// GET /api/browse?path=<dir
|
|
3520
|
-
// frontend folder browser (project picker).
|
|
3521
|
-
// indicating whether the path itself can be attached as a project.
|
|
3562
|
+
// GET /api/browse?path=<dir>&showDrives=1 — list subdirectories for the
|
|
3563
|
+
// frontend folder browser (project picker). showDrives=1 returns drive roots.
|
|
3522
3564
|
if (pathname === '/api/browse' && req.method === 'GET') {
|
|
3523
3565
|
const session = adminSessions.validate(bearerToken(req));
|
|
3524
3566
|
if (!session) {
|
|
@@ -3529,22 +3571,68 @@ function handleRequest(req, res) {
|
|
|
3529
3571
|
writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot browse directories.` });
|
|
3530
3572
|
return;
|
|
3531
3573
|
}
|
|
3532
|
-
// Extract ?path= from the URL query string.
|
|
3533
3574
|
const urlObj = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
3534
3575
|
const rawPath = urlObj.searchParams.get('path') || '';
|
|
3576
|
+
const showDrives = urlObj.searchParams.get('showDrives') === '1';
|
|
3535
3577
|
const dirPath = rawPath.trim();
|
|
3578
|
+
// Collect drive roots for the platform.
|
|
3579
|
+
const homeDir = homedir();
|
|
3580
|
+
const collectDrives = () => {
|
|
3581
|
+
const drives = [];
|
|
3582
|
+
const plat = process.platform;
|
|
3583
|
+
if (plat === 'win32') {
|
|
3584
|
+
drives.push({ name: '🏠 Home', path: homeDir, type: 'home' });
|
|
3585
|
+
for (const ch of 'CDEFGH'.split('')) {
|
|
3586
|
+
const dp = `${ch}:\\`;
|
|
3587
|
+
try {
|
|
3588
|
+
if (existsSync(dp))
|
|
3589
|
+
drives.push({ name: `${ch}:`, path: dp, type: 'drive' });
|
|
3590
|
+
}
|
|
3591
|
+
catch { /* skip */ }
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
else if (plat === 'darwin') {
|
|
3595
|
+
drives.push({ name: '🏠 Home', path: homeDir, type: 'home' });
|
|
3596
|
+
try {
|
|
3597
|
+
const vols = readdirSync('/Volumes', { withFileTypes: true })
|
|
3598
|
+
.filter((e) => e.isDirectory())
|
|
3599
|
+
.map((e) => ({ name: `💾 ${e.name}`, path: `/Volumes/${e.name}`, type: 'drive' }));
|
|
3600
|
+
drives.push(...vols);
|
|
3601
|
+
}
|
|
3602
|
+
catch { /* /Volumes may not exist */ }
|
|
3603
|
+
drives.push({ name: ' Root /', path: '/', type: 'system' });
|
|
3604
|
+
}
|
|
3605
|
+
else {
|
|
3606
|
+
drives.push({ name: '🏠 Home', path: homeDir, type: 'home' });
|
|
3607
|
+
drives.push({ name: ' Root /', path: '/', type: 'system' });
|
|
3608
|
+
}
|
|
3609
|
+
return drives;
|
|
3610
|
+
};
|
|
3611
|
+
// No path → show home directory contents + drive bar.
|
|
3536
3612
|
if (!dirPath) {
|
|
3537
|
-
// No path given — start from the user's home directory.
|
|
3538
|
-
const homeDir = homedir();
|
|
3539
3613
|
try {
|
|
3540
3614
|
const entries = readdirSync(homeDir, { withFileTypes: true })
|
|
3541
|
-
.filter((e) => e.isDirectory())
|
|
3542
|
-
.map((e) =>
|
|
3543
|
-
|
|
3544
|
-
|
|
3615
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
3616
|
+
.map((e) => {
|
|
3617
|
+
const fp = join(homeDir, e.name);
|
|
3618
|
+
let modified = 0;
|
|
3619
|
+
try {
|
|
3620
|
+
modified = statSync(fp).mtimeMs;
|
|
3621
|
+
}
|
|
3622
|
+
catch { /* skip */ }
|
|
3623
|
+
return { name: e.name, path: fp, isDir: true, modified };
|
|
3624
|
+
})
|
|
3625
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
3626
|
+
.slice(0, 100);
|
|
3627
|
+
writeJson(res, 200, {
|
|
3628
|
+
ok: true, path: homeDir, entries, parent: null,
|
|
3629
|
+
isProject: existsSync(homeDir) && statSync(homeDir).isDirectory(),
|
|
3630
|
+
drives: collectDrives(),
|
|
3631
|
+
breadcrumbs: [{ name: 'Home', path: homeDir }],
|
|
3632
|
+
});
|
|
3545
3633
|
}
|
|
3546
3634
|
catch {
|
|
3547
|
-
writeJson(res, 200, { ok: true, path: homeDir, entries: [], parent: null, isProject: false });
|
|
3635
|
+
writeJson(res, 200, { ok: true, path: homeDir, entries: [], parent: null, isProject: false, drives: collectDrives(), breadcrumbs: [] });
|
|
3548
3636
|
}
|
|
3549
3637
|
return;
|
|
3550
3638
|
}
|
|
@@ -3556,17 +3644,96 @@ function handleRequest(req, res) {
|
|
|
3556
3644
|
try {
|
|
3557
3645
|
const entries = readdirSync(target, { withFileTypes: true })
|
|
3558
3646
|
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
3559
|
-
.map((e) =>
|
|
3647
|
+
.map((e) => {
|
|
3648
|
+
const fp = join(target, e.name);
|
|
3649
|
+
let modified = 0;
|
|
3650
|
+
try {
|
|
3651
|
+
modified = statSync(fp).mtimeMs;
|
|
3652
|
+
}
|
|
3653
|
+
catch { /* skip */ }
|
|
3654
|
+
return { name: e.name, path: fp, isDir: true, modified };
|
|
3655
|
+
})
|
|
3560
3656
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
3561
3657
|
.slice(0, 100);
|
|
3562
3658
|
const parent = dirname(target);
|
|
3563
|
-
|
|
3659
|
+
const parts = target.split(/[\/]/).filter(Boolean);
|
|
3660
|
+
const breadcrumbs = [];
|
|
3661
|
+
let cum = target.startsWith('/') ? '/' : '';
|
|
3662
|
+
for (const part of parts) {
|
|
3663
|
+
cum = cum === '/' ? `/${part}` : `${cum}/${part}`;
|
|
3664
|
+
breadcrumbs.push({ name: part, path: cum });
|
|
3665
|
+
}
|
|
3666
|
+
writeJson(res, 200, {
|
|
3667
|
+
ok: true, path: target, entries,
|
|
3668
|
+
parent: parent !== target ? parent : null,
|
|
3669
|
+
isProject: true, breadcrumbs,
|
|
3670
|
+
});
|
|
3564
3671
|
}
|
|
3565
3672
|
catch {
|
|
3566
|
-
writeJson(res, 200, { ok: true, path: target, entries: [], parent: dirname(target), isProject: true });
|
|
3673
|
+
writeJson(res, 200, { ok: true, path: target, entries: [], parent: dirname(target), isProject: true, breadcrumbs: [] });
|
|
3567
3674
|
}
|
|
3568
3675
|
return;
|
|
3569
3676
|
}
|
|
3677
|
+
// POST /api/config/platforms/:platform/verify — verify a platform's token
|
|
3678
|
+
// by calling the platform's API (e.g. Telegram getMe, Discord /users/@me).
|
|
3679
|
+
const verifyMatch = /^\/api\/config\/platforms\/([^/]+)\/verify$/.exec(pathname);
|
|
3680
|
+
if (verifyMatch && req.method === 'POST') {
|
|
3681
|
+
void (async () => {
|
|
3682
|
+
const session = adminSessions.validate(bearerToken(req));
|
|
3683
|
+
if (!session) {
|
|
3684
|
+
writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (!roleCan(session.role, 'routing.operate')) {
|
|
3688
|
+
writeJson(res, 403, { ok: false, error: 'Access denied.' });
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
const platform = verifyMatch[1];
|
|
3692
|
+
const body = await readJsonBody(req);
|
|
3693
|
+
const values = (body?.values ?? {});
|
|
3694
|
+
try {
|
|
3695
|
+
let result;
|
|
3696
|
+
if (platform === 'telegram') {
|
|
3697
|
+
const token = values.BUFF_TELEGRAM_TOKEN ?? '';
|
|
3698
|
+
if (!token) {
|
|
3699
|
+
writeJson(res, 400, { ok: false, error: 'No token provided.' });
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
const r = await fetch(`https://api.telegram.org/bot${token}/getMe`, { signal: AbortSignal.timeout(10_000) });
|
|
3703
|
+
const d = await r.json();
|
|
3704
|
+
result = d.ok && d.result ? { ok: true, info: `@${d.result.username} (${d.result.first_name})` } : { ok: false, error: `Telegram rejected the token: ${JSON.stringify(d)}` };
|
|
3705
|
+
}
|
|
3706
|
+
else if (platform === 'discord') {
|
|
3707
|
+
const token = values.BUFF_DISCORD_BOT_TOKEN ?? '';
|
|
3708
|
+
if (!token) {
|
|
3709
|
+
writeJson(res, 400, { ok: false, error: 'No token provided.' });
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const r = await fetch('https://discord.com/api/v10/users/@me', { headers: { Authorization: `Bot ${token}` }, signal: AbortSignal.timeout(10_000) });
|
|
3713
|
+
const d = await r.json();
|
|
3714
|
+
result = d.username ? { ok: true, info: `@${d.username} (${d.id})` } : { ok: false, error: `Discord rejected the token: ${JSON.stringify(d)}` };
|
|
3715
|
+
}
|
|
3716
|
+
else if (platform === 'slack') {
|
|
3717
|
+
const token = values.BUFF_SLACK_BOT_TOKEN ?? '';
|
|
3718
|
+
if (!token) {
|
|
3719
|
+
writeJson(res, 400, { ok: false, error: 'No token provided.' });
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
const r = await fetch('https://slack.com/api/auth.test', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(10_000) });
|
|
3723
|
+
const d = await r.json();
|
|
3724
|
+
result = d.ok ? { ok: true, info: `${d.user} @ ${d.team}` } : { ok: false, error: `Slack rejected the token: ${JSON.stringify(d)}` };
|
|
3725
|
+
}
|
|
3726
|
+
else {
|
|
3727
|
+
result = { ok: false, error: `Verification not available for '${platform}' — save the token and test via the Gateway tab.` };
|
|
3728
|
+
}
|
|
3729
|
+
writeJson(res, 200, result);
|
|
3730
|
+
}
|
|
3731
|
+
catch (err) {
|
|
3732
|
+
writeJson(res, 200, { ok: false, error: `Verification failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3733
|
+
}
|
|
3734
|
+
})();
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3570
3737
|
// GET /api/sessions — P4 session sidebar: past conversations (most recent
|
|
3571
3738
|
// first) with title/preview/turnCount. Same auth as /api/chat (the sidebar
|
|
3572
3739
|
// is part of the chat surface).
|