Free high quality AI (agentic) from Harbour apps 🚀

108 views
Skip to first unread message

antonio....@gmail.com

unread,
Sep 30, 2026, 12:36:27 AM (24 hours ago) Sep 30
to Harbour Users
Dear Harbour friends,

I would like to share TInstinct, a small Harbour class based on Instinct. It lets an application exchange messages by email with the user's own Instinct assistant. Instinct is the assistant behind this approach; TInstinct is the Harbour-side transport class, not a separate AI model.

If you would like to try Instinct, the invitation link is:

The idea is to bring high-quality AI and access to agents into our Harbour applications without adding a separate model API integration. My aim is to make this possible at no additional AI API cost, using the user's own assistant. This prototype does not establish Instinct's pricing, account availability or service limits; those depend on the user's account and current service terms.

The class is neutral: there is no hard-coded assistant address, and each user configures their own sender, shared token and destination. It uses Harbour and hbtip, not FiveWin. Console programs and applications with other interfaces can use the same class.

This is a transport prototype, not an official Instinct API or a ready-made agent execution system. Before using it, arrange with your own assistant how requests will be handled and how JSON replies containing "id" and "text" will be returned. Sending an email does not by itself authorize the assistant to act on your behalf.

I have included the usage notes and source code in this message, rather than a ZIP.

1. Three configuration values
-----------------------------

- cFrom: your own email address, from which the application sends requests. Use a mailbox you control and arrange the permitted communication scope with your assistant through your normal trusted conversation.
- cToken: a shared secret. Choose your own value and exchange it securely. Do not publish a real token in source code, documentation or this group. The token alone does not prove identity or authorize actions.
- cTo: the email address of your own Instinct. Ask your assistant for its email address. There is no default destination.

Replace TU_EMAIL, TU_TOKEN and EMAIL_DE_TU_INSTINCT in the examples. The constructor order is always:

```harbour
TInstinct():New( cFrom, cToken, cTo )
```

2. A local SMTP/POP relay is required
------------------------------------

The class connects only to a local relay. The default cHost is "127.0.0.1", with nSmtpPort = 25 and nPopPort = 110. Change the ports to match your relay.

Set cSmtpUser/cSmtpPassword if the relay requires SMTP authentication, and cPopUser/cPopPassword to read replies. The relay must protect the external connection with TLS and validate certificates. TInstinct does not provide that external TLS connection, and a direct unencrypted remote SMTP/POP connection is not supported.

3. Minimal example: send and wait
---------------------------------

Save this example as test.prg:

```harbour
PROCEDURE Main()
   LOCAL oInstinct := TInstinct():New( "TU_EMAIL", "TU_TOKEN", ;
                                       "EMAIL_DE_TU_INSTINCT" )
   LOCAL cRespuesta

   IF ! oInstinct:Send( "Hola, Instinct" )
      ? oInstinct:cError
      RETURN
   ENDIF

   cRespuesta := oInstinct:Answer() // Waits up to 30 seconds.
   IF cRespuesta == NIL
      ? oInstinct:cError
   ELSE
      ? cRespuesta
   ENDIF
RETURN
```

Send() returns .T. when the relay accepts the email, not when Instinct has received it. The request identifier is stored in cId.

Answer() waits for the reply to that request. Use Answer( 60 ) to wait up to 60 seconds. It returns the reply text, or NIL on failure/timeout. Do not blindly resend after an uncertain send result: the relay may already have accepted the message.

4. Callback and timer-based polling
-----------------------------------

For an application with a user interface, register a callback with OnAnswer() and call Pump() from its timer or idle loop.

Save this second example as test_async.prg. Its loop calls Pump() once a second for up to 30 seconds:

```harbour
PROCEDURE Main()
   LOCAL oInstinct := TInstinct():New( "TU_EMAIL", "TU_TOKEN", ;
                                       "EMAIL_DE_TU_INSTINCT" )
   LOCAL lRespondido := .F.
   LOCAL nInicio, nSiguiente

   oInstinct:OnAnswer( {| cTexto, cId | ;
      MostrarRespuesta( cTexto, cId ), lRespondido := .T. } )

   IF ! oInstinct:Send( "Resume mis tareas pendientes" )
      ? oInstinct:cError
      RETURN
   ENDIF

   nInicio := hb_MilliSeconds()
   nSiguiente := nInicio
   DO WHILE ! lRespondido .AND. hb_MilliSeconds() - nInicio < 30000
      IF hb_MilliSeconds() >= nSiguiente
         oInstinct:Pump()
         IF ! Empty( oInstinct:cError )
            ? oInstinct:cError
            EXIT
         ENDIF
         nSiguiente := hb_MilliSeconds() + 1000
      ENDIF
      hb_IdleSleep( 0.05 )
   ENDDO
RETURN

FUNCTION MostrarRespuesta( cTexto, cId )
   ? cId, cTexto
RETURN NIL
```

OnAnswer() receives the text and identifier of each new reply. Pump() does not wait for a reply to arrive, but the POP connection and polling operation can still take time. It is polling, not a background worker thread.

5. Build, errors and log
-----------------------

Compile each example separately with Harbour and hbtip:

```sh
hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
hbmk2 test_async.prg TInstinct.prg hbtip.hbc -gtstd
```

JSON support belongs to the Harbour runtime; no hbjson.hbc dependency is needed.

- cError contains the last error. Check it immediately after the operation.
- cLog records sends, polls, replies and errors.
- SaveLog( "miapp.log" ) saves the log and returns .T. or .F. Without an argument it uses tinstinct.log.

The class handles JSON, simple text/plain MIME replies and persistent POP UIDL deduplication. It does not delete mail from the reply mailbox. Use a dedicated mailbox and archive processed messages. Parsing a JSON reply is not sender authentication, and reply text should not be treated as automatic permission to execute actions.

Mailbox and pending replies
--------------------------

Use a dedicated POP mailbox and only one instance of the class per mailbox. PollOnce( nLimit ) processes up to nLimit new messages per call (20 by default, range 1 to 100), skipping UIDLs already processed even if they occupy the beginning of the mailbox. Subsequent calls reach the rest. UIDL state is saved in cUidlFile; no mail is deleted from the server. Archive processed mail periodically to avoid scanning an ever-growing mailbox.

If Answer() waits for ID A and receives ID B first, it preserves B in aPending. A later Answer( seconds, "B" ) can collect it; Pump() also delivers pending replies to the callback. Pump() without a callback does not consume mail. This queue lives in the instance, not on disk: consume it before closing the application. UIDL persistence prevents duplicate processing, but does not provide a durable response queue across restarts.

The complete corrected class and a standalone Harbour demo appear below. These are the locally validated sources; their addition to FWH is pending.

I would appreciate your feedback, especially test results, problems you find and suggestions for technical improvements.

Best regards,
Antonio Linares


SOURCE CODE
===========

TInstinct.prg
-------------

```harbour
/* TInstinct: prototipo de transporte, NO una API oficial de Instinct.
 *
 * Cada usuario habla con SU propio Instinct: configura el destino con la
 * direccion del buzon de tu asistente (preguntale a tu Instinct por ella)
 * y usa tu propio token compartido. Sin destino configurado, Send() falla.
 * La libreria es neutral: no lleva la direccion de ningun usuario concreto.
 *
 * La clase absorbe toda la fontaneria (JSON, MIME, SMTP, POP, deduplicacion
 * UIDL, espera de respuesta y log) y el programa queda en tres pasos:
 *
 * oInstinct := TInstinct():New( "y...@miapp.es", "MI-TOKEN", ;
 * oInstinct:Send( "Hola Instinct" ) // .T./.F.; el id queda en oInstinct:cId
 * ? oInstinct:Answer() // espera y devuelve lo recibido
 *
 * En una app con interfaz no conviene bloquear: registra un bloque con
 * oInstinct:OnAnswer( {| cTexto, cId | ... } ) y llama a oInstinct:Pump()
 * desde un timer o el bucle idle; el bloque se dispara con cada respuesta.
 *
 * La clase guarda un log de todo lo ocurrido (envios, sondeos, respuestas y
 * errores recibidos del servidor) en oInstinct:cLog; oInstinct:SaveLog() lo
 * vuelca a disco y oInstinct:cError conserva el ultimo error.
 *
 * Compilar la demo: hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
 * Compilar la bateria: hbmk2 test_full.prg TInstinct.prg hbtip.hbc -gtstd
 * JSON pertenece al runtime de Harbour; no existe dependencia hbjson.hbc.
 * Solo acepta un relay local. El relay debe proteger el salto externo con TLS
 * y validar certificados. Nunca enviar tokens/passwords por SMTP/POP remoto
 * sin cifrar. El token del JSON NO demuestra identidad ni autoriza acciones.
 * El receptor y sus respuestas JSON necesitan un acuerdo/implementacion aparte.
 */
#include "hbclass.ch"
#ifdef __XHARBOUR__
#include "hbcompat.ch"
   #xcommand BEGIN SEQUENCE WITH <b> => TRY
   #xcommand RECOVER USING <e> => CATCH <e>
   #xcommand END SEQUENCE => END
   #xtranslate hb_eol() => ( Chr( 13 ) + Chr( 10 ) )
   #xtranslate hb_MemoRead( <f> ) => MemoRead( <f> )
#endif

CLASS TInstinct
   /* Configuracion: valores por defecto razonables; tocar solo lo necesario. */
   DATA cHost INIT "127.0.0.1" // relay local; jamas un host remoto
   DATA nSmtpPort INIT 25 // puerto SMTP del relay local
   DATA nPopPort INIT 110 // puerto POP3 del buzon de respuestas
   DATA cSmtpUser INIT "" // auth SMTP opcional (LOGIN)
   DATA cSmtpPassword INIT ""
   DATA cPopUser INIT "" // auth POP3 del buzon
   DATA cPopPassword INIT ""
   DATA cFrom INIT "" // remitente (quien envia)
   DATA cTo INIT "" // buzon del Instinct del usuario: obligatorio
   DATA cToken INIT "" // secreto compartido con su Instinct
   DATA cApp INIT "Harbour" // etiqueta de la aplicacion
   DATA nTimeout INIT 5000 // ms de espera de conexion
   DATA nMaxBytes INIT 1048576 // tope de tamano de un correo
   DATA nPollEvery INIT 500 // ms entre sondeos en Answer()
   DATA cUidlFile INIT "tinstinct.uidl" // deduplicacion persistente
   /* Estado y resultados. */
   DATA cId INIT "" // id del ultimo Send() aceptado
   DATA cSentAt INIT "" // sello UTC del ultimo Send()
   DATA cError INIT "" // ultimo error recibido
   DATA aWarnings INIT {} // correos no validos vistos al sondear
   DATA cLog INIT "" // log de actividad (ver SaveLog())
   DATA hSeen INIT { => } // UIDL ya tratados (en memoria)
   DATA aPending INIT {} // replies waiting for another Answer/Pump
   DATA bOnAnswer INIT NIL // bloque async: {| cTexto, cId | ... }
   DATA lSeenLoaded INIT .F. // hSeen ya cargado de disco
   METHOD New( cFrom, cToken, cTo ) CONSTRUCTOR
   METHOD Send( cText, cId ) // -> .T./.F.; el id queda en ::cId
   METHOD Answer( nSeconds, cId ) // -> texto recibido o NIL (espera)
   METHOD OnAnswer( bBlock ) // registra el bloque para Pump()
   METHOD Pump() // timer/idle: dispara bOnAnswer
   METHOD PollOnce( nLimit ) // -> array de respuestas NUEVAS o NIL
   METHOD ParseReply( cRaw, cExpectedId )
   METHOD LogAdd( cMsg ) // anota una linea en ::cLog
   METHOD SetError( cMsg ) // fija ::cError y lo anota en el log
   METHOD SaveLog( cFile ) // vuelca ::cLog a disco
   METHOD LoadSeen() // carga ::hSeen de ::cUidlFile
   METHOD SaveSeen() // guarda ::hSeen en ::cUidlFile
ENDCLASS

METHOD New( cFrom, cToken, cTo ) CLASS TInstinct
   ::cFrom := InstinctDefault( cFrom, "" )
   ::cToken := InstinctDefault( cToken, "" )
   ::cTo := InstinctDefault( cTo, "" )
   ::LogAdd( "TInstinct listo: from=" + ::cFrom + " to=" + ::cTo )
RETURN Self

METHOD Send( cText, cId ) CLASS TInstinct
   LOCAL oURL, oSMTP, oMail, hPayload, lOK := .F., oErr, cRaw
   ::cError := ""
   IF ! LocalHost( ::cHost )
      ::SetError( "Se requiere relay local; transporte remoto sin TLS prohibido" )
      RETURN .F.
   ENDIF
   IF Empty( ::cTo )
      ::SetError( "Falta el destino: configura cTo con la direccion de tu propio Instinct (pregunta a tu Instinct por ella)" )
      RETURN .F.
   ENDIF
   IF ! Mailbox( ::cFrom ) .OR. ! Mailbox( ::cTo ) .OR. ! HB_ISSTRING( cText )
      ::SetError( "Remitente, destino o texto no valido" )
      RETURN .F.
   ENDIF
   IF cId == NIL
      cId := Lower( hb_StrToHex( hb_randStr( 16 ) ) ) // ID automatico
   ENDIF
   IF ! SafeId( cId )
      ::SetError( "ID no valido (1-128 caracteres: letras, cifras, guion, punto, _)" )
      RETURN .F.
   ENDIF
   #ifdef __XHARBOUR__
   ::cSentAt := TI_UtcStamp()
   #else
   ::cSentAt := StrTran( hb_TSToStr( hb_DateTime() - hb_UTCOffset() / 86400 ), " ", "T" ) + "Z"
   #endif
   hPayload := { "app" => ::cApp, "id" => cId, "text" => cText, ;
                 "token" => ::cToken, "sent_at" => ::cSentAt }
   oMail := TIPMail():New( hb_jsonEncode( hPayload ), "base64" )
   oMail:SetHeader( "TInstinct " + cId, ::cFrom, ::cTo )
   oMail:hHeaders[ "Content-Type" ] := 'text/plain; charset="UTF-8"'
   cRaw := oMail:ToString()
   IF Len( cRaw ) > ::nMaxBytes
      ::SetError( "Mensaje demasiado grande" )
      RETURN .F.
   ENDIF
   /* Asignar campos evita meter passwords en URLs o trazas. */
   oURL := TUrl():New( "smtp://" + ::cHost )
   oURL:nPort := ::nSmtpPort
   BEGIN SEQUENCE WITH {| e | Break( e ) }
      oSMTP := TInstinctSMTP():New( oURL, .F. )
      oSMTP:nConnTimeout := ::nTimeout
      IF oSMTP:OpenSecure()
         lOK := .T.
         IF ! Empty( ::cSmtpUser )
            lOK := oSMTP:Auth( ::cSmtpUser, ::cSmtpPassword )
         ENDIF
         IF lOK
            lOK := oSMTP:Mail( ::cFrom )
         ENDIF
         IF lOK
            lOK := oSMTP:Rcpt( ::cTo )
         ENDIF
         IF lOK
            lOK := oSMTP:Data( cRaw )
         ENDIF
      ENDIF
      IF ! lOK
         ::SetError( "SMTP rechazado o conexion fallida: " + InstinctDefault( oSMTP:cReply, "sin respuesta" ) )
      ENDIF
   RECOVER USING oErr
      ::SetError( "SMTP: " + oErr:Description )
   END SEQUENCE
   IF HB_ISOBJECT( oSMTP )
      oSMTP:Close()
   ENDIF
   IF lOK
      ::cId := cId
      ::LogAdd( "SEND ok id=" + cId + " bytes=" + hb_ntos( Len( cRaw ) ) )
   ENDIF
   /* Exito significa aceptacion del relay, no llegada ni respuesta de Instinct.
    * Ante un timeout final, NO reintentar a ciegas: pudo haberse aceptado. */
RETURN lOK

METHOD Answer( nSeconds, cId ) CLASS TInstinct
   LOCAL nStart := InstinctMillis(), aReplies, hItem, cPollError := "", n
   nSeconds := InstinctDefault( nSeconds, 30 )
   cId := InstinctDefault( cId, ::cId )
   IF ! HB_ISNUMERIC( nSeconds ) .OR. nSeconds < 1 .OR. nSeconds > 3600
      ::SetError( "Segundos de espera fuera de 1..3600" )
      RETURN NIL
   ENDIF
   IF ! SafeId( cId )
      ::SetError( "No hay ID de envio que esperar (falta Send()?)" )
      RETURN NIL
   ENDIF
   ::LogAdd( "ANSWER esperando id=" + cId + " hasta " + hb_ntos( nSeconds ) + " s" )
   DO WHILE .T.
      FOR n := 1 TO Len( ::aPending )
         IF ::aPending[ n ][ "reply" ][ "id" ] == cId
            hItem := ::aPending[ n ]
            ADel( ::aPending, n )
            ASize( ::aPending, Len( ::aPending ) - 1 )
            ::LogAdd( "ANSWER recibido id=" + cId )
            RETURN hItem[ "reply" ][ "text" ]
         ENDIF
      NEXT
      IF InstinctElapsed( nStart ) >= nSeconds * 1000
         EXIT
      ENDIF
      aReplies := ::PollOnce()
      IF HB_ISARRAY( aReplies )
         cPollError := ""
         FOR EACH hItem IN aReplies
            AAdd( ::aPending, hItem )
         NEXT
         IF ! Empty( aReplies )
            LOOP // inspect the queue without losing replies for other IDs
         ENDIF
      ELSE
         cPollError := ::cError // guardar: reintentamos hasta el limite
      ENDIF
      IF InstinctElapsed( nStart ) >= nSeconds * 1000
         EXIT
      ENDIF
      hb_IdleSleep( ::nPollEvery / 1000 )
   ENDDO
   ::SetError( iif( Empty( cPollError ), ;
      "Tiempo agotado esperando la respuesta " + cId, cPollError ) )
RETURN NIL

/* Registra el bloque que recibira cada respuesta nueva: {| cTexto, cId | }. */
METHOD OnAnswer( bBlock ) CLASS TInstinct
   IF bBlock != NIL .AND. ! HB_ISBLOCK( bBlock )
      ::SetError( "OnAnswer espera un bloque de codigo" )
      RETURN .F.
   ENDIF
   ::bOnAnswer := bBlock
RETURN .T.

/* Llamar desde un timer o el bucle idle: sondea UNA vez (sin esperar) y
 * dispara el bloque de OnAnswer con cada respuesta nueva. Devuelve cuantas
 * se entregaron; ante un error de sondeo devuelve 0 y fija ::cError. */
METHOD Pump() CLASS TInstinct
   LOCAL aReplies, hItem, nFired := 0
   IF ! HB_ISBLOCK( ::bOnAnswer )
      RETURN 0 // no receiver: do not consume replies
   ENDIF
   aReplies := ::PollOnce()
   IF HB_ISARRAY( aReplies )
      FOR EACH hItem IN aReplies
         AAdd( ::aPending, hItem )
      NEXT
   ENDIF
   DO WHILE ! Empty( ::aPending ) .AND. HB_ISBLOCK( ::bOnAnswer )
      hItem := ::aPending[ 1 ]
      ADel( ::aPending, 1 )
      ASize( ::aPending, Len( ::aPending ) - 1 )
      Eval( ::bOnAnswer, hItem[ "reply" ][ "text" ], hItem[ "reply" ][ "id" ] )
      nFired++
   ENDDO
RETURN nFired

METHOD PollOnce( nLimit ) CLASS TInstinct
   LOCAL oURL, oPOP, aReplies := {}, nCount, n, cUID, cRaw, hReply, oErr, lNew := .F., nProcessed := 0
   ::cError := ""
   IF ! LocalHost( ::cHost )
      ::SetError( "POP requiere proxy local; POP remoto sin TLS prohibido" )
      RETURN NIL
   ENDIF
   ::aWarnings := {}
   nLimit := InstinctDefault( nLimit, 20 )
   IF nLimit < 1 .OR. nLimit > 100
      ::SetError( "Limite POP fuera de 1..100" )
      RETURN NIL
   ENDIF
   ::LoadSeen()
   oURL := TUrl():New( "pop://" + ::cHost )
   oURL:nPort := ::nPopPort
   oURL:cUserid := ::cPopUser
   oURL:cPassword := ::cPopPassword
   BEGIN SEQUENCE WITH {| e | Break( e ) }
      oPOP := TIPClientPOP():New( oURL, .F. )
      oPOP:nConnTimeout := ::nTimeout
      IF ! oPOP:Open()
         Break( InstinctError( "Conexion/autenticacion POP fallida" ) )
      ENDIF
      nCount := oPOP:CountMail()
      IF nCount < 0
         Break( InstinctError( "STAT POP fallido" ) )
      ENDIF
      FOR n := 1 TO nCount
         cUID := oPOP:UIDL( n )
         IF ! HB_ISSTRING( cUID ) .OR. Empty( cUID )
            Break( InstinctError( "UIDL POP fallido" ) )
         ENDIF
         IF SafeUid( cUID ) .AND. hb_HHasKey( ::hSeen, cUID )
            LOOP // ya tratado en un sondeo anterior
         ENDIF
         IF nProcessed >= Int( nLimit )
            EXIT
         ENDIF
         nProcessed++
         oPOP:Reset()
         /* Retrieve limitado: no descargar indefinidamente mensajes ajenos. */
         cRaw := oPOP:Retrieve( n, ::nMaxBytes + 1 )
         IF ! HB_ISSTRING( cRaw ) .OR. Len( cRaw ) > ::nMaxBytes
            Break( InstinctError( "RETR fallido o mensaje demasiado grande" ) )
         ENDIF
         hReply := ::ParseReply( cRaw )
         IF SafeUid( cUID )
            ::hSeen[ cUID ] := .T.
            lNew := .T.
         ENDIF
         IF HB_ISHASH( hReply )
            AAdd( aReplies, { "uid" => cUID, "reply" => hReply } )
         ELSE
            AAdd( ::aWarnings, { "uid" => cUID, "error" => ::cError } )
         ENDIF
         ::cError := ""
      NEXT
   RECOVER USING oErr
      ::SetError( "POP: " + oErr:Description )
      aReplies := NIL
   END SEQUENCE
   IF HB_ISOBJECT( oPOP )
      oPOP:Close()
   ENDIF
   IF HB_ISARRAY( aReplies )
      ::LogAdd( "POLL nuevos=" + hb_ntos( Len( aReplies ) ) + ;
                " avisos=" + hb_ntos( Len( ::aWarnings ) ) )
   ENDIF
   IF lNew
      ::SaveSeen()
   ENDIF
   /* No DELE: el buzon conserva el correo y la deduplicacion es por UIDL
    * persistido en ::cUidlFile. Usar un buzon dedicado y archivar lo tratado. */
RETURN aReplies

METHOD ParseReply( cRaw, cExpectedId ) CLASS TInstinct
   LOCAL oMail, hReply, cBody, nUsed, oErr
   ::cError := ""
   IF ! HB_ISSTRING( cRaw ) .OR. Len( cRaw ) > ::nMaxBytes
      ::SetError( "Correo ausente o demasiado grande" )
      RETURN NIL
   ENDIF
   BEGIN SEQUENCE WITH {| e | Break( e ) }
      #ifdef __XHARBOUR__
      /* TIPMail on xHarbour only consumes complete CRLF lines. */
      IF Right( cRaw, 2 ) != Chr( 13 ) + Chr( 10 )
         cRaw += Chr( 13 ) + Chr( 10 )
      ENDIF
      #endif
      oMail := TIPMail():New()
      IF oMail:FromString( cRaw ) <= 0 .OR. oMail:IsMultiPart() .OR. ;
         !( Lower( oMail:GetContentType() ) == "text/plain" )
         ::SetError( "Se espera un correo MIME simple text/plain" )
         Break( InstinctError( ::cError ) )
      ENDIF
      cBody := oMail:GetBody()
      DO WHILE Len( cBody ) > 0 .AND. Asc( Right( cBody, 1 ) ) <= 32
         cBody := Left( cBody, Len( cBody ) - 1 )
      ENDDO
      DO WHILE Len( cBody ) > 0 .AND. Asc( Left( cBody, 1 ) ) <= 32
         cBody := SubStr( cBody, 2 )
      ENDDO
      nUsed := hb_jsonDecode( cBody, @hReply )
      IF nUsed != Len( cBody ) .OR. ! HB_ISHASH( hReply )
         ::SetError( "JSON incompleto/no valido" )
         Break( InstinctError( ::cError ) )
      ENDIF
      IF ! hb_HHasKey( hReply, "id" ) .OR. ! hb_HHasKey( hReply, "text" )
         ::SetError( "Respuesta sin id/text" )
         Break( InstinctError( ::cError ) )
      ENDIF
      IF ! SafeId( hReply[ "id" ] ) .OR. ! HB_ISSTRING( hReply[ "text" ] )
         ::SetError( "Tipos id/text no validos" )
         Break( InstinctError( ::cError ) )
      ENDIF
      IF cExpectedId != NIL .AND. !( hReply[ "id" ] == cExpectedId )
         ::SetError( "ID no corresponde a la peticion" )
         Break( InstinctError( ::cError ) )
      ENDIF
   RECOVER USING oErr
      ::SetError( "MIME/JSON: " + oErr:Description )
      RETURN NIL
   END SEQUENCE
   /* Parsear no autentica al autor. No ejecutar instrucciones de este hash. */
RETURN hReply

/* Cada linea del log lleva fecha y hora local: que ocurrio y cuando. */
METHOD LogAdd( cMsg ) CLASS TInstinct
   ::cLog += DToC( Date() ) + " " + Time() + " " + cMsg + hb_eol()
RETURN NIL

METHOD SetError( cMsg ) CLASS TInstinct
   ::cError := cMsg
   ::LogAdd( "ERROR: " + cMsg )
RETURN NIL

METHOD SaveLog( cFile ) CLASS TInstinct
   cFile := InstinctDefault( cFile, "tinstinct.log" )
   ::LogAdd( "LOG guardado en " + cFile )
RETURN hb_MemoWrit( cFile, ::cLog )

METHOD LoadSeen() CLASS TInstinct
   LOCAL cLine
   IF ::lSeenLoaded
      RETURN NIL
   ENDIF
   ::lSeenLoaded := .T.
   IF hb_FileExists( ::cUidlFile )
      FOR EACH cLine IN hb_ATokens( StrTran( hb_MemoRead( ::cUidlFile ), ;
            Chr( 13 ) + Chr( 10 ), Chr( 10 ) ), Chr( 10 ) )
         IF SafeUid( cLine )
            ::hSeen[ cLine ] := .T.
         ENDIF
      NEXT
   ENDIF
RETURN NIL

METHOD SaveSeen() CLASS TInstinct
   LOCAL cUid, cOut := ""
   FOR EACH cUid IN hb_HKeys( ::hSeen )
      cOut += cUid + hb_eol()
   NEXT
RETURN hb_MemoWrit( ::cUidlFile, cOut )

/* hbtip original admite algunos 4xx como OK; aqui se rechazan. */
CLASS TInstinctSMTP FROM TIPClientSMTP
#ifdef __XHARBOUR__
   METHOD OpenSecure( cUrl )
#endif
   METHOD GetOK()
   METHOD Data( cData )
ENDCLASS
#ifdef __XHARBOUR__
METHOD OpenSecure( cUrl ) CLASS TInstinctSMTP
   /* xHarbour's OpenSecure sends EHLO but returns the greeting, leaving
    * its 250 reply unread. Consume that reply before MAIL/AUTH. */
   IF ! ::super:OpenSecure( cUrl ) .OR. !( Left( ::cReply, 3 ) == "220" )
      RETURN .F.
   ENDIF
   DO WHILE .T.
      IF ! ::GetOK() .OR. !( Left( ::cReply, 3 ) == "250" )
         RETURN .F.
      ENDIF
      IF SubStr( ::cReply, 4, 1 ) != "-"
         EXIT
      ENDIF
   ENDDO
RETURN .T.
#endif
METHOD GetOK() CLASS TInstinctSMTP
   LOCAL nCode
   ::cReply := ::inetRecvLine( ::SocketCon,, 512 )
   IF ::inetErrorCode( ::SocketCon ) != 0 .OR. ! HB_ISSTRING( ::cReply )
      RETURN .F.
   ENDIF
   nCode := Val( Left( ::cReply, 3 ) )
RETURN ( nCode >= 200 .AND. nCode < 300 ) .OR. nCode == 334 .OR. nCode == 354
METHOD Data( cData ) CLASS TInstinctSMTP
   ::inetSendAll( ::SocketCon, "DATA" + ::cCRLF )
   IF ! ::GetOK() .OR. !( Left( ::cReply, 3 ) == "354" )
      RETURN .F.
   ENDIF
   IF ::inetSendAll( ::SocketCon, cData + ::cCRLF + "." + ::cCRLF ) < Len( cData ) + 5
      RETURN .F.
   ENDIF
RETURN ::GetOK() .AND. Left( ::cReply, 3 ) == "250"

STATIC FUNCTION LocalHost( cHost )
RETURN HB_ISSTRING( cHost ) .AND. ( cHost == "127.0.0.1" .OR. cHost == "localhost" )
STATIC FUNCTION Mailbox( cValue )
RETURN HB_ISSTRING( cValue ) .AND. ! Empty( cValue ) .AND. ;
   ! Empty( hb_regex( "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+$", cValue ) )
STATIC FUNCTION SafeId( cValue )
RETURN HB_ISSTRING( cValue ) .AND. Len( cValue ) >= 1 .AND. Len( cValue ) <= 128 .AND. ;
   ! Empty( hb_regex( "^[A-Za-z0-9._-]+$", cValue ) )
STATIC FUNCTION SafeUid( cValue )
RETURN HB_ISSTRING( cValue ) .AND. Len( cValue ) >= 1 .AND. Len( cValue ) <= 128 .AND. ;
   ! Empty( hb_regex( "^[A-Za-z0-9._:@%+-]+$", cValue ) )

STATIC FUNCTION InstinctError( cDescription )
   LOCAL oError := ErrorNew()
   oError:Description := cDescription
RETURN oError

STATIC FUNCTION InstinctDefault( xValue, xDefault )
RETURN iif( ValType( xValue ) == ValType( xDefault ), xValue, xDefault )

STATIC FUNCTION InstinctMillis()
#ifdef __XHARBOUR__
RETURN TI_MilliSeconds()
#else
RETURN hb_MilliSeconds()
#endif

#ifdef __XHARBOUR__
/* xHarbour has no hb_UTCOffset/hb_MilliSeconds. Use UTC and monotonic
 * clocks directly, never label local time as UTC. */
#pragma BEGINDUMP
#include "hbapi.h"
#include <stdio.h>
#if defined( _WIN32 )
#include <windows.h>
#else
#include <time.h>
#endif
HB_FUNC( TI_UTCSTAMP )
{
   char stamp[ 32 ];
#if defined( _WIN32 )
   SYSTEMTIME utc;
   GetSystemTime( &utc );
   sprintf( stamp, "%04u-%02u-%02uT%02u:%02u:%02u.%03uZ",
      utc.wYear, utc.wMonth, utc.wDay, utc.wHour, utc.wMinute,
      utc.wSecond, utc.wMilliseconds );
#else
   time_t now = time( NULL );
   struct tm utc;
   gmtime_r( &now, &utc );
   strftime( stamp, sizeof( stamp ), "%Y-%m-%dT%H:%M:%SZ", &utc );
#endif
   hb_retc( stamp );
}
HB_FUNC( TI_MILLISECONDS )
{
#if defined( _WIN32 )
   hb_retnd( ( double ) GetTickCount() );
#else
   struct timespec ts;
   clock_gettime( CLOCK_MONOTONIC, &ts );
   hb_retnd( ( double ) ts.tv_sec * 1000 + ts.tv_nsec / 1000000 );
#endif
}
#pragma ENDDUMP
#endif

STATIC FUNCTION InstinctElapsed( nStart )
   LOCAL nElapsed := InstinctMillis() - nStart
#ifdef __XHARBOUR__
   IF nElapsed < 0
      nElapsed += 4294967296
   ENDIF
#endif
RETURN nElapsed
```

Standalone Harbour demo: test.prg
--------------------------------

```harbour
/* test.prg - demo minima de TInstinct: tres pasos (crear, enviar, recibir).
 * Cada usuario configura la clase con SU remitente, SU token y la direccion
 * de SU propio Instinct; la clase absorbe JSON, MIME, SMTP, POP,
 * deduplicacion UIDL y log.
 * Compilar: hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
 */
PROCEDURE Main()
   LOCAL oInstinct, cAnswer, nTries
   hb_cdpSelect( "UTF8" )
   /* Configuracion por usuario: remitente, token compartido y la direccion
    * del buzon de SU Instinct (cada uno le pregunta a su Instinct por ella). */
   oInstinct := TInstinct():New( "a...@example.test", ; // quien envia
                                 "LOCAL-DEMO-NOT-A-SECRET", ; // su token
                                 "mi-in...@mail.instinct.com" ) // SU Instinct
   /* Solo en esta demo: apuntar a los servidores locales de prueba. */
   oInstinct:nSmtpPort := 25252
   oInstinct:nPopPort := 21110
   oInstinct:cPopUser := "local-user"
   oInstinct:cPopPassword := "local-password"
   IF oInstinct:Send( "Hola Instinct: TInstinct en tres lineas" )
      cAnswer := oInstinct:Answer( 10 ) // espera y devuelve lo recibido
      OutStd( "Enviado id=" + oInstinct:cId + hb_eol() )
      OutStd( iif( cAnswer == NIL, "Sin respuesta: " + oInstinct:cError, ;
                   "Respuesta: " + cAnswer ) + hb_eol() )
   ELSE
      OutStd( "Error: " + oInstinct:cError + hb_eol() )
      ErrorLevel( 1 )
   ENDIF
   /* Estilo GUI (opcional): bloque + Pump() desde un timer o bucle idle,
    * asi la app no se queda bloqueada esperando la respuesta. */
   oInstinct:OnAnswer( {| cTexto, cId | OutStd( "Async [" + cId + "]: " + ;
                                               cTexto + hb_eol() ) } )
   IF oInstinct:Send( "y ahora sin bloquear" )
      nTries := 0
      DO WHILE oInstinct:Pump() == 0 .AND. Empty( oInstinct:cError ) .AND. ++nTries < 20
         hb_IdleSleep( 0.5 ) // aqui la app atenderia su interfaz
      ENDDO
   ENDIF
   oInstinct:SaveLog( "demo.log" ) // la clase guarda todo lo ocurrido
RETURN
```

antonio....@gmail.com

unread,
Sep 30, 2026, 12:37:15 AM (24 hours ago) Sep 30
to Harbour Users
Estimados amigos de Harbour:

Quiero compartir TInstinct, una pequeña clase Harbour basada en Instinct. Permite a una aplicación intercambiar mensajes por email con el asistente Instinct de su propio usuario. Instinct es el asistente en el que se apoya esta propuesta; TInstinct es la clase de transporte del lado de Harbour, no un modelo de IA independiente.

Si queréis probar Instinct, tenéis el enlace de invitación en el mensaje inicial de este hilo:

La idea es tener IA de gran calidad y acceso a agentes en nuestras aplicaciones Harbour sin añadir una integración separada con la API de un modelo. Mi objetivo es hacerlo sin un coste adicional de API de IA, usando el asistente que ya tiene el usuario. Este prototipo no establece los precios, la disponibilidad de cuentas ni los límites de Instinct; eso depende de la cuenta del usuario y de las condiciones actuales del servicio.

La clase es neutral: no contiene una dirección de asistente fija, y cada usuario configura su remitente, token compartido y destino. Usa Harbour y hbtip, no FiveWin. La misma clase puede utilizarse en programas de consola y en aplicaciones con otras interfaces.

Es un prototipo de transporte, no una API oficial de Instinct ni un sistema de ejecución de agentes listo para usar. Antes de utilizarlo, acuerda con tu propio asistente cómo recibirá las peticiones y devolverá respuestas JSON con "id" y "text". Enviar un email no autoriza por sí solo al asistente a actuar en tu nombre.

Incluyo las instrucciones de uso y el código fuente en este mensaje, en lugar de un ZIP.

1. Los tres datos de configuración
----------------------------------

- cFrom: tu propia dirección de email, desde la que enviará la aplicación. Usa un buzón tuyo y acuerda con tu asistente el alcance permitido de esa comunicación en vuestra conversación habitual de confianza.
- cToken: un secreto compartido. Elige tu propio valor y comunícalo de forma segura. No publiques un token real en el código, en la documentación ni en este grupo. El token por sí solo no demuestra identidad ni autoriza acciones.
- cTo: la dirección de email de tu propio Instinct. Pregúntale a tu asistente cuál es su dirección de email. No hay destino por defecto.

Sustituye TU_EMAIL, TU_TOKEN y EMAIL_DE_TU_INSTINCT en los ejemplos. El orden del constructor es siempre:

```harbour
TInstinct():New( cFrom, cToken, cTo )
```

2. Hace falta un relay SMTP/POP local
------------------------------------

La clase solo conecta con un relay local. cHost vale "127.0.0.1" por defecto, con nSmtpPort = 25 y nPopPort = 110. Cambia los puertos según tu relay.

Configura cSmtpUser/cSmtpPassword si necesita autenticación SMTP, y cPopUser/cPopPassword para leer las respuestas. El relay debe proteger el salto a internet con TLS y validar los certificados. TInstinct no proporciona esa conexión TLS externa y no admite una conexión directa SMTP/POP remota sin cifrar.

3. Ejemplo mínimo: enviar y esperar
-----------------------------------

Guarda este ejemplo como test.prg:

```harbour
PROCEDURE Main()
   LOCAL oInstinct := TInstinct():New( "TU_EMAIL", "TU_TOKEN", ;
                                       "EMAIL_DE_TU_INSTINCT" )
   LOCAL cRespuesta

   IF ! oInstinct:Send( "Hola, Instinct" )
      ? oInstinct:cError
      RETURN
   ENDIF

   cRespuesta := oInstinct:Answer() // Espera hasta 30 segundos.
   IF cRespuesta == NIL
      ? oInstinct:cError
   ELSE
      ? cRespuesta
   ENDIF
RETURN
```

Send() devuelve .T. si el relay acepta el correo, no si Instinct ya lo recibió. El identificador queda en cId.

Answer() espera la respuesta de ese envío. Puedes usar Answer( 60 ) para esperar hasta 60 segundos. Devuelve el texto recibido o NIL si falla o se agota el tiempo. No reintentes a ciegas un envío con resultado incierto: el relay podría haber aceptado ya el mensaje.

4. Callback y sondeo temporizado
--------------------------------

En una aplicación con interfaz, registra un callback con OnAnswer() y llama a Pump() desde su timer o bucle idle.

Guarda este segundo ejemplo como test_async.prg. El bucle llama a Pump() cada segundo durante un máximo de 30 segundos:
OnAnswer() recibe el texto y el identificador de cada respuesta nueva. Pump() no espera a que llegue una respuesta, aunque la conexión y el sondeo POP pueden tardar. Es sondeo, no un hilo de trabajo en segundo plano.

5. Compilación, errores y log
-----------------------------

Compila cada ejemplo por separado con Harbour y hbtip:

```sh
hbmk2 test.prg TInstinct.prg hbtip.hbc -gtstd
hbmk2 test_async.prg TInstinct.prg hbtip.hbc -gtstd
```

El soporte JSON pertenece al runtime de Harbour; no hace falta una dependencia hbjson.hbc.

- cError contiene el último error. Consúltalo justo después de la operación.
- cLog registra los envíos, sondeos, respuestas y errores.
- SaveLog( "miapp.log" ) guarda el log y devuelve .T. o .F. Sin argumento usa tinstinct.log.

La clase gestiona JSON, respuestas MIME simples text/plain y deduplicación POP persistente por UIDL. No borra el correo del buzón de respuestas. Usa un buzón dedicado y archiva los mensajes ya tratados. Interpretar un JSON no autentica al remitente, y el texto recibido no debe considerarse permiso automático para ejecutar acciones.

Buzón y respuestas pendientes
-----------------------------

Usa un buzón POP dedicado y una sola instancia de la clase por buzón. PollOnce( nLimit ) trata hasta nLimit mensajes nuevos por llamada (20 por defecto, de 1 a 100). Salta los UIDL ya tratados, aunque estén al principio del buzón, y las llamadas siguientes llegan al resto. El estado de UIDL se guarda en cUidlFile; no se borra correo del servidor. Archiva periódicamente lo tratado para que el sondeo no tenga que recorrer una lista cada vez mayor.

Si Answer() espera el ID A y recibe antes el ID B, conserva B en aPending. Otro Answer( segundos, "B" ) puede recogerlo; Pump() también entrega las respuestas pendientes al callback. Pump() sin un callback no consume correo. La cola vive en esa instancia, no en disco: consúmela antes de cerrar la aplicación. La persistencia UIDL evita repetir mensajes, pero no es una cola durable de respuestas entre reinicios.

La clase completa corregida y una demo Harbour independiente están incluidas en el mensaje inicial en inglés de este mismo hilo. Son las fuentes validadas localmente; su incorporación a FWH está pendiente.

Agradecería vuestro feedback, especialmente los resultados de las pruebas, los problemas que encontréis y las sugerencias de mejora técnica.

Un cordial saludo,
Antonio Linares

antonio....@gmail.com

unread,
Sep 30, 2026, 12:38:26 AM (24 hours ago) Sep 30
to Harbour Users
Actualización tras el primer feedback: la clase se ha corregido y el paquete redistribuido ya incluye estas modificaciones.

Muchas gracias por revisar el código con tanto detalle. Has encontrado dos bugs reales y el proyecto ha mejorado gracias a tu análisis.

Ya están corregidos:

- PollOnce() ahora recorre el buzón saltando los UIDL ya vistos. El límite de 20 se aplica a los correos nuevos que procesa, no a las primeras 20 posiciones del buzón. Archivar los procesados ayuda al rendimiento, pero ya no es necesario para que pueda leer los nuevos.
- Answer() conserva las respuestas de otros id en una cola pendiente, en lugar de perderlas mientras espera el suyo. Se pueden recoger después con Answer() o entregar mediante Pump().

La batería ampliada pasa 78/78 comprobaciones tanto en Harbour como en xHarbour, incluidos los casos de buzón acumulado y respuestas desordenadas. El README explica también el uso de una sola instancia y que la cola pendiente vive en memoria: hay que consumirla antes de cerrar o reiniciar.

Sobre el fondo, tienes razón: las pruebas locales demuestran el transporte, no que Instinct funcione como una API. TInstinct es un prototipo de transporte por correo, no una API oficial. Falta la prueba decisiva: enviar un mensaje real y recibir de Instinct la respuesta acordada en JSON. Sigue pendiente; no quiero darla por hecha ni presentar el eco local como esa prueba.

También son límites reales mantener el relay local, la latencia del correo y necesitar tu propia cuenta de Instinct. "Gratis" no significa una API compartida. Y el token en el cuerpo no autentica el mensaje: el riesgo de prompt injection al introducir texto de terceros merece especial cuidado. Estos límites quedan recogidos en la documentación, no como garantías resueltas.

Gracias de nuevo. Si ves más fallos o pruebas que falten, serán bienvenidos 🙂

Antonio Linares

Auge & Ohr

unread,
Sep 30, 2026, 5:31:12 AM (19 hours ago) Sep 30
to Harbour Users
hi,
hb_randStr( 16 ) in line 106 is UNKNOWN ?
where can i find it ?
greetings Jimmy

antonio....@gmail.com

unread,
Sep 30, 2026, 9:59:49 AM (15 hours ago) Sep 30
to Harbour Users
FUNCTION hb_randStr( n )
   LOCAL c := "", i
   FOR i := 1 TO n
      c += Chr( hb_randomInt( 0, 255 ) )
   NEXT
RETURN c

Auge & Ohr

unread,
Sep 30, 2026, 10:21:06 AM (14 hours ago) Sep 30
to Harbour Users
hi,
THX for Function HB_randstr()
NEXT Error " missing" Class TIPMAIL ?
Jimmy

Auge & Ohr

unread,
Sep 30, 2026, 11:30:03 AM (13 hours ago) Sep 30
to Harbour Users
hi.
OK found Class TIPMAIL in \Fivewin\harbour_all.zip\contrib\hbtip\mail.prg
so how to include it with  TInstinct ?
Jimmy
Reply all
Reply to author
Forward
0 new messages