@@ -1,3 +1,4 @@
import fs from "node:fs" ;
import type { ClawdbotConfig } from "../../config/config.js" ;
import {
listDiscordAccountIds ,
@@ -35,10 +36,36 @@ import { formatAge } from "./format.js";
export type ProviderRow = {
provider : string ;
enabled : boolean ;
configured : boolean ;
state : "ok" | "setup" | "warn" | "off" ;
detail : string ;
} ;
function summarizeSources ( sources : Array < string | undefined > ) : {
label : string ;
parts : string [ ] ;
} {
const counts = new Map < string , number > ( ) ;
for ( const s of sources ) {
const key = s ? . trim ( ) ? s . trim ( ) : "unknown" ;
counts . set ( key , ( counts . get ( key ) ? ? 0 ) + 1 ) ;
}
const parts = [ . . . counts . entries ( ) ]
. sort ( ( a , b ) = > b [ 1 ] - a [ 1 ] )
. map ( ( [ key , n ] ) = > ` ${ key } ${ n > 1 ? ` × ${ n } ` : "" } ` ) ;
const label = parts . length > 0 ? parts . join ( "+" ) : "unknown" ;
return { label , parts } ;
}
function existsSyncMaybe ( p : string | undefined ) : boolean | null {
const path = p ? . trim ( ) || "" ;
if ( ! path ) return null ;
try {
return fs . existsSync ( path ) ;
} catch {
return null ;
}
}
export async function buildProvidersTable ( cfg : ClawdbotConfig ) : Promise < {
rows : ProviderRow [ ] ;
details : Array < {
@@ -67,11 +94,11 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
rows . push ( {
provider : "WhatsApp" ,
enabled : waEnabled ,
configured : waLinked ,
state : ! waEnabled ? "off" : waLinked ? "ok" : "setup" ,
detail : waEnabled
? waLinked
? ` linked ${ waSelf ? ` ${ waSelf } ` : "" } ${ waAuthAgeMs ? ` · auth ${ formatAge ( waAuthAgeMs ) } ` : "" } · accounts ${ waAccounts . length || 1 } `
: "not linked"
: "not linked (run clawdbot login) "
: "disabled" ,
} ) ;
if ( waLinked ) {
@@ -96,7 +123,7 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
Account : account.name?.trim ( )
? ` ${ account . accountId } ( ${ account . name . trim ( ) } ) `
: account . accountId ,
Status : account.enabled ? "OK" : "WARN " ,
Status : account.enabled ? "OK" : "OFF " ,
Notes : notes.join ( " · " ) ,
} ;
} ) ,
@@ -108,15 +135,43 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
const tgAccounts = listTelegramAccountIds ( cfg ) . map ( ( accountId ) = >
resolveTelegramAccount ( { cfg , accountId } ) ,
) ;
const tgConfigured = tgAccounts . some ( ( a ) = > Boolean ( a . token ? . trim ( ) ) ) ;
const tgEnabledAccounts = tgAccounts . filter ( ( a ) = > a . enabled ) ;
const tgTokenAccounts = tgEnabledAccounts . filter ( ( a ) = > a . token ? . trim ( ) ) ;
const tgSources = summarizeSources ( tgTokenAccounts . map ( ( a ) = > a . tokenSource ) ) ;
const tgMissingFiles : string [ ] = [ ] ;
const tgGlobalTokenFileExists = existsSyncMaybe ( cfg . telegram ? . tokenFile ) ;
if (
tgEnabled &&
cfg . telegram ? . tokenFile ? . trim ( ) &&
tgGlobalTokenFileExists === false
) {
tgMissingFiles . push ( "telegram.tokenFile" ) ;
}
for ( const accountId of listTelegramAccountIds ( cfg ) ) {
const tokenFile =
cfg . telegram ? . accounts ? . [ accountId ] ? . tokenFile ? . trim ( ) || "" ;
const ok = existsSyncMaybe ( tokenFile ) ;
if ( tgEnabled && tokenFile && ok === false ) {
tgMissingFiles . push ( ` telegram.accounts. ${ accountId } .tokenFile ` ) ;
}
}
const tgMisconfigured = tgMissingFiles . length > 0 ;
rows . push ( {
provider : "Telegram" ,
enabled : tgEnabled ,
configured : tgEnabled && tgConfigured ,
state : ! tgEnabled
? "off"
: tgMisconfigured
? "warn"
: tgTokenAccounts . length > 0
? "ok"
: "setup" ,
detail : tgEnabled
? tgC onfigured
? ` accounts ${ tgAccounts . filter ( ( a ) = > a . token ? . trim ( ) ) . length } `
: "not configured"
? tgMisc onfigured
? ` token file missing ( ${ tgMissingFiles [ 0 ] } ) `
: tgTokenAccounts . length > 0
? ` bot token ${ tgSources . label } · accounts ${ tgTokenAccounts . length } / ${ tgEnabledAccounts . length || 1 } `
: "no bot token (TELEGRAM_BOT_TOKEN / telegram.botToken)"
: "disabled" ,
} ) ;
@@ -125,15 +180,17 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
const dcAccounts = listDiscordAccountIds ( cfg ) . map ( ( accountId ) = >
resolveDiscordAccount ( { cfg , accountId } ) ,
) ;
const dcConfigured = dcAccounts . some ( ( a ) = > Boolean ( a . token ? . trim ( ) ) ) ;
const dcEnabledAccounts = dcAccounts . filter ( ( a ) = > a . enabled ) ;
const dcTokenAccounts = dcEnabledAccounts . filter ( ( a ) = > a . token ? . trim ( ) ) ;
const dcSources = summarizeSources ( dcTokenAccounts . map ( ( a ) = > a . tokenSource ) ) ;
rows . push ( {
provider : "Discord" ,
enabled : dcEnabled ,
configured : dcEnabled && dcConfigured ,
state : ! dcEnabled ? "off" : dcTokenAccounts . length > 0 ? "ok" : "setup" ,
detail : dcEnabled
? dcConfigured
? ` accounts ${ dcAccounts . filter ( ( a ) = > a . token ? . trim ( ) ) . length } `
: "not configured "
? dcTokenAccounts . length > 0
? ` bot token ${ dcSources . label } · accounts ${ dcTokenAccounts . length } / ${ dcEnabledAccounts . length || 1 } `
: "no bot token (DISCORD_BOT_TOKEN / discord.token) "
: "disabled" ,
} ) ;
@@ -142,17 +199,42 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
const slAccounts = listSlackAccountIds ( cfg ) . map ( ( accountId ) = >
resolveSlackAccount ( { cfg , accountId } ) ,
) ;
const slConfigured = slAccounts . some (
const slEnabledAccounts = slAccounts . filter ( ( a ) = > a . enabled ) ;
const slReady = slEnabledAccounts . filter (
( a ) = > Boolean ( a . botToken ? . trim ( ) ) && Boolean ( a . appToken ? . trim ( ) ) ,
) ;
const slPartial = slEnabledAccounts . filter (
( a ) = >
( a . botToken ? . trim ( ) && ! a . appToken ? . trim ( ) ) ||
( ! a . botToken ? . trim ( ) && a . appToken ? . trim ( ) ) ,
) ;
const slHasAnyToken = slEnabledAccounts . some (
( a ) = > Boolean ( a . botToken ? . trim ( ) ) || Boolean ( a . appToken ? . trim ( ) ) ,
) ;
const slBotSources = summarizeSources (
slReady . map ( ( a ) = > a . botTokenSource ? ? "none" ) ,
) ;
const slAppSources = summarizeSources (
slReady . map ( ( a ) = > a . appTokenSource ? ? "none" ) ,
) ;
rows . push ( {
provider : "Slack" ,
enabled : slEnabled ,
configured : slEnabled && slConfigured ,
state : ! slEnabled
? "off"
: slPartial . length > 0
? "warn"
: slReady . length > 0
? "ok"
: "setup" ,
detail : slEnabled
? slConfigured
? ` accounts ${ slAccounts . filter ( ( a ) = > a . botToken ? . trim ( ) && a . appToken ? . trim ( ) ) . length } `
: "not configured"
? slPartial . length > 0
? ` partial tokens (need bot+app) · accounts ${ slPartial . length } `
: slReady . length > 0
? ` tokens ok (bot ${ slBotSources . label } , app ${ slAppSources . label } ) · accounts ${ slReady . length } / ${ slEnabledAccounts . length || 1 } `
: slHasAnyToken
? "tokens incomplete (need bot+app)"
: "no tokens (SLACK_BOT_TOKEN + SLACK_APP_TOKEN)"
: "disabled" ,
} ) ;
@@ -161,15 +243,20 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
const siAccounts = listSignalAccountIds ( cfg ) . map ( ( accountId ) = >
resolveSignalAccount ( { cfg , accountId } ) ,
) ;
const siConfigured = siAccounts . some ( ( a ) = > a . configur ed) ;
const siEnabledAccounts = siAccounts . filter ( ( a ) = > a . enabl ed) ;
const siConfiguredAccounts = siEnabledAccounts . filter ( ( a ) = > a . configured ) ;
rows . push ( {
provider : "Signal" ,
enabled : siEnabled ,
configured : siEnabled && siConfigured ,
state : ! siEnabled
? "off"
: siConfiguredAccounts . length > 0
? "ok"
: "setup" ,
detail : siEnabled
? siConfigured
? ` accounts ${ siAccounts . filter ( ( a ) = > a . configured ) . length } `
: "no t configured "
? siConfiguredAccounts . length > 0
? ` configured · accounts ${ siConfigured Accounts . length } / ${ siEnabledAccounts . length || 1 } `
: "defaul t config (no overrides) "
: "disabled" ,
} ) ;
@@ -178,29 +265,55 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{
const imAccounts = listIMessageAccountIds ( cfg ) . map ( ( accountId ) = >
resolveIMessageAccount ( { cfg , accountId } ) ,
) ;
const imConfigured = imAccounts . some ( ( a ) = > a . configur ed) ;
const imEnabledAccounts = imAccounts . filter ( ( a ) = > a . enabl ed) ;
const imConfiguredAccounts = imEnabledAccounts . filter ( ( a ) = > a . configured ) ;
rows . push ( {
provider : "iMessage" ,
enabled : imEnabled ,
configured : imEnabled && imConfigured ,
state : ! imEnabled
? "off"
: imConfiguredAccounts . length > 0
? "ok"
: "setup" ,
detail : imEnabled
? imConfigured
? ` accounts ${ imAccounts . length } `
: "no t configured "
? imConfiguredAccounts . length > 0
? ` configured · accounts ${ imConfigured Accounts . length } / ${ imEnabledAccounts . length || 1 } `
: "defaul t config (no overrides) "
: "disabled" ,
} ) ;
// MS Teams
const msEnabled = cfg . msteams ? . enabled !== false ;
const msConfigu red = Boolean ( resolveMSTeamsCredentials ( cfg . msteams ) ) ;
const msCreds = resolveMSTeamsCredentials ( cfg . msteams ) ;
const msAppId =
cfg . msteams ? . appId ? . trim ( ) || process . env . MSTEAMS_APP_ID ? . trim ( ) ;
const msAppPassword =
cfg . msteams ? . appPassword ? . trim ( ) ||
process . env . MSTEAMS_APP_PASSWORD ? . trim ( ) ;
const msTenantId =
cfg . msteams ? . tenantId ? . trim ( ) || process . env . MSTEAMS_TENANT_ID ? . trim ( ) ;
const msMissing = [
! msAppId ? "appId" : null ,
! msAppPassword ? "appPassword" : null ,
! msTenantId ? "tenantId" : null ,
] . filter ( Boolean ) as string [ ] ;
const msAnyPresent = Boolean ( msAppId || msAppPassword || msTenantId ) ;
rows . push ( {
provider : "MS Teams" ,
enabled : msEnabled ,
configured : msEnabled && msConfigured ,
state : ! msEnabled
? "off"
: msCreds
? "ok"
: msAnyPresent
? "warn"
: "setup" ,
detail : msEnabled
? msConfigu red
? "credentials pre sen t"
: "not configured"
? msCreds
? "credentials set"
: msAnyPresent
? ` credentials incomplete (missing ${ msMissing . join ( ", " ) } ) `
: "no credentials (MSTEAMS_APP_ID / _PASSWORD / _TENANT_ID)"
: "disabled" ,
} ) ;