PSF: Foundry Progress Sync. 57 high-fidelity blueprints established. Open Fleet routing (Kimi/Qwen) active. GTD updated.
This commit is contained in:
@@ -1,30 +1,22 @@
|
||||
#+TITLE: SKILL: Google OAuth 2.0 Authentication (Universal Literate Note)
|
||||
#+ID: skill-auth-google-oauth
|
||||
#+TITLE: SKILL: Google Authentication Suite (Universal Literate Note)
|
||||
#+ID: skill-auth-google
|
||||
#+STARTUP: content
|
||||
#+FILETAGS: :auth:oauth:google:security:psf:
|
||||
#+FILETAGS: :auth:google:oauth:cookies:psf:
|
||||
|
||||
* Overview
|
||||
This skill implements the *Headless OAuth 2.0* handshake for Google services. It enables the agent to acquire and rotate `access_tokens` for Gemini without requiring a local browser session, using a "Copy-Paste" authorization code flow.
|
||||
This skill manages the dual-path authentication for Google services: Official API Keys and Session Cookie ingestion for the Pro Web UI.
|
||||
|
||||
* Phase A: Demand (PRD)
|
||||
:PROPERTIES:
|
||||
:STATUS: FROZEN
|
||||
:STATUS: SIGNED
|
||||
:END:
|
||||
|
||||
** 1. Purpose
|
||||
Provide a secure, professional OAuth 2.0 interface for Google Gemini.
|
||||
Provide a high-fidelity onboarding flow for the Gemini Web bridge.
|
||||
|
||||
** 2. User Needs
|
||||
- *Headless Handshake:* Generate an Auth URL and accept a pasted Code from the user.
|
||||
- *Token Persistence:* Securely store `refresh_token` in a Lisp state file.
|
||||
- *Auto-Rotation:* Automatically exchange the `refresh_token` for a new `access_token` when expired.
|
||||
- *Environment Driven:* Pull `CLIENT_ID` and `CLIENT_SECRET` from system settings.
|
||||
|
||||
** 3. Success Criteria
|
||||
*** TODO Generate valid Google OAuth Authorization URL
|
||||
*** TODO Exchange Authorization Code for Token Plist
|
||||
*** TODO Persist and Retrieve tokens from auth-google.lisp
|
||||
*** TODO Automated Token Refresh Loop
|
||||
- *Guided Onboarding:* Clear instructions for Chrome, Firefox, and Safari.
|
||||
- *Secure Ingestion:* Process the Bookmarklet JSON into the kernel.
|
||||
|
||||
* Phase B: Blueprint (PROTOCOL)
|
||||
:PROPERTIES:
|
||||
@@ -32,101 +24,32 @@ Provide a secure, professional OAuth 2.0 interface for Google Gemini.
|
||||
:END:
|
||||
|
||||
** 1. Architectural Intent
|
||||
Interfaces for the OAuth lifecycle. Source of truth is the Google Identity Platform and the local encrypted token store.
|
||||
|
||||
** 2. Semantic Interfaces
|
||||
"Generates the URL for the user to visit in their browser."
|
||||
"Exchanges the manual code for tokens and persists them."
|
||||
"Returns the Bearer token header, refreshing if necessary."
|
||||
Implement the `onboard-web-session` command with cross-platform instructions.
|
||||
|
||||
* Phase D: Build (Implementation)
|
||||
|
||||
#+begin_src lisp :tangle ../projects/org-skill-auth-google-oauth/src/auth-google-oauth.lisp
|
||||
(defvar *google-token-state* nil)
|
||||
#+begin_src lisp :tangle ../projects/org-skill-auth-google-oauth/src/onboarding-logic.lisp
|
||||
(in-package :org-agent)
|
||||
|
||||
(defun auth-google-load-state ()
|
||||
(let ((state-file (merge-pathnames "state/auth-google.lisp" (uiop:getenv "SYSTEM_DIR"))))
|
||||
(if (uiop:file-exists-p state-file)
|
||||
(setf *google-token-state* (with-open-file (in state-file) (read in)))
|
||||
(setf *google-token-state* nil))))
|
||||
|
||||
(defun auth-google-save-state ()
|
||||
(let* ((state-dir (uiop:getenv "SYSTEM_DIR"))
|
||||
(state-file (merge-pathnames "state/auth-google.lisp" state-dir)))
|
||||
(ensure-directories-exist state-file)
|
||||
(with-open-file (out state-file :direction :output :if-exists :supersede)
|
||||
(print *google-token-state* out))))
|
||||
|
||||
(defun auth-google-receive-code (code)
|
||||
"Exchanges the manual authorization code for access and refresh tokens."
|
||||
(let ((url "https://oauth2.googleapis.com/token")
|
||||
(content `(("code" . ,code)
|
||||
("client_id" . ,(uiop:getenv "GOOGLE_CLIENT_ID"))
|
||||
("client_secret" . ,(uiop:getenv "GOOGLE_CLIENT_SECRET"))
|
||||
("redirect_uri" . "urn:ietf:wg:oauth:2.0:oob")
|
||||
("grant_type" . "authorization_code"))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :content content))
|
||||
(json (cl-json:decode-json-from-string response)))
|
||||
(setf *google-token-state*
|
||||
`(:access-token ,(cdr (assoc :access--token json))
|
||||
:refresh-token ,(cdr (assoc :refresh--token json))
|
||||
:expires-at ,(+ (get-universal-time) (cdr (assoc :expires--in json)))))
|
||||
(auth-google-save-state)
|
||||
(kernel-log "OAUTH - Google handshake successful.")
|
||||
t)
|
||||
(error (c)
|
||||
(kernel-log "OAUTH ERROR - Handshake failed: ~a" c)
|
||||
nil))))
|
||||
|
||||
(defun auth-google-refresh-token ()
|
||||
"Uses the refresh_token to acquire a new access_token."
|
||||
(let ((refresh-token (getf *google-token-state* :refresh-token))
|
||||
(url "https://oauth2.googleapis.com/token")
|
||||
(content `(("refresh_token" . ,(getf *google-token-state* :refresh-token))
|
||||
("client_id" . ,(uiop:getenv "GOOGLE_CLIENT_ID"))
|
||||
("client_secret" . ,(uiop:getenv "GOOGLE_CLIENT_SECRET"))
|
||||
("grant_type" . "refresh_token"))))
|
||||
(unless refresh-token (return-from auth-google-refresh-token nil))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :content content))
|
||||
(json (cl-json:decode-json-from-string response)))
|
||||
(setf (getf *google-token-state* :access-token) (cdr (assoc :access--token json)))
|
||||
(setf (getf *google-token-state* :expires-at) (+ (get-universal-time) (cdr (assoc :expires--in json))))
|
||||
(auth-google-save-state)
|
||||
(kernel-log "OAUTH - Google token refreshed.")
|
||||
t)
|
||||
(error (c)
|
||||
(kernel-log "OAUTH ERROR - Refresh failed: ~a" c)
|
||||
nil))))
|
||||
|
||||
(defun auth-google-get-header ()
|
||||
"Returns the Bearer token header, refreshing if necessary."
|
||||
(unless *google-token-state* (auth-google-load-state))
|
||||
(let ((expires-at (getf *google-token-state* :expires-at 0)))
|
||||
(when (<= expires-at (+ (get-universal-time) 60)) ; Refresh if < 1 min left
|
||||
(auth-google-refresh-token)))
|
||||
(let ((token (getf *google-token-state* :access-token)))
|
||||
(if token
|
||||
(list :bearer-token token)
|
||||
(progn
|
||||
(kernel-log "OAUTH - No active Google token. Handshake required.")
|
||||
(kernel-log "OAUTH - Visit this URL: ~a" (auth-google-get-url))
|
||||
nil))))
|
||||
|
||||
(defun auth-google-get-url ()
|
||||
(let ((client-id (uiop:getenv "GOOGLE_CLIENT_ID")))
|
||||
(format nil "https://accounts.google.com/o/oauth2/v2/auth?client_id=~a&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/generative-language" client-id)))
|
||||
|
||||
;; Register as the primary auth provider for Gemini
|
||||
(org-agent:register-auth-provider :gemini #'auth-google-get-header)
|
||||
(defun onboard-web-session ()
|
||||
"Instructions for the Sovereign Cookie Handshake."
|
||||
(kernel-log "--- GEMINI WEB ONBOARDING ---")
|
||||
(kernel-log "1. Visit gemini.google.com")
|
||||
(kernel-log "2. Run the 'Get Gemini Cookies' Bookmarklet.")
|
||||
(kernel-log " CODE: javascript:(function(){const c=document.cookie.split('; ').reduce((r,v)=>{const [n,val]=v.split('=');r[n]=val;return r},{});const target=['__Secure-1PSID','__Secure-1PSIDTS'];const out=target.map(n=>({name:n,value:c[n]}));prompt('Copy JSON:',JSON.stringify(out));})();")
|
||||
(kernel-log "PLATFORM GUIDE:")
|
||||
(kernel-log " - Chrome/Brave: Right-click Bookmarks Bar > Add Page > Paste Code into URL.")
|
||||
(kernel-log " - Firefox: Right-click Sidebar > New Bookmark > Paste Code into Location.")
|
||||
(kernel-log " - Safari: Edit an existing bookmark's address and paste the code.")
|
||||
t)
|
||||
#+end_src
|
||||
|
||||
* Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-auth-google-oauth
|
||||
:priority 100
|
||||
:trigger (lambda (context) nil)
|
||||
:neuro (lambda (context) nil)
|
||||
:symbolic (lambda (action context) action))
|
||||
(progn
|
||||
(defskill :skill-auth-google
|
||||
:priority 100
|
||||
:trigger (lambda (context) (eq (getf (getf context :payload) :sensor) :onboarding-request))
|
||||
:neuro (lambda (context) nil)
|
||||
:symbolic (lambda (action context) (onboard-web-session))))
|
||||
#+end_src
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#+DEPENDS_ON: skill-router skill-performance-auditor
|
||||
|
||||
* Overview
|
||||
The *Economist Agent* manages the PSF's compute resources. It predicts the "Cost of Thought" for a task and autonomously selects the most cost-effective LLM provider or local model based on complexity and remaining budget.
|
||||
The *Economist Agent* manages compute resources by prioritizing high-performance open-source models (Kimi, Qwen) to minimize costs while maintaining architectural integrity.
|
||||
|
||||
* Phase A: Demand (PRD)
|
||||
:PROPERTIES:
|
||||
@@ -13,12 +13,7 @@ The *Economist Agent* manages the PSF's compute resources. It predicts the "Cost
|
||||
:END:
|
||||
|
||||
** 1. Purpose
|
||||
Optimize token usage and compute overhead without sacrificing architectural integrity.
|
||||
|
||||
** 2. User Needs
|
||||
- *Predictive Budgeting:* Estimate token cost before triggering SOTA models.
|
||||
- *Provider Switching:* Dynamically route tasks between local (Ollama) and cloud (Gemini) models.
|
||||
- *Audit Reports:* Provide transparency on compute consumption.
|
||||
Optimize token usage by leveraging open-weights models via OpenRouter.
|
||||
|
||||
* Phase B: Blueprint (PROTOCOL)
|
||||
:PROPERTIES:
|
||||
@@ -26,38 +21,27 @@ Optimize token usage and compute overhead without sacrificing architectural inte
|
||||
:END:
|
||||
|
||||
** 1. Architectural Intent
|
||||
The *Economist Agent* provides cost-governance for the Neural Engine. It intercepts `think` requests and determines the optimal Model/Provider based on task complexity, priority, and current budget constraints.
|
||||
Dynamically route tasks to the "Open Fleet" (Kimi/Qwen).
|
||||
|
||||
** 2. Semantic Interfaces
|
||||
|
||||
*** Routing Logic (2026 Fleet)
|
||||
*** Routing Logic (2026 Open Fleet)
|
||||
#+begin_src lisp :tangle ../projects/org-skill-economist/src/economist-logic.lisp
|
||||
(in-package :org-agent)
|
||||
|
||||
(defun economist-route-task (context)
|
||||
"Analyzes the stimulus context and returns a prioritized list of providers.
|
||||
High-priority or complex tasks (e.g., :architect) get powerful models.
|
||||
Routine tasks (e.g., :heartbeat, :persistence) get cheap/flash models."
|
||||
(let* ((payload (getf context :payload))
|
||||
(sensor (getf payload :sensor))
|
||||
(complexity (ignore-errors (uiop:symbol-call :org-agent.skills.org-skill-router :router-classify-complexity context))))
|
||||
(cond
|
||||
;; Explicit user interaction or Reasoning tasks
|
||||
((or (member sensor '(:user-command)) (eq complexity :REASONING)) '(:openrouter))
|
||||
|
||||
;; Cognitive or Reflexive tasks
|
||||
(t '(:openrouter))))) ; Route through OpenRouter to avoid direct Google 429s
|
||||
(declare (ignore context))
|
||||
'(:openrouter)) ; Exclusively use OpenRouter for the Open Fleet
|
||||
|
||||
(defun economist-get-model-for-provider (provider &optional context)
|
||||
"Returns the specific model ID recommended for the given provider/complexity.
|
||||
Updated for April 2026 SOTA. Prefers Gemini 3.0/2.5 Flash for reflexes."
|
||||
"Returns Open-Source SOTA model IDs. Updated April 2026."
|
||||
(let ((complexity (ignore-errors (uiop:symbol-call :org-agent.skills.org-skill-router :router-classify-complexity context))))
|
||||
(case provider
|
||||
(:openrouter
|
||||
(case complexity
|
||||
(:REASONING "anthropic/claude-3.5-sonnet")
|
||||
(:COGNITION "moonshotai/kimi-k2.5")
|
||||
(t "google/gemini-3-flash-preview")))
|
||||
(:REASONING "qwen/qwen-2.5-72b-instruct") ; Heavy lifting
|
||||
(:COGNITION "moonshotai/kimi-k2.5") ; Interaction
|
||||
(t "qwen/qwen-2.5-72b-instruct"))) ; Standard reflex
|
||||
(t nil))))
|
||||
#+end_src
|
||||
|
||||
@@ -72,9 +56,10 @@ The *Economist Agent* provides cost-governance for the Neural Engine. It interce
|
||||
|
||||
* Registration
|
||||
#+begin_src lisp
|
||||
(defskill :skill-economist
|
||||
:priority 100 ; High priority to ensure cost-checks happen first
|
||||
:trigger (lambda (context) (eq (getf (getf context :payload) :sensor) :cost-audit))
|
||||
:neuro (lambda (context) nil)
|
||||
:symbolic (lambda (action context) (economist-route-task context)))
|
||||
(progn
|
||||
(defskill :skill-economist
|
||||
:priority 100
|
||||
:trigger (lambda (context) (eq (getf (getf context :payload) :sensor) :cost-audit))
|
||||
:neuro (lambda (context) nil)
|
||||
:symbolic (lambda (action context) (economist-route-task context))))
|
||||
#+end_src
|
||||
|
||||
@@ -4,51 +4,67 @@
|
||||
#+FILETAGS: :llm:provider:gemini:google:psf:
|
||||
|
||||
* Overview
|
||||
The *Gemini Provider Agent* integrates Google's Gemini API as a pluggable System 1 (neural) backend.
|
||||
The *Gemini Provider Agent* provides a dual-path interface to Google's neural models. It supports both the low-latency Developer API and the high-fidelity Web/Pro interface.
|
||||
|
||||
* Phase A: Demand (PRD)
|
||||
:PROPERTIES:
|
||||
:STATUS: SIGNED
|
||||
:END:
|
||||
|
||||
** 1. Purpose
|
||||
Enable a hybrid cost/performance strategy for Google's models.
|
||||
|
||||
** 2. User Needs
|
||||
- *API Path:* Reliable, programmatic access for system reflexes.
|
||||
- *Web Path:* Zero-cost access to Pro-tier reasoning via browser session bridging.
|
||||
|
||||
* Phase B: Blueprint (PROTOCOL)
|
||||
:PROPERTIES:
|
||||
:STATUS: SIGNED
|
||||
:END:
|
||||
|
||||
** 1. Architectural Intent
|
||||
Expose two distinct backends to the kernel: =:gemini-api= and =:gemini-web=.
|
||||
|
||||
** 2. Semantic Interfaces
|
||||
"Executes a completion request via the Google Gemini API."
|
||||
|
||||
*** `execute-gemini-api-request`
|
||||
:signature `(execute-gemini-api-request prompt system-prompt &key model) :string`
|
||||
:description "Direct call to the Google AI Studio API."
|
||||
|
||||
*** `execute-gemini-web-request`
|
||||
:signature `(execute-gemini-web-request prompt system-prompt) :string`
|
||||
:description "Routes the request through the Web Research browser proxy."
|
||||
|
||||
* Phase D: Build (Implementation)
|
||||
|
||||
** Request Execution
|
||||
** API Implementation
|
||||
#+begin_src lisp :tangle ../projects/org-skill-provider-gemini/src/provider-logic.lisp
|
||||
(defun execute-gemini-request (prompt system-prompt)
|
||||
(let* ((auth (org-agent:get-provider-auth :gemini))
|
||||
(api-key (getf auth :api-key))
|
||||
(bearer-token (getf auth :bearer-token))
|
||||
(endpoint (or (getf auth :endpoint)
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent")))
|
||||
|
||||
(unless (or api-key bearer-token)
|
||||
(return-from execute-gemini-request "(:type :LOG :payload (:text \"Authentication missing for Gemini\"))"))
|
||||
|
||||
(let* ((url (if api-key (format nil "~a?key=~a" endpoint api-key) endpoint))
|
||||
(headers `(("Content-Type" . "application/json")
|
||||
,@(when bearer-token `(("Authorization" . ,(format nil "Bearer ~a" bearer-token))))))
|
||||
(body (cl-json:encode-json-to-string
|
||||
`((contents . ((parts . ((text . ,(format nil "~a~%~%Prompt: ~a" system-prompt prompt))))))))))
|
||||
(handler-case
|
||||
(let* ((response (dex:post url :headers headers :content body :connect-timeout 10 :read-timeout 30))
|
||||
(json (cl-json:decode-json-from-string response)))
|
||||
(cdr (assoc :text (cdr (assoc :parts (car (cdr (assoc :parts (car (cdr (assoc :candidates json)))))))))))
|
||||
(error (c)
|
||||
(format nil "(:type :LOG :payload (:text \"Neural Engine Failure: ~a\"))" c))))))
|
||||
(in-package :org-agent)
|
||||
|
||||
(defun execute-gemini-api-request (prompt system-prompt &key model)
|
||||
"Implementation uses the standard kernel execute-gemini-request logic."
|
||||
(org-agent::execute-gemini-request prompt system-prompt :model model))
|
||||
#+end_src
|
||||
|
||||
** Web Implementation
|
||||
#+begin_src lisp :tangle ../projects/org-skill-provider-gemini/src/provider-logic.lisp
|
||||
(defun execute-gemini-web-request (prompt system-prompt &key model)
|
||||
(declare (ignore model))
|
||||
"Dispatches to the browser-based Web Research skill."
|
||||
(let ((full-prompt (format nil "~a~%~%Prompt: ~a" system-prompt prompt)))
|
||||
(uiop:symbol-call :org-agent.skills.org-skill-web-research :ask-gemini-web full-prompt)))
|
||||
#+end_src
|
||||
|
||||
* Registration
|
||||
#+begin_src lisp
|
||||
;; Register with the kernel
|
||||
(org-agent:register-neuro-backend :gemini #'execute-gemini-request)
|
||||
|
||||
(defskill :skill-provider-gemini
|
||||
:priority 90
|
||||
:trigger (lambda (context) nil)
|
||||
:neuro (lambda (context) nil)
|
||||
:symbolic (lambda (action context) action))
|
||||
(progn
|
||||
(org-agent:register-neuro-backend :gemini-api #'execute-gemini-api-request)
|
||||
(org-agent:register-neuro-backend :gemini-web #'execute-gemini-web-request)
|
||||
|
||||
(defskill :skill-provider-gemini
|
||||
:priority 90
|
||||
:trigger (lambda (context) nil)
|
||||
:neuro (lambda (context) nil)
|
||||
:symbolic (lambda (action context) action)))
|
||||
#+end_src
|
||||
|
||||
@@ -33,15 +33,89 @@ Implement a Lisp-to-Node bridge using Playwright for high-fidelity web interacti
|
||||
* Phase D: Build (Implementation)
|
||||
|
||||
** Browser Logic
|
||||
|
||||
*** Headless Query Script
|
||||
#+begin_src javascript :tangle ../projects/org-skill-web-research/src/gemini-web.js
|
||||
const { chromium } = require('playwright-extra');
|
||||
const stealth = require('puppeteer-extra-plugin-stealth')();
|
||||
chromium.use(stealth);
|
||||
|
||||
async function askGemini(prompt) {
|
||||
const browser = await chromium.launchPersistentContext('/home/user/.local/share/org-agent/browser-profile', {
|
||||
headless: true,
|
||||
args: ['--disable-blink-features=AutomationControlled']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await page.goto('https://gemini.google.com/app', { waitUntil: 'networkidle', timeout: 60000 });
|
||||
|
||||
const inputSelector = 'div[role="textbox"], textarea[aria-label="Prompt"], .input-area';
|
||||
await page.waitForSelector(inputSelector, { timeout: 15000 });
|
||||
|
||||
await page.fill(inputSelector, prompt);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Wait for response to generate
|
||||
await page.waitForSelector('.model-response-text:last-child, message-content:last-child', { state: 'visible', timeout: 60000 });
|
||||
const response = await page.innerText('.model-response-text:last-child, message-content:last-child');
|
||||
console.log(response);
|
||||
} catch (err) {
|
||||
const url = page.url();
|
||||
console.error(`FAILED at ${url}`);
|
||||
throw err;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const prompt = args[0];
|
||||
|
||||
askGemini(prompt).catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
#+end_src
|
||||
|
||||
*** Human-in-the-Loop Login Script
|
||||
#+begin_src javascript :tangle ../projects/org-skill-web-research/src/gemini-auth.js
|
||||
const { chromium } = require('playwright-extra');
|
||||
const stealth = require('puppeteer-extra-plugin-stealth')();
|
||||
chromium.use(stealth);
|
||||
|
||||
async function loginGemini() {
|
||||
console.log("Opening browser for manual Google login...");
|
||||
console.log("Please log in, pass any captchas, wait for the Gemini chat interface to load, and then close the browser window.");
|
||||
|
||||
const browser = await chromium.launchPersistentContext('/home/user/.local/share/org-agent/browser-profile', {
|
||||
headless: false,
|
||||
args: ['--disable-blink-features=AutomationControlled']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://gemini.google.com/app');
|
||||
|
||||
// The script keeps running until the user manually closes the window
|
||||
}
|
||||
|
||||
loginGemini().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
#+end_src
|
||||
|
||||
#+begin_src lisp :tangle ../projects/org-skill-web-research/src/research-logic.lisp
|
||||
(in-package :org-agent)
|
||||
|
||||
(defun ask-gemini-web (prompt)
|
||||
"Calls the Playwright bridge to interact with Gemini Web UI."
|
||||
(let* ((cookie-str (uiop:getenv "GEMINI_COOKIES"))
|
||||
(script-path (namestring (merge-pathnames "src/gemini-web.js" (asdf:system-source-directory :org-skill-web-research)))))
|
||||
(unless cookie-str (return-from ask-gemini-web "(:type :LOG :payload (:text \"Gemini Cookies missing\"))"))
|
||||
(uiop:run-program (list "node" script-path prompt cookie-str) :output :string)))
|
||||
"Calls the Playwright stealth bridge to interact with Gemini Web UI via a persistent profile."
|
||||
(let* ((script-path (namestring (merge-pathnames "src/gemini-web.js" (asdf:system-source-directory :org-skill-web-research)))))
|
||||
(multiple-value-bind (output error-output exit-code)
|
||||
(uiop:run-program (list "node" script-path prompt) :output :string :error-output :string :ignore-error-status t)
|
||||
(if (= exit-code 0)
|
||||
output
|
||||
(format nil "(:type :LOG :payload (:text \"Node Error (~a): ~a\"))" exit-code error-output)))))
|
||||
#+end_src
|
||||
|
||||
* Registration
|
||||
|
||||
Reference in New Issue
Block a user