From 197edc708bf23beb90616df4441e4f6487d386f1 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 6 Apr 2025 17:42:01 +0800 Subject: [PATCH 01/12] making tool prototype in typescript --- .gitignore | 1 + calcit.cirru | 183 +++++++++++++++++-------------- compact.cirru | 30 ++--- package.json | 16 ++- src/main.mts | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 23 ++++ yarn.lock | 27 +++++ 7 files changed, 475 insertions(+), 101 deletions(-) create mode 100644 src/main.mts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index c5acf0f..1699205 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ storage.cirru backups/ .DS_Store +lib/* diff --git a/calcit.cirru b/calcit.cirru index 2c114ed..b0fa632 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -640,7 +640,7 @@ |comp-container $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1500541010211) (:by nil) :data $ {} - |T $ %{} :Leaf (:at 1500541010211) (:by |root) (:text |defcomp) + |T $ %{} :Leaf (:at 1743920036200) (:by |B1y7Rc-Zz) (:text |defcomp) |j $ %{} :Leaf (:at 1500541010211) (:by |root) (:text |comp-container) |r $ %{} :Expr (:at 1500541010211) (:by nil) :data $ {} @@ -736,6 +736,7 @@ |v $ %{} :Leaf (:at 1657793910144) (:by |B1y7Rc-Zz) (:text |css/column) |r $ %{} :Expr (:at 1500541010211) (:by nil) :data $ {} + |D $ %{} :Leaf (:at 1743920400077) (:by |B1y7Rc-Zz) (:text |memof1-call) |T $ %{} :Leaf (:at 1523120265747) (:by |root) (:text |comp-navigation) |j $ %{} :Expr (:at 1500541010211) (:by nil) :data $ {} @@ -766,90 +767,14 @@ |n $ %{} :Expr (:at 1525106918943) (:by |root) :data $ {} |T $ %{} :Leaf (:at 1525106921967) (:by |root) (:text |:home) - |j $ %{} :Expr (:at 1615983410742) (:by |B1y7Rc-Zz) + |b $ %{} :Expr (:at 1743920233497) (:by |B1y7Rc-Zz) :data $ {} - |D $ %{} :Leaf (:at 1615983411413) (:by |B1y7Rc-Zz) (:text |div) - |L $ %{} :Expr (:at 1615983411569) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615983411979) (:by |B1y7Rc-Zz) (:text |{}) - |b $ %{} :Expr (:at 1657793997580) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1657794001542) (:by |B1y7Rc-Zz) (:text |:class-name) - |b $ %{} :Leaf (:at 1657794003581) (:by |B1y7Rc-Zz) (:text |css/expand) - |j $ %{} :Expr (:at 1615984833365) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615984834879) (:by |B1y7Rc-Zz) (:text |:style) - |j $ %{} :Expr (:at 1615984835144) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615984835584) (:by |B1y7Rc-Zz) (:text |{}) - |j $ %{} :Expr (:at 1615984836078) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615984838548) (:by |B1y7Rc-Zz) (:text |:padding) - |j $ %{} :Leaf (:at 1615984843071) (:by |B1y7Rc-Zz) (:text "|\"8px") - |P $ %{} :Expr (:at 1615983412854) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615983414003) (:by |B1y7Rc-Zz) (:text |input) - |j $ %{} :Expr (:at 1615983414320) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615983414651) (:by |B1y7Rc-Zz) (:text |{}) - |j $ %{} :Expr (:at 1615983414866) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1657793994071) (:by |B1y7Rc-Zz) (:text |:class-name) - |j $ %{} :Leaf (:at 1657793995830) (:by |B1y7Rc-Zz) (:text |css/input) - |r $ %{} :Expr (:at 1615985094926) (:by |B1y7Rc-Zz) - :data $ {} - |D $ %{} :Leaf (:at 1615985095890) (:by |B1y7Rc-Zz) (:text |:value) - |T $ %{} :Expr (:at 1615985092894) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615985093771) (:by |B1y7Rc-Zz) (:text |:demo) - |j $ %{} :Leaf (:at 1615985100223) (:by |B1y7Rc-Zz) (:text |state) - |R $ %{} :Expr (:at 1615984821815) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615984822633) (:by |B1y7Rc-Zz) (:text |=<) - |j $ %{} :Leaf (:at 1615984823004) (:by |B1y7Rc-Zz) (:text |8) - |r $ %{} :Leaf (:at 1615984823512) (:by |B1y7Rc-Zz) (:text |nil) - |T $ %{} :Expr (:at 1539195346168) (:by |root) + |T $ %{} :Leaf (:at 1743920239196) (:by |B1y7Rc-Zz) (:text |comp-main-ui) + |b $ %{} :Expr (:at 1743920280977) (:by |B1y7Rc-Zz) :data $ {} - |T $ %{} :Leaf (:at 1539195347113) (:by |root) (:text |<>) - |j $ %{} :Leaf (:at 1615984827345) (:by |B1y7Rc-Zz) (:text "|\"demo page") - |j $ %{} :Expr (:at 1615984262519) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1615984303661) (:by |B1y7Rc-Zz) (:text |pre) - |j $ %{} :Expr (:at 1615984304152) (:by |B1y7Rc-Zz) - :data $ {} - |D $ %{} :Leaf (:at 1615984304642) (:by |B1y7Rc-Zz) (:text |{}) - |L $ %{} :Expr (:at 1646150434536) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1646150435604) (:by |B1y7Rc-Zz) (:text |:style) - |b $ %{} :Expr (:at 1646150436256) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1646150436589) (:by |B1y7Rc-Zz) (:text |{}) - |b $ %{} :Expr (:at 1646150436881) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1646150442246) (:by |B1y7Rc-Zz) (:text |:line-height) - |b $ %{} :Leaf (:at 1646150449670) (:by |B1y7Rc-Zz) (:text |1.4) - |e $ %{} :Expr (:at 1646150479259) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1646150480685) (:by |B1y7Rc-Zz) (:text |:padding) - |b $ %{} :Leaf (:at 1646150482506) (:by |B1y7Rc-Zz) (:text |4) - |h $ %{} :Expr (:at 1646150454548) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1646150472578) (:by |B1y7Rc-Zz) (:text |:border) - |b $ %{} :Expr (:at 1646150459003) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1646150461073) (:by |B1y7Rc-Zz) (:text |str) - |b $ %{} :Leaf (:at 1646150477219) (:by |B1y7Rc-Zz) (:text "|\"1px solid #ddd") - |T $ %{} :Expr (:at 1615984305087) (:by |B1y7Rc-Zz) - :data $ {} - |D $ %{} :Leaf (:at 1615984307552) (:by |B1y7Rc-Zz) (:text |:inner-text) - |T $ %{} :Expr (:at 1615984265859) (:by |B1y7Rc-Zz) - :data $ {} - |L $ %{} :Leaf (:at 1615984302121) (:by |B1y7Rc-Zz) (:text |str) - |j $ %{} :Leaf (:at 1615984270059) (:by |B1y7Rc-Zz) (:text "|\"backend data") - |r $ %{} :Expr (:at 1615984270835) (:by |B1y7Rc-Zz) - :data $ {} - |T $ %{} :Leaf (:at 1623719461768) (:by |B1y7Rc-Zz) (:text |format-cirru-edn) - |j $ %{} :Leaf (:at 1615984279552) (:by |B1y7Rc-Zz) (:text |store) + |D $ %{} :Leaf (:at 1743920282012) (:by |B1y7Rc-Zz) (:text |>>) + |L $ %{} :Leaf (:at 1743920287384) (:by |B1y7Rc-Zz) (:text |states) + |T $ %{} :Leaf (:at 1743920258792) (:by |B1y7Rc-Zz) (:text |:home) |r $ %{} :Expr (:at 1500541010211) (:by nil) :data $ {} |T $ %{} :Leaf (:at 1500541010211) (:by |root) (:text |:profile) @@ -885,7 +810,7 @@ |L $ %{} :Leaf (:at 1521911495407) (:by |root) (:text |dev?) |T $ %{} :Expr (:at 1500541010211) (:by nil) :data $ {} - |T $ %{} :Leaf (:at 1500541010211) (:by |root) (:text |comp-inspect) + |T $ %{} :Leaf (:at 1743915836427) (:by |B1y7Rc-Zz) (:text |comp-inspect) |j $ %{} :Leaf (:at 1562176377826) (:by |B1y7Rc-Zz) (:text "|\"Store") |r $ %{} :Leaf (:at 1500541010211) (:by |root) (:text |store) |v $ %{} :Expr (:at 1500541010211) (:by nil) @@ -944,6 +869,88 @@ |r $ %{} :Expr (:at 1507828721052) (:by |root) :data $ {} |T $ %{} :Leaf (:at 1507828722268) (:by |root) (:text |{}) + |comp-main-ui $ %{} :CodeEntry (:doc |) + :code $ %{} :Expr (:at 1743920239640) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920241766) (:by |B1y7Rc-Zz) (:text |defn) + |b $ %{} :Leaf (:at 1743920239640) (:by |B1y7Rc-Zz) (:text |comp-main-ui) + |h $ %{} :Expr (:at 1743920239640) (:by |B1y7Rc-Zz) + :data $ {} + |b $ %{} :Leaf (:at 1743920254394) (:by |B1y7Rc-Zz) (:text |states) + |l $ %{} :Expr (:at 1743920293752) (:by |B1y7Rc-Zz) + :data $ {} + |D $ %{} :Leaf (:at 1743920294288) (:by |B1y7Rc-Zz) (:text |let) + |L $ %{} :Expr (:at 1743920294552) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Expr (:at 1743920295365) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920297445) (:by |B1y7Rc-Zz) (:text |cursor) + |b $ %{} :Expr (:at 1743920298123) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920298371) (:by |B1y7Rc-Zz) (:text |:cursor) + |b $ %{} :Leaf (:at 1743920301358) (:by |B1y7Rc-Zz) (:text |states) + |b $ %{} :Expr (:at 1743920302534) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920303935) (:by |B1y7Rc-Zz) (:text |state) + |b $ %{} :Expr (:at 1743920304375) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920304919) (:by |B1y7Rc-Zz) (:text |:data) + |b $ %{} :Leaf (:at 1743920306648) (:by |B1y7Rc-Zz) (:text |states) + |T $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |div) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |{}) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:class-name) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |str-spaced) + |b $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |css/expand) + |h $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |css/column) + |h $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:style) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |{}) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:padding) + |b $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text "|\"8px") + |h $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |div) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |{}) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:class-name) + |b $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |css/expand) + |l $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |textarea) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |{}) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:class-name) + |b $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |css/textarea) + |h $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:placeholder) + |b $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text "|\"prompt for task..") + |l $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:value) + |b $ %{} :Expr (:at 1743920242834) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |:demo) + |b $ %{} :Leaf (:at 1743920242834) (:by |B1y7Rc-Zz) (:text |state) |comp-offline $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1519314599832) (:by |root) :data $ {} @@ -1225,6 +1232,7 @@ |y $ %{} :Leaf (:at 1507815955483) (:by |root) (:text |button) |yT $ %{} :Leaf (:at 1615983477070) (:by |B1y7Rc-Zz) (:text |input) |yj $ %{} :Leaf (:at 1615984295300) (:by |B1y7Rc-Zz) (:text |pre) + |z $ %{} :Leaf (:at 1743916051467) (:by |B1y7Rc-Zz) (:text |textarea) |xT $ %{} :Expr (:at 1657794056783) (:by |B1y7Rc-Zz) :data $ {} |T $ %{} :Leaf (:at 1657794057967) (:by |B1y7Rc-Zz) (:text |respo.css) @@ -1298,6 +1306,13 @@ |j $ %{} :Leaf (:at 1535564718729) (:by |B1y7Rc-Zz) (:text |app.config) |r $ %{} :Leaf (:at 1535564719687) (:by |B1y7Rc-Zz) (:text |:as) |v $ %{} :Leaf (:at 1535564721387) (:by |B1y7Rc-Zz) (:text |config) + |z $ %{} :Expr (:at 1743920404783) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920409331) (:by |B1y7Rc-Zz) (:text |memof.once) + |b $ %{} :Leaf (:at 1743920410058) (:by |B1y7Rc-Zz) (:text |:refer) + |h $ %{} :Expr (:at 1743920410261) (:by |B1y7Rc-Zz) + :data $ {} + |T $ %{} :Leaf (:at 1743920412686) (:by |B1y7Rc-Zz) (:text |memof1-call) |app.comp.login $ %{} :FileEntry :defs $ {} |comp-login $ %{} :CodeEntry (:doc |) diff --git a/compact.cirru b/compact.cirru index fd54368..8505aec 100644 --- a/compact.cirru +++ b/compact.cirru @@ -125,20 +125,10 @@ router-data $ :data router div {} $ :class-name (str-spaced css/preset css/global css/fullscreen css/column) - comp-navigation (:logged-in? store) (:count store) + memof1-call comp-navigation (:logged-in? store) (:count store) if (:logged-in? store) case-default (:name router) (<> router) - :home $ div - {} (:class-name css/expand) - :style $ {} (:padding "\"8px") - input $ {} (:class-name css/input) - :value $ :demo state - =< 8 nil - <> "\"demo page" - pre $ {} - :style $ {} (:line-height 1.4) (:padding 4) - :border $ str "\"1px solid #ddd" - :inner-text $ str "\"backend data" (format-cirru-edn store) + :home $ comp-main-ui (>> states :home) :profile $ comp-profile (:user store) (:data router) comp-login $ >> states :login comp-status-color $ :color store @@ -149,6 +139,19 @@ {} fn (info d!) (d! :session/remove-message info) when dev? $ comp-reel (:reel-length store) ({}) + |comp-main-ui $ %{} :CodeEntry (:doc |) + :code $ quote + defn comp-main-ui (states) + let + cursor $ :cursor states + state $ :data states + div + {} + :class-name $ str-spaced css/expand css/column + :style $ {} (:padding "\"8px") + div $ {} (:class-name css/expand) + textarea $ {} (:class-name css/textarea) (:placeholder "\"prompt for task..") + :value $ :demo state |comp-offline $ %{} :CodeEntry (:doc |) :code $ quote defcomp comp-offline (mark) @@ -192,7 +195,7 @@ respo.util.format :refer $ hsl respo-ui.core :as ui respo-ui.css :as css - respo.core :refer $ defcomp <> >> div span button input pre + respo.core :refer $ defcomp <> >> div span button input pre textarea respo.css :refer $ defstyle respo.comp.inspect :refer $ comp-inspect respo.comp.space :refer $ =< @@ -204,6 +207,7 @@ app.config :refer $ dev? app.schema :as schema app.config :as config + memof.once :refer $ memof1-call |app.comp.login $ %{} :FileEntry :defs $ {} |comp-login $ %{} :CodeEntry (:doc |) diff --git a/package.json b/package.json index 127d84e..ecff6cf 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,24 @@ { "dependencies": { - "@calcit/procs": "^0.9.9" + "@calcit/procs": "^0.9.9", + "@google/generative-ai": "^0.24.0", + "chalk": "^5.4.1" }, "scripts": { "compile-page": "cr --once js", "release-page": "vite build --base=./", - "watch-page": "cr js" + "watch-page": "cr js", + "tool": "tsc && node lib/main.mjs" }, "devDependencies": { + "@types/node": "^22.14.0", "bottom-tip": "^0.1.5", + "typescript": "^5.8.3", "url-parse": "^1.5.10", "vite": "^6.2.5" }, - "version": "0.0.1" -} + "version": "0.0.1", + "cirruInfo": { + "calcitVersion": "0.9.9" + } +} \ No newline at end of file diff --git a/src/main.mts b/src/main.mts new file mode 100644 index 0000000..81255bf --- /dev/null +++ b/src/main.mts @@ -0,0 +1,296 @@ +import { + GoogleGenerativeAI, + FunctionCallingMode, + Tool, + SchemaType, + ToolConfig, +} from "@google/generative-ai"; +import * as readline from "readline"; +import path from "path"; +import fs from "fs/promises"; +import { exec, execFile } from "child_process"; +import { promisify } from "util"; +import chalk from "chalk"; + +// Promisify functions +const execPromise = promisify(exec); +const execFilePromise = promisify(execFile); + +// Initialize the Generative AI client +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); +const model = genAI.getGenerativeModel( + // { model: "gemini-1.5-flash" }, + { model: "gemini-2.0-flash" }, + { baseUrl: "https://sf.chenyong.life" } +); + +// Define function to execute Node.js code +const executeNodeJsCode = async (code: string, tempDir: string) => { + // Create temp directory if it doesn't exist + try { + await fs.mkdir(tempDir, { recursive: true }); + } catch (err) { + // Directory might already exist + } + + // Create a temporary file with the code + const tempFilePath = path.join(tempDir, `exec_${Date.now()}.mjs`); + await fs.writeFile(tempFilePath, code); + + // Execute the file as an ES module + const result = await execFilePromise("node", [ + "--experimental-modules", + tempFilePath, + ]); + + // Clean up the temporary file + await fs + .unlink(tempFilePath) + .catch((err) => console.error("Failed to delete temp file:", err)); + + return result; +}; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const ask = (question: string) => { + return new Promise((resolve) => { + rl.question(chalk.cyan(question), (answer) => { + resolve(answer); + }); + }); +}; + +const sayingOk = (message: string) => { + return message === "ok" || message === "yes" || message === "y"; +}; + +const main = async () => { + try { + // Create a chat session + // Define a function declaration tool + const tools: Tool[] = [ + { + functionDeclarations: [ + { + name: "executeBashCommand", + description: + "provide bash command, run in system, and return the output. it can access file system. also called 'bash 脚本'", + parameters: { + type: SchemaType.OBJECT, + properties: { + command: { + type: SchemaType.STRING, + description: "The bash command to execute", + }, + }, + required: ["command"], + }, + }, + { + name: "executeNodeCode", + description: + "provide node.js script in ES Module syntax. Execute in system and return the output. it can access the file system. also called 'node.js 脚本'", + parameters: { + type: SchemaType.OBJECT, + properties: { + code: { + type: SchemaType.STRING, + description: + "The Node.js code to execute. Must use ES module syntax (e.g., 'import fs from \"fs/promises\"' instead of 'const fs = require(\"fs\")'). Write self-contained code that doesn't depend on external files.", + }, + }, + required: ["code"], + }, + }, + ], + }, + ]; + + // Configure tool settings + const toolConfig: ToolConfig = { + functionCallingConfig: { + mode: FunctionCallingMode.AUTO, // Let the model decide when to call the function + }, + }; + + // Create a chat session with the defined tool + const chat = model.startChat({ + tools, + toolConfig, + history: [ + { + role: "user", + parts: [ + { + text: "每次回答都可以先考虑是否调用 executeBashCommand 或者 executeNodeCode 这些工具函数然后再回复, 注意结合上下文. 请尽可能使用中文回复我的问题,但是代码保留英文。", + }, + ], + }, + ], + }); + + while (true) { + const question = await ask("\nWhat's the task: "); + + if (question.toLowerCase() === "exit") { + console.log("Bye!"); + break; + } + + console.log("\nResponse:"); + // Use the chat API to send messages and get streaming responses + const response = await chat.sendMessage(question); + const textResponse = response.response.text(); + if (textResponse) { + process.stdout.write(textResponse); + } + + // Handle function calls if any + const functionCalls = response.response.functionCalls(); + if (functionCalls && functionCalls.length > 0) { + for (const functionCall of functionCalls) { + if (functionCall.name === "executeBashCommand") { + const args: any = functionCall.args; + const command = args.command; + // Ask for user confirmation + console.log(`\nBash command to execute:`); + console.log(chalk.yellow(command)); + const confirmation = await ask( + `\nExecute this Bash command? (y/n):` + ); + + if (sayingOk(confirmation)) { + console.log(`\nExecuting bash command: ${command}`); + + try { + const { stdout, stderr } = await execPromise(command); + const result = { stdout, stderr, success: true }; + if (result.success) { + console.log(result.stdout); + } else { + console.log(result.stderr); + } + + // Send the result back to the model + const functionResponse = await chat.sendMessage( + JSON.stringify(result) + ); + console.log("\n"); + process.stdout.write(functionResponse.response.text()); + } catch (error) { + const result = { + stdout: error.stdout || "", + stderr: error.stderr || error.message, + success: false, + }; + + const functionResponse = await chat.sendMessage( + JSON.stringify(result) + ); + console.log("\n"); + console.log("Command failed:", result); + console.log("\nFunction response:"); + process.stdout.write(functionResponse.response.text()); + } + } else { + const result = { + stdout: "", + stderr: "User declined to execute the command", + success: false, + }; + + const functionResponse = await chat.sendMessage( + JSON.stringify(result) + ); + console.log("\nUser declined to execute the command"); + } + } else if (functionCall.name === "executeNodeCode") { + const args: any = functionCall.args; + const code = args.code; + + // Ask for user confirmation + // Display code with syntax highlighting and ask for confirmation + console.log("\nNode.js code to execute:"); + console.log(chalk.yellow(code)); + const confirmation = await ask( + `\nExecute this Node.js code? (y/n): ` + ); + + if (sayingOk(confirmation)) { + console.log(`\nExecuting Node.js code`); + + try { + // Execute the code + const tempDir = path.join(process.cwd(), "./"); + const { stdout, stderr } = await executeNodeJsCode( + code, + tempDir + ); + + const output = { + stdout, + stderr, + success: true, + }; + if (output.success) { + console.log(output.stdout); + } else { + console.log(chalk.red("Execution failed:"), output); + } + + // Send the result back to the model using streaming + const functionResponse = await chat.sendMessageStream( + JSON.stringify(output) + ); + console.log("\n"); + for await (const chunk of functionResponse.stream) { + process.stdout.write(chunk.text()); + } + } catch (error) { + const output = { + stdout: "", + stderr: error.message || String(error), + success: false, + }; + + const functionResponse = await chat.sendMessage( + JSON.stringify(output) + ); + console.log("Code execution failed:", output); + console.log("\n"); + process.stdout.write(functionResponse.response.text()); + } + } else { + const result = { + stdout: "", + stderr: "User declined to execute the code", + success: false, + }; + + const functionResponse = await chat.sendMessageStream( + JSON.stringify(result) + ); + for await (const chunk of functionResponse.stream) { + process.stdout.write(chunk.text()); + } + console.log("\nUser declined to execute the code"); + } + } + } + } + + console.log("\n"); + } + } catch (err) { + console.error("发生错误:", err); + rl.close(); + process.exit(1); + } finally { + rl.close(); + } +}; +main(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..10a4bb9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "sourceMap": false, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "moduleResolution": "node", + "module": "ESNext", + "target": "es2020", + "jsx": "react-jsx", + "lib": ["es2022", "dom", "dom.iterable"], + "types": ["node"], + "baseUrl": "./src/", + "plugins": [], + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./lib" + } +} diff --git a/yarn.lock b/yarn.lock index 13c594f..dec5a8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -151,6 +151,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz#839f72c2decd378f86b8f525e1979a97b920c67d" integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA== +"@google/generative-ai@^0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.0.tgz#4d27af7d944c924a27a593c17ad1336535d53846" + integrity sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q== + "@rollup/rollup-android-arm-eabi@4.39.0": version "4.39.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz#1d8cc5dd3d8ffe569d8f7f67a45c7909828a0f66" @@ -256,6 +261,13 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== +"@types/node@^22.14.0": + version "22.14.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.0.tgz#d3bfa3936fef0dbacd79ea3eb17d521c628bb47e" + integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA== + dependencies: + undici-types "~6.21.0" + bottom-tip@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/bottom-tip/-/bottom-tip-0.1.5.tgz#ca81e738fba6ae956a5b4c55a78a127820c9b99e" @@ -274,6 +286,11 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== +chalk@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -439,6 +456,16 @@ string-template@~0.2.0: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== +typescript@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + url-parse@^1.5.10: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" From d00afad77f102bbb51dffd122c6e34921e13c4e2 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 6 Apr 2025 17:53:53 +0800 Subject: [PATCH 02/12] install locally --- .npmignore | 23 +++++++++++++++++++++++ bin.mjs | 3 +++ package.json | 6 +++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .npmignore create mode 100755 bin.mjs diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..4d9d5e1 --- /dev/null +++ b/.npmignore @@ -0,0 +1,23 @@ + + +.compact-inc.cirru +.calcit-error.cirru + +js-out/ +dist/ + +yarn-error.log + +storage.cirru + +backups/ + +.DS_Store + +*.cirru + +.github + +tsconfig.json + +index.html diff --git a/bin.mjs b/bin.mjs new file mode 100755 index 0000000..fd05e55 --- /dev/null +++ b/bin.mjs @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import "./lib/main.mjs"; diff --git a/package.json b/package.json index ecff6cf..f055fb5 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "macrophylla", "dependencies": { "@calcit/procs": "^0.9.9", "@google/generative-ai": "^0.24.0", @@ -10,6 +11,9 @@ "watch-page": "cr js", "tool": "tsc && node lib/main.mjs" }, + "bin": { + "mcpl": "./bin.mjs" + }, "devDependencies": { "@types/node": "^22.14.0", "bottom-tip": "^0.1.5", @@ -21,4 +25,4 @@ "cirruInfo": { "calcitVersion": "0.9.9" } -} \ No newline at end of file +} From ae6f72072964385051e70ee54580f8edf6ac889c Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 7 Apr 2025 01:32:08 +0800 Subject: [PATCH 03/12] simplify code a bit. better interactions --- .gitignore | 2 +- .vscode/settings.json | 12 ++ README.md | 23 +++- src/main.mts | 291 +++++++++++++++++++----------------------- 4 files changed, 164 insertions(+), 164 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 1699205..f0bdca7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ storage.cirru backups/ .DS_Store -lib/* +lib diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4a855c3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "files.exclude": { + "lib/**": true, + "js-out/**": true, + "node_modules/**": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + } +} diff --git a/README.md b/README.md index 1ff42a0..fc8549f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ - -Macrophylla ----- +## Macrophylla > try smarter CLI tools, still experimenting... +Macrophylla 是一个命令行助手,它使用 Gemini API 与用户交互,并能够执行 Bash 命令和 Node.js 代码。 + +### 使用方式 + +1. **准备 Gemini API Key:** 确保你已经设置了 `GEMINI_API_KEY` 环境变量。 + +2. **运行工具:** 直接运行该工具。 + +3. **与助手交互:** 工具会提示你输入任务描述。 你可以使用自然语言描述你的需求。 + +4. **执行 Bash 命令和 Node.js 代码:** 工具会根据你的描述,自动判断是否需要执行 Bash 命令或 Node.js 代码来完成任务。 如果需要执行,会先向你确认,然后执行并将结果返回。 + +5. **示例:** + + - 用户: 读取当前目录下的 `README.md` 文件内容。 + - 助手: (判断需要执行 Bash 命令) Bash command to execute: `cat README.md` Execute this Bash command? (y/n): + - 用户: y + - 助手: (执行命令,并将结果返回) + ### License MIT diff --git a/src/main.mts b/src/main.mts index 81255bf..17c08db 100644 --- a/src/main.mts +++ b/src/main.mts @@ -65,50 +65,53 @@ const ask = (question: string) => { }; const sayingOk = (message: string) => { - return message === "ok" || message === "yes" || message === "y"; + return ( + message === "ok" || message === "yes" || message === "y" || message === "" + ); }; -const main = async () => { - try { - // Create a chat session - // Define a function declaration tool - const tools: Tool[] = [ +const tools: Tool[] = [ + { + functionDeclarations: [ { - functionDeclarations: [ - { - name: "executeBashCommand", - description: - "provide bash command, run in system, and return the output. it can access file system. also called 'bash 脚本'", - parameters: { - type: SchemaType.OBJECT, - properties: { - command: { - type: SchemaType.STRING, - description: "The bash command to execute", - }, - }, - required: ["command"], + name: "executeBashCommand", + description: + "provide bash command, run in system, and return the output. it can access file system. also called 'bash 脚本'", + parameters: { + type: SchemaType.OBJECT, + properties: { + command: { + type: SchemaType.STRING, + description: "The bash command to execute", }, }, - { - name: "executeNodeCode", - description: - "provide node.js script in ES Module syntax. Execute in system and return the output. it can access the file system. also called 'node.js 脚本'", - parameters: { - type: SchemaType.OBJECT, - properties: { - code: { - type: SchemaType.STRING, - description: - "The Node.js code to execute. Must use ES module syntax (e.g., 'import fs from \"fs/promises\"' instead of 'const fs = require(\"fs\")'). Write self-contained code that doesn't depend on external files.", - }, - }, - required: ["code"], + required: ["command"], + }, + }, + { + name: "executeNodeCode", + description: + "provide node.js script in ES Module syntax. Execute in system and return the output. it can access the file system. also called 'node.js 脚本'", + parameters: { + type: SchemaType.OBJECT, + properties: { + code: { + type: SchemaType.STRING, + description: + "The Node.js code to execute. Must use ES module syntax. Write self-contained code that doesn't depend on external files.", }, }, - ], + required: ["code"], + }, }, - ]; + ], + }, +]; + +const main = async () => { + try { + // Create a chat session + // Define a function declaration tool // Configure tool settings const toolConfig: ToolConfig = { @@ -126,22 +129,25 @@ const main = async () => { role: "user", parts: [ { - text: "每次回答都可以先考虑是否调用 executeBashCommand 或者 executeNodeCode 这些工具函数然后再回复, 注意结合上下文. 请尽可能使用中文回复我的问题,但是代码保留英文。", + text: "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复, 注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。", }, ], }, ], }); + let nextQuestion: string = ""; + while (true) { - const question = await ask("\nWhat's the task: "); + const question = nextQuestion || (await ask("\nWhat's the task: ")); if (question.toLowerCase() === "exit") { console.log("Bye!"); break; } - console.log("\nResponse:"); + console.log(chalk.gray("\nResponding...\n")); + // Use the chat API to send messages and get streaming responses const response = await chat.sendMessage(question); const textResponse = response.response.text(); @@ -149,148 +155,113 @@ const main = async () => { process.stdout.write(textResponse); } + // clear the next question cache + nextQuestion = ""; + // Handle function calls if any - const functionCalls = response.response.functionCalls(); - if (functionCalls && functionCalls.length > 0) { - for (const functionCall of functionCalls) { - if (functionCall.name === "executeBashCommand") { - const args: any = functionCall.args; - const command = args.command; - // Ask for user confirmation - console.log(`\nBash command to execute:`); - console.log(chalk.yellow(command)); - const confirmation = await ask( - `\nExecute this Bash command? (y/n):` - ); - - if (sayingOk(confirmation)) { - console.log(`\nExecuting bash command: ${command}`); - - try { - const { stdout, stderr } = await execPromise(command); - const result = { stdout, stderr, success: true }; - if (result.success) { - console.log(result.stdout); - } else { - console.log(result.stderr); - } - - // Send the result back to the model - const functionResponse = await chat.sendMessage( - JSON.stringify(result) - ); - console.log("\n"); - process.stdout.write(functionResponse.response.text()); - } catch (error) { - const result = { - stdout: error.stdout || "", - stderr: error.stderr || error.message, - success: false, - }; - - const functionResponse = await chat.sendMessage( - JSON.stringify(result) - ); - console.log("\n"); - console.log("Command failed:", result); - console.log("\nFunction response:"); - process.stdout.write(functionResponse.response.text()); + const functionCalls = response.response.functionCalls() || []; + + for (const functionCall of functionCalls) { + if (functionCall.name === "executeBashCommand") { + const args: any = functionCall.args; + const command = args.command; + // Ask for user confirmation + console.log(`\nBash command to execute:`); + console.log(chalk.yellow(command)); + const confirmation = await ask( + `\nExecute this Bash command? (y/n): ` + ); + + if (sayingOk(confirmation)) { + console.log(`\nExecuting bash command: ${command}`); + + try { + const { stdout, stderr } = await execPromise(command); + const result = { stdout, stderr, success: true }; + if (result.success) { + console.log(result.stdout); + } else { + console.log(result.stderr); } - } else { - const result = { - stdout: "", - stderr: "User declined to execute the command", - success: false, - }; - const functionResponse = await chat.sendMessage( + // Send the result back to the model + const functionResponse = await chat.sendMessageStream( JSON.stringify(result) ); - console.log("\nUser declined to execute the command"); - } - } else if (functionCall.name === "executeNodeCode") { - const args: any = functionCall.args; - const code = args.code; - - // Ask for user confirmation - // Display code with syntax highlighting and ask for confirmation - console.log("\nNode.js code to execute:"); - console.log(chalk.yellow(code)); - const confirmation = await ask( - `\nExecute this Node.js code? (y/n): ` - ); - - if (sayingOk(confirmation)) { - console.log(`\nExecuting Node.js code`); - - try { - // Execute the code - const tempDir = path.join(process.cwd(), "./"); - const { stdout, stderr } = await executeNodeJsCode( - code, - tempDir - ); - - const output = { - stdout, - stderr, - success: true, - }; - if (output.success) { - console.log(output.stdout); - } else { - console.log(chalk.red("Execution failed:"), output); - } - - // Send the result back to the model using streaming - const functionResponse = await chat.sendMessageStream( - JSON.stringify(output) - ); - console.log("\n"); - for await (const chunk of functionResponse.stream) { - process.stdout.write(chunk.text()); - } - } catch (error) { - const output = { - stdout: "", - stderr: error.message || String(error), - success: false, - }; - - const functionResponse = await chat.sendMessage( - JSON.stringify(output) - ); - console.log("Code execution failed:", output); - console.log("\n"); - process.stdout.write(functionResponse.response.text()); + console.log("\n"); + for await (const chunk of functionResponse.stream) { + process.stdout.write(chunk.text()); } - } else { + } catch (error) { const result = { - stdout: "", - stderr: "User declined to execute the code", + stdout: error.stdout || "", + stderr: error.stderr || error.message, success: false, }; + console.log("\n"); + console.log("Command failed:", result); - const functionResponse = await chat.sendMessageStream( - JSON.stringify(result) - ); - for await (const chunk of functionResponse.stream) { - process.stdout.write(chunk.text()); + nextQuestion = `命令执行过程当中失败: ${result.stderr}, 你能否改进一下方案?`; + } + } else { + nextQuestion = `用户拒绝了这条命令: (${confirmation}), 尝试改进一下方案.`; + } + } else if (functionCall.name === "executeNodeCode") { + const args: any = functionCall.args; + const code = args.code; + + // Ask for user confirmation + // Display code with syntax highlighting and ask for confirmation + console.log(chalk.gray("\nNode.js code to execute:")); + console.log(chalk.yellow(code)); + const confirmation = await ask( + `\nExecute this Node.js code? (y/n): ` + ); + + if (sayingOk(confirmation)) { + console.log(chalk.gray(`\nExecuting Node.js code`)); + + try { + // Execute the code + const tempDir = path.join(process.cwd(), "./"); + const { stdout, stderr } = await executeNodeJsCode(code, tempDir); + + const output = { + stdout, + stderr, + success: true, + }; + if (output.success) { + console.log(output.stdout); + } else { + console.log(chalk.red("Execution failed:"), output); } - console.log("\nUser declined to execute the code"); + + nextQuestion = JSON.stringify(output); + } catch (error) { + const output = { + stdout: "", + stderr: error.message || String(error), + success: false, + }; + console.log("\n"); + console.log("Code execution failed:", output); + nextQuestion = `Node.js 代码执行失败: ${output.stderr}, 你能否改进一下方案?`; } + } else { + nextQuestion = `用户拒绝了这段代码: (${confirmation}), 尝试改进一下方案.`; } } } - - console.log("\n"); } } catch (err) { - console.error("发生错误:", err); + console.error("Error:", err); rl.close(); process.exit(1); } finally { + console.log("Exiting..."); rl.close(); } }; + main(); From fde8d7a470443c174daa5db1c905eaf1eb8c744b Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 7 Apr 2025 01:51:14 +0800 Subject: [PATCH 04/12] fix loop branching of nextQuestion --- src/main.mts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main.mts b/src/main.mts index 17c08db..c85ddc4 100644 --- a/src/main.mts +++ b/src/main.mts @@ -138,7 +138,7 @@ const main = async () => { let nextQuestion: string = ""; - while (true) { + outerWhile: while (true) { const question = nextQuestion || (await ask("\nWhat's the task: ")); if (question.toLowerCase() === "exit") { @@ -149,17 +149,16 @@ const main = async () => { console.log(chalk.gray("\nResponding...\n")); // Use the chat API to send messages and get streaming responses - const response = await chat.sendMessage(question); - const textResponse = response.response.text(); - if (textResponse) { - process.stdout.write(textResponse); + const response = await chat.sendMessageStream(question); + for await (const chunk of response.stream) { + process.stdout.write(chunk.text()); } // clear the next question cache nextQuestion = ""; // Handle function calls if any - const functionCalls = response.response.functionCalls() || []; + const functionCalls = (await response.response).functionCalls() || []; for (const functionCall of functionCalls) { if (functionCall.name === "executeBashCommand") { @@ -184,14 +183,8 @@ const main = async () => { console.log(result.stderr); } - // Send the result back to the model - const functionResponse = await chat.sendMessageStream( - JSON.stringify(result) - ); - console.log("\n"); - for await (const chunk of functionResponse.stream) { - process.stdout.write(chunk.text()); - } + nextQuestion = JSON.stringify(result); + continue outerWhile; } catch (error) { const result = { stdout: error.stdout || "", @@ -202,9 +195,11 @@ const main = async () => { console.log("Command failed:", result); nextQuestion = `命令执行过程当中失败: ${result.stderr}, 你能否改进一下方案?`; + continue outerWhile; } } else { nextQuestion = `用户拒绝了这条命令: (${confirmation}), 尝试改进一下方案.`; + continue outerWhile; } } else if (functionCall.name === "executeNodeCode") { const args: any = functionCall.args; @@ -238,6 +233,7 @@ const main = async () => { } nextQuestion = JSON.stringify(output); + continue outerWhile; } catch (error) { const output = { stdout: "", @@ -247,9 +243,11 @@ const main = async () => { console.log("\n"); console.log("Code execution failed:", output); nextQuestion = `Node.js 代码执行失败: ${output.stderr}, 你能否改进一下方案?`; + continue outerWhile; } } else { nextQuestion = `用户拒绝了这段代码: (${confirmation}), 尝试改进一下方案.`; + continue outerWhile; } } } From 6a2a1c7a69369a480b537f340f05b69a1c5901a4 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 7 Apr 2025 11:05:30 +0800 Subject: [PATCH 05/12] Update model name to gemini-2.0-flash-lite --- src/main.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.mts b/src/main.mts index c85ddc4..3e8111d 100644 --- a/src/main.mts +++ b/src/main.mts @@ -20,7 +20,7 @@ const execFilePromise = promisify(execFile); const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel( // { model: "gemini-1.5-flash" }, - { model: "gemini-2.0-flash" }, + { model: "gemini-2.0-flash-lite" }, { baseUrl: "https://sf.chenyong.life" } ); From 015d85d6a06407f4698a96d700d55af3f47852b3 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 8 Apr 2025 01:57:43 +0800 Subject: [PATCH 06/12] wrap code that going to be excuted in box --- package.json | 3 ++- src/main.mts | 48 ++++++++++++++++++++++++++++++++++++++++-------- yarn.lock | 31 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f055fb5..9a05df8 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "dependencies": { "@calcit/procs": "^0.9.9", "@google/generative-ai": "^0.24.0", - "chalk": "^5.4.1" + "chalk": "^5.4.1", + "string-width": "^7.2.0" }, "scripts": { "compile-page": "cr --once js", diff --git a/src/main.mts b/src/main.mts index 3e8111d..b41511a 100644 --- a/src/main.mts +++ b/src/main.mts @@ -11,6 +11,7 @@ import fs from "fs/promises"; import { exec, execFile } from "child_process"; import { promisify } from "util"; import chalk from "chalk"; +import stringWidth from "string-width"; // Promisify functions const execPromise = promisify(exec); @@ -56,7 +57,12 @@ const rl = readline.createInterface({ output: process.stdout, }); -const ask = (question: string) => { +const ask = (question: string, seperator: boolean = false) => { + if (seperator) { + console.log( + chalk.gray("\n-------------------------------------------------") + ); + } return new Promise((resolve) => { rl.question(chalk.cyan(question), (answer) => { resolve(answer); @@ -108,6 +114,27 @@ const tools: Tool[] = [ }, ]; +const displayBoxedText = (text: string) => { + const lines = text.split("\n"); + const maxLength = lines.reduce( + (max: number, line: string) => Math.max(max, stringWidth(line)), + 0 + ); + const horizontalLine = "┌" + "─".repeat(maxLength + 2) + "┐"; + const verticalLine = "│ "; + + console.log(chalk.gray(horizontalLine)); + lines.forEach((line: string) => { + const displayLength = stringWidth(line); + console.log( + chalk.gray( + verticalLine + line + " ".repeat(maxLength - displayLength) + " │" + ) + ); + }); + console.log(chalk.gray("└" + "─".repeat(maxLength + 2) + "┘")); +}; + const main = async () => { try { // Create a chat session @@ -129,7 +156,10 @@ const main = async () => { role: "user", parts: [ { - text: "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复, 注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。", + text: + "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复" + + "注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。" + + "另外注意提早获取当前系统的信息, 方便后续的命令执行", }, ], }, @@ -139,10 +169,11 @@ const main = async () => { let nextQuestion: string = ""; outerWhile: while (true) { - const question = nextQuestion || (await ask("\nWhat's the task: ")); + const question = + nextQuestion || (await ask("\nWhat's the task: ", true)) || "继续"; if (question.toLowerCase() === "exit") { - console.log("Bye!"); + console.log("\nBye!"); break; } @@ -165,8 +196,10 @@ const main = async () => { const args: any = functionCall.args; const command = args.command; // Ask for user confirmation - console.log(`\nBash command to execute:`); - console.log(chalk.yellow(command)); + console.log(`\nBash command to execute:\n`); + + console.log(`\nBash command to execute:\n`); + displayBoxedText(command); const confirmation = await ask( `\nExecute this Bash command? (y/n): ` ); @@ -208,7 +241,7 @@ const main = async () => { // Ask for user confirmation // Display code with syntax highlighting and ask for confirmation console.log(chalk.gray("\nNode.js code to execute:")); - console.log(chalk.yellow(code)); + displayBoxedText(code); const confirmation = await ask( `\nExecute this Node.js code? (y/n): ` ); @@ -257,7 +290,6 @@ const main = async () => { rl.close(); process.exit(1); } finally { - console.log("Exiting..."); rl.close(); } }; diff --git a/yarn.lock b/yarn.lock index dec5a8a..2493d44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -268,6 +268,11 @@ dependencies: undici-types "~6.21.0" +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + bottom-tip@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/bottom-tip/-/bottom-tip-0.1.5.tgz#ca81e738fba6ae956a5b4c55a78a127820c9b99e" @@ -296,6 +301,11 @@ dom-walk@^0.1.0: resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + error@^4.3.0: version "4.4.0" resolved "https://registry.yarnpkg.com/error/-/error-4.4.0.tgz#bf69ff251fb4a279c19adccdaa6b61e90d9bf12a" @@ -348,6 +358,11 @@ fsevents@~2.3.2, fsevents@~2.3.3: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + global@^4.3.0: version "4.4.0" resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" @@ -456,6 +471,22 @@ string-template@~0.2.0: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== +string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + typescript@^5.8.3: version "5.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" From b9e00a8eceb6783b94b9334b2260d1d493da99d1 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 8 Apr 2025 19:42:24 +0800 Subject: [PATCH 07/12] include system info in early prompt --- src/main.mts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main.mts b/src/main.mts index b41511a..d8b38cf 100644 --- a/src/main.mts +++ b/src/main.mts @@ -5,10 +5,11 @@ import { SchemaType, ToolConfig, } from "@google/generative-ai"; +import os from "os"; import * as readline from "readline"; import path from "path"; import fs from "fs/promises"; -import { exec, execFile } from "child_process"; +import { exec, execFile, execFileSync, execSync } from "child_process"; import { promisify } from "util"; import chalk from "chalk"; import stringWidth from "string-width"; @@ -136,6 +137,12 @@ const displayBoxedText = (text: string) => { }; const main = async () => { + let osInfo = `${process.platform}, 架构: ${process.arch}, CPU 核心数: ${ + os.cpus().length + }.`; + let nodeInfo = `${process.version}, 当前目录: ${process.cwd()}.`; + let bashInfo = execSync("bash --version | head -n 1"); + try { // Create a chat session // Define a function declaration tool @@ -159,7 +166,8 @@ const main = async () => { text: "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复" + "注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。" + - "另外注意提早获取当前系统的信息, 方便后续的命令执行", + "另外注意提早获取当前系统的信息, 方便后续的命令执行." + + `当前系统信息: ${osInfo}, Node.js 信息: ${nodeInfo}, Bash 信息: ${bashInfo}`, }, ], }, @@ -205,7 +213,7 @@ const main = async () => { ); if (sayingOk(confirmation)) { - console.log(`\nExecuting bash command: ${command}`); + console.log(chalk.gray(`\nExecuting bash command...`)); try { const { stdout, stderr } = await execPromise(command); @@ -247,7 +255,7 @@ const main = async () => { ); if (sayingOk(confirmation)) { - console.log(chalk.gray(`\nExecuting Node.js code`)); + console.log(chalk.gray(`\nExecuting Node.js code...`)); try { // Execute the code From fa3281a5e7f5051a13ec12a23e43643d581a20ab Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 9 Apr 2025 02:13:42 +0800 Subject: [PATCH 08/12] steaming stdout output --- src/main.mts | 103 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 16 deletions(-) diff --git a/src/main.mts b/src/main.mts index d8b38cf..edb304a 100644 --- a/src/main.mts +++ b/src/main.mts @@ -27,7 +27,13 @@ const model = genAI.getGenerativeModel( ); // Define function to execute Node.js code -const executeNodeJsCode = async (code: string, tempDir: string) => { +const executeNodeJsCode = async ( + code: string, + tempDir: string +): Promise<{ + stdout: string; + stderr: string; +}> => { // Create temp directory if it doesn't exist try { await fs.mkdir(tempDir, { recursive: true }); @@ -38,12 +44,41 @@ const executeNodeJsCode = async (code: string, tempDir: string) => { // Create a temporary file with the code const tempFilePath = path.join(tempDir, `exec_${Date.now()}.mjs`); await fs.writeFile(tempFilePath, code); - // Execute the file as an ES module - const result = await execFilePromise("node", [ - "--experimental-modules", - tempFilePath, - ]); + const child = execFile("node", ["--experimental-modules", tempFilePath], { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs + }); + + let stdout = ""; + let stderr = ""; + + // Stream stdout in real-time while preserving color + child.stdout?.on("data", (data) => { + process.stdout.write(data); + stdout += data; + }); + + // Stream stderr in real-time while preserving color + child.stderr?.on("data", (data) => { + process.stderr.write(data); + stderr += data; + }); + + // Wait for process to complete + const result = await new Promise<{ stdout: string; stderr: string }>( + (resolve, reject) => { + child.on("close", (code) => { + if (code === 0 || code === null) { + resolve({ stdout, stderr }); + } else { + reject(new Error(`Process exited with code ${code}\n${stderr}`)); + } + }); + + child.on("error", reject); + } + ); // Clean up the temporary file await fs @@ -53,6 +88,44 @@ const executeNodeJsCode = async (code: string, tempDir: string) => { return result; }; +let execBash = async ( + command: string +): Promise<{ stdout: string; stderr: string }> => { + // Execute the command + const child = exec(command, { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs + }); + + let stdout = ""; + let stderr = ""; + + // Stream stdout in real-time while preserving colors + child.stdout?.on("data", (data) => { + process.stdout.write(data); + stdout += data; + }); + + // Stream stderr in real-time while preserving colors + child.stderr?.on("data", (data) => { + process.stderr.write(data); + stderr += data; + }); + + // Wait for process to complete + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { + child.on("close", (code) => { + if (code === 0 || code === null) { + resolve({ stdout, stderr }); + } else { + reject(new Error(`Process exited with code ${code}\n${stderr}`)); + } + }); + + child.on("error", reject); + }); +}; + const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -164,9 +237,9 @@ const main = async () => { parts: [ { text: - "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复" + - "注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。" + - "另外注意提早获取当前系统的信息, 方便后续的命令执行." + + "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复. " + + "注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。 " + + "另外注意提早获取当前系统的信息, 方便后续的命令执行. " + `当前系统信息: ${osInfo}, Node.js 信息: ${nodeInfo}, Bash 信息: ${bashInfo}`, }, ], @@ -204,8 +277,6 @@ const main = async () => { const args: any = functionCall.args; const command = args.command; // Ask for user confirmation - console.log(`\nBash command to execute:\n`); - console.log(`\nBash command to execute:\n`); displayBoxedText(command); const confirmation = await ask( @@ -216,12 +287,12 @@ const main = async () => { console.log(chalk.gray(`\nExecuting bash command...`)); try { - const { stdout, stderr } = await execPromise(command); + const { stdout, stderr } = await execBash(command); const result = { stdout, stderr, success: true }; if (result.success) { - console.log(result.stdout); + console.log(chalk.green("运行成功.")); } else { - console.log(result.stderr); + console.log(chalk.red("运行失败\n" + result.stderr)); } nextQuestion = JSON.stringify(result); @@ -268,9 +339,9 @@ const main = async () => { success: true, }; if (output.success) { - console.log(output.stdout); + console.log(chalk.green("运行成功.")); } else { - console.log(chalk.red("Execution failed:"), output); + console.log(chalk.red("运行失败\n" + output.stderr)); } nextQuestion = JSON.stringify(output); From d62398ba5796e8e6d88f4e09b0d6d0f9f6d87ece Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 9 Apr 2025 02:13:53 +0800 Subject: [PATCH 09/12] insert context reminder --- src/main.mts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main.mts b/src/main.mts index edb304a..99cd544 100644 --- a/src/main.mts +++ b/src/main.mts @@ -126,6 +126,16 @@ let execBash = async ( }); }; +// 添加一个函数来生成上下文提醒 +const getContextReminder = () => { + return ( + "提醒: 你是一个命令行助手。你可以:\n" + + "1. 使用 executeBashCommand 执行 bash 命令\n" + + "2. 使用 executeNodeCode 执行 Node.js 代码\n" + + "请在每次回答时都考虑使用这些工具来帮助用户。" + ); +}; + const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -237,10 +247,16 @@ const main = async () => { parts: [ { text: - "你现在是一个命令行助手, 基于当前进程所在的目录工作. 每次回答都可以考虑调用 executeBashCommand 或者 executeNodeCode 收集更详细的信息来回复. " + - "注意结合上下文来理解意思以便失败的时候重试. 使用中文回复我的指令,代码保留英文。 " + - "另外注意提早获取当前系统的信息, 方便后续的命令执行. " + - `当前系统信息: ${osInfo}, Node.js 信息: ${nodeInfo}, Bash 信息: ${bashInfo}`, + "系统初始化配置:\n" + + "1. 你是一个专业的命令行助手,工作在当前进程目录下\n" + + "2. 你可以调用 executeBashCommand 执行 bash 命令\n" + + "3. 你可以调用 executeNodeCode 执行 Node.js 代码\n" + + "4. 每次回答都应该考虑使用这些工具收集信息\n" + + "5. 使用中文回复,但代码保持英文\n" + + "6. 失败时要理解上下文并优化重试\n" + + `当前系统信息: ${osInfo}\n` + + `Node.js 信息: ${nodeInfo}\n` + + `Bash 信息: ${bashInfo}`, }, ], }, @@ -248,15 +264,23 @@ const main = async () => { }); let nextQuestion: string = ""; + let messageCount = 0; outerWhile: while (true) { - const question = + let question = nextQuestion || (await ask("\nWhat's the task: ", true)) || "继续"; if (question.toLowerCase() === "exit") { console.log("\nBye!"); break; } + // 每隔 5 轮对话,插入上下文提醒 + messageCount++; + if (messageCount % 10 === 0) { + const reminder = getContextReminder(); + console.log(chalk.gray("\n\n" + reminder)); + question = `${reminder}\n\n${question}`; + } console.log(chalk.gray("\nResponding...\n")); From ef8d6d9f835ac7add05c04fc6998b2b75eaf96fe Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 10 Apr 2025 01:10:30 +0800 Subject: [PATCH 10/12] some abstract over tool in order to add more file tools --- src/exec.mts | 103 +++++++++++++++++ src/main.mts | 317 ++++++++++++++++++++++----------------------------- src/util.mts | 23 ++++ 3 files changed, 260 insertions(+), 183 deletions(-) create mode 100644 src/exec.mts create mode 100644 src/util.mts diff --git a/src/exec.mts b/src/exec.mts new file mode 100644 index 0000000..868dd05 --- /dev/null +++ b/src/exec.mts @@ -0,0 +1,103 @@ +import { exec, execFile } from "child_process"; +import path from "path"; +import fs from "fs/promises"; + +// Define function to execute Node.js code +export const executeNodeJsCode = async ( + code: string, + tempDir: string +): Promise<{ + stdout: string; + stderr: string; +}> => { + // Create temp directory if it doesn't exist + try { + await fs.mkdir(tempDir, { recursive: true }); + } catch (err) { + // Directory might already exist + } + + // Create a temporary file with the code + const tempFilePath = path.join(tempDir, `exec_${Date.now()}.mjs`); + await fs.writeFile(tempFilePath, code); + // Execute the file as an ES module + const child = execFile("node", ["--experimental-modules", tempFilePath], { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs + }); + + let stdout = ""; + let stderr = ""; + + // Stream stdout in real-time while preserving color + child.stdout?.on("data", (data) => { + process.stdout.write(data); + stdout += data; + }); + + // Stream stderr in real-time while preserving color + child.stderr?.on("data", (data) => { + process.stderr.write(data); + stderr += data; + }); + + // Wait for process to complete + const result = await new Promise<{ stdout: string; stderr: string }>( + (resolve, reject) => { + child.on("close", (code) => { + if (code === 0 || code === null) { + resolve({ stdout, stderr }); + } else { + reject(new Error(`Process exited with code ${code}\n${stderr}`)); + } + }); + + child.on("error", reject); + } + ); + + // Clean up the temporary file + await fs + .unlink(tempFilePath) + .catch((err) => console.error("Failed to delete temp file:", err)); + + return result; +}; + +export let execBash = async ( + command: string +): Promise<{ stdout: string; stderr: string }> => { + // Execute the command + const child = exec(command, { + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs + }); + + let stdout = ""; + let stderr = ""; + + // Stream stdout in real-time while preserving colors + child.stdout?.on("data", (data) => { + process.stdout.write(data); + stdout += data; + }); + + // Stream stderr in real-time while preserving colors + child.stderr?.on("data", (data) => { + process.stderr.write(data); + stderr += data; + }); + + // Wait for process to complete + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { + child.on("close", (code) => { + if (code === 0 || code === null) { + resolve({ stdout, stderr }); + } else { + reject(new Error(`Process exited with code ${code}\n${stderr}`)); + } + }); + + child.on("error", reject); + }); +}; diff --git a/src/main.mts b/src/main.mts index 99cd544..fed1bf1 100644 --- a/src/main.mts +++ b/src/main.mts @@ -7,16 +7,12 @@ import { } from "@google/generative-ai"; import os from "os"; import * as readline from "readline"; -import path from "path"; -import fs from "fs/promises"; -import { exec, execFile, execFileSync, execSync } from "child_process"; -import { promisify } from "util"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { execSync } from "child_process"; import chalk from "chalk"; -import stringWidth from "string-width"; - -// Promisify functions -const execPromise = promisify(exec); -const execFilePromise = promisify(execFile); +import { execBash, executeNodeJsCode } from "./exec.mjs"; +import { displayBoxedText } from "./util.mjs"; // Initialize the Generative AI client const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); @@ -26,112 +22,14 @@ const model = genAI.getGenerativeModel( { baseUrl: "https://sf.chenyong.life" } ); -// Define function to execute Node.js code -const executeNodeJsCode = async ( - code: string, - tempDir: string -): Promise<{ - stdout: string; - stderr: string; -}> => { - // Create temp directory if it doesn't exist - try { - await fs.mkdir(tempDir, { recursive: true }); - } catch (err) { - // Directory might already exist - } - - // Create a temporary file with the code - const tempFilePath = path.join(tempDir, `exec_${Date.now()}.mjs`); - await fs.writeFile(tempFilePath, code); - // Execute the file as an ES module - const child = execFile("node", ["--experimental-modules", tempFilePath], { - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs - }); - - let stdout = ""; - let stderr = ""; - - // Stream stdout in real-time while preserving color - child.stdout?.on("data", (data) => { - process.stdout.write(data); - stdout += data; - }); - - // Stream stderr in real-time while preserving color - child.stderr?.on("data", (data) => { - process.stderr.write(data); - stderr += data; - }); - - // Wait for process to complete - const result = await new Promise<{ stdout: string; stderr: string }>( - (resolve, reject) => { - child.on("close", (code) => { - if (code === 0 || code === null) { - resolve({ stdout, stderr }); - } else { - reject(new Error(`Process exited with code ${code}\n${stderr}`)); - } - }); - - child.on("error", reject); - } - ); - - // Clean up the temporary file - await fs - .unlink(tempFilePath) - .catch((err) => console.error("Failed to delete temp file:", err)); - - return result; -}; - -let execBash = async ( - command: string -): Promise<{ stdout: string; stderr: string }> => { - // Execute the command - const child = exec(command, { - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs - }); - - let stdout = ""; - let stderr = ""; - - // Stream stdout in real-time while preserving colors - child.stdout?.on("data", (data) => { - process.stdout.write(data); - stdout += data; - }); - - // Stream stderr in real-time while preserving colors - child.stderr?.on("data", (data) => { - process.stderr.write(data); - stderr += data; - }); - - // Wait for process to complete - return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { - child.on("close", (code) => { - if (code === 0 || code === null) { - resolve({ stdout, stderr }); - } else { - reject(new Error(`Process exited with code ${code}\n${stderr}`)); - } - }); - - child.on("error", reject); - }); -}; - // 添加一个函数来生成上下文提醒 const getContextReminder = () => { return ( "提醒: 你是一个命令行助手。你可以:\n" + "1. 使用 executeBashCommand 执行 bash 命令\n" + "2. 使用 executeNodeCode 执行 Node.js 代码\n" + + "3. 使用 saveToFile 保存输出到文件\n" + + "4. 使用 readTextFile 读取文件内容\n" + "请在每次回答时都考虑使用这些工具来帮助用户。" ); }; @@ -194,29 +92,122 @@ const tools: Tool[] = [ required: ["code"], }, }, + { + name: "saveToFile", + description: + "save the output to a path based on the current working directory", + parameters: { + type: SchemaType.OBJECT, + properties: { + code: { + type: SchemaType.STRING, + description: "any code that you want to save to file", + }, + filepath: { + type: SchemaType.STRING, + description: + "the path to save the file, relative to the current working directory", + }, + }, + }, + }, + { + name: "readTextFile", + description: + "read a text file and return the content(utf8). it can access the file system. also called '读取文本文件'", + parameters: { + type: SchemaType.OBJECT, + properties: { + filepath: { + type: SchemaType.STRING, + description: + "the path to read, relative to the current working directory", + }, + }, + }, + }, ], }, ]; -const displayBoxedText = (text: string) => { - const lines = text.split("\n"); - const maxLength = lines.reduce( - (max: number, line: string) => Math.max(max, stringWidth(line)), - 0 - ); - const horizontalLine = "┌" + "─".repeat(maxLength + 2) + "┐"; - const verticalLine = "│ "; - - console.log(chalk.gray(horizontalLine)); - lines.forEach((line: string) => { - const displayLength = stringWidth(line); - console.log( - chalk.gray( - verticalLine + line + " ".repeat(maxLength - displayLength) + " │" - ) - ); - }); - console.log(chalk.gray("└" + "─".repeat(maxLength + 2) + "┘")); +let toolsDict: Record< + string, + { + shortName: string; + toolFn: (args: any) => Promise; + previewFn: (args: any) => void; + } +> = { + executeBashCommand: { + shortName: "Bash", + toolFn: async (args: any) => { + const command = args.command; + return await execBash(command); + }, + previewFn: (args: any) => { + displayBoxedText(args.command); + }, + }, + executeNodeCode: { + toolFn: async (args: any) => { + const code = args.code; + const tempDir = path.join(process.cwd(), "./"); + return await executeNodeJsCode(code, tempDir); + }, + shortName: "Node.js", + previewFn: (args: any) => { + displayBoxedText(args.code); + }, + }, + saveToFile: { + shortName: "Saving", + previewFn: (args: any) => { + displayBoxedText( + `Saving to ${args.path}:\n-------------\n${args.code}...` + ); + }, + toolFn: async (args: any) => { + const code = args.code; + const filepath = args.filepath as string; + const filePath = filepath.startsWith("/") + ? filepath + : path.join("./", filepath); + await fs.writeFile(filePath, code); + return { + stdout: `File saved to ${filePath}`, + stderr: "", + success: true, + }; + }, + }, + readTextFile: { + shortName: "Read File", + previewFn: (args: any) => { + displayBoxedText(`Reading file ${args.filepath}`); + }, + toolFn: async (args: any) => { + const filepath = args.filepath as string; + const filePath = filepath.startsWith("/") + ? filepath + : path.join("./", filepath); + try { + console.log(`Reading file ${filePath}`); + const data = await fs.readFile(filePath, "utf8"); + console.log(data); + return { + stdout: data, + stderr: "", + success: true, + }; + } catch (error) { + return { + stdout: "", + stderr: `Error reading file ${filePath}: ${error.message}`, + success: false, + }; + } + }, + }, }; const main = async () => { @@ -297,21 +288,21 @@ const main = async () => { const functionCalls = (await response.response).functionCalls() || []; for (const functionCall of functionCalls) { - if (functionCall.name === "executeBashCommand") { - const args: any = functionCall.args; - const command = args.command; + let tool = toolsDict[functionCall.name]; + const args: any = functionCall.args; + if (tool) { // Ask for user confirmation - console.log(`\nBash command to execute:\n`); - displayBoxedText(command); + console.log(`\n${tool.shortName} to execute:\n`); + tool.previewFn(args); const confirmation = await ask( - `\nExecute this Bash command? (y/n): ` + `\nExecute this ${tool.shortName} script? (y/n): ` ); if (sayingOk(confirmation)) { - console.log(chalk.gray(`\nExecuting bash command...`)); + console.log(chalk.gray(`\nExecuting ${tool.shortName} command...`)); try { - const { stdout, stderr } = await execBash(command); + const { stdout, stderr } = await tool.toolFn(args); const result = { stdout, stderr, success: true }; if (result.success) { console.log(chalk.green("运行成功.")); @@ -337,54 +328,14 @@ const main = async () => { nextQuestion = `用户拒绝了这条命令: (${confirmation}), 尝试改进一下方案.`; continue outerWhile; } - } else if (functionCall.name === "executeNodeCode") { - const args: any = functionCall.args; - const code = args.code; - - // Ask for user confirmation - // Display code with syntax highlighting and ask for confirmation - console.log(chalk.gray("\nNode.js code to execute:")); - displayBoxedText(code); - const confirmation = await ask( - `\nExecute this Node.js code? (y/n): ` + } else { + console.log( + chalk.red( + `\n\nError: Unsupported function call ${functionCall.name}` + ) ); - - if (sayingOk(confirmation)) { - console.log(chalk.gray(`\nExecuting Node.js code...`)); - - try { - // Execute the code - const tempDir = path.join(process.cwd(), "./"); - const { stdout, stderr } = await executeNodeJsCode(code, tempDir); - - const output = { - stdout, - stderr, - success: true, - }; - if (output.success) { - console.log(chalk.green("运行成功.")); - } else { - console.log(chalk.red("运行失败\n" + output.stderr)); - } - - nextQuestion = JSON.stringify(output); - continue outerWhile; - } catch (error) { - const output = { - stdout: "", - stderr: error.message || String(error), - success: false, - }; - console.log("\n"); - console.log("Code execution failed:", output); - nextQuestion = `Node.js 代码执行失败: ${output.stderr}, 你能否改进一下方案?`; - continue outerWhile; - } - } else { - nextQuestion = `用户拒绝了这段代码: (${confirmation}), 尝试改进一下方案.`; - continue outerWhile; - } + nextQuestion = `不支持的函数调用: ${functionCall.name}, 你能否改进一下方案?`; + continue outerWhile; } } } diff --git a/src/util.mts b/src/util.mts new file mode 100644 index 0000000..702c7be --- /dev/null +++ b/src/util.mts @@ -0,0 +1,23 @@ +import stringWidth from "string-width"; +import chalk from "chalk"; + +export const displayBoxedText = (text: string) => { + const lines = text.split("\n"); + const maxLength = lines.reduce( + (max: number, line: string) => Math.max(max, stringWidth(line)), + 0 + ); + const horizontalLine = "┌" + "─".repeat(maxLength + 2) + "┐"; + const verticalLine = "│ "; + + console.log(chalk.gray(horizontalLine)); + lines.forEach((line: string) => { + const displayLength = stringWidth(line); + console.log( + chalk.gray( + verticalLine + line + " ".repeat(maxLength - displayLength) + " │" + ) + ); + }); + console.log(chalk.gray("└" + "─".repeat(maxLength + 2) + "┘")); +}; From 561208dbc876492a6aacd070cc9cc355c9469252 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 10 Apr 2025 01:30:34 +0800 Subject: [PATCH 11/12] include newer google ai sdk to use ground search --- package.json | 1 + src/main.mts | 69 ++++++++++++++++++++ yarn.lock | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) diff --git a/package.json b/package.json index 9a05df8..b26b05f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "macrophylla", "dependencies": { "@calcit/procs": "^0.9.9", + "@google/genai": "^0.8.0", "@google/generative-ai": "^0.24.0", "chalk": "^5.4.1", "string-width": "^7.2.0" diff --git a/src/main.mts b/src/main.mts index fed1bf1..fd24935 100644 --- a/src/main.mts +++ b/src/main.mts @@ -13,9 +13,12 @@ import { execSync } from "child_process"; import chalk from "chalk"; import { execBash, executeNodeJsCode } from "./exec.mjs"; import { displayBoxedText } from "./util.mjs"; +import { GoogleGenAI } from "@google/genai"; // Initialize the Generative AI client const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); +const aiNew = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! }); + const model = genAI.getGenerativeModel( // { model: "gemini-1.5-flash" }, { model: "gemini-2.0-flash-lite" }, @@ -30,6 +33,7 @@ const getContextReminder = () => { "2. 使用 executeNodeCode 执行 Node.js 代码\n" + "3. 使用 saveToFile 保存输出到文件\n" + "4. 使用 readTextFile 读取文件内容\n" + + "5. 使用 groundSearch 搜索最新信息\n" + "请在每次回答时都考虑使用这些工具来帮助用户。" ); }; @@ -109,6 +113,7 @@ const tools: Tool[] = [ "the path to save the file, relative to the current working directory", }, }, + required: ["code", "filepath"], }, }, { @@ -124,6 +129,22 @@ const tools: Tool[] = [ "the path to read, relative to the current working directory", }, }, + required: ["filepath"], + }, + }, + { + name: "groundSearch", + description: + "search the web for the latest information, with gemini groundSearch", + parameters: { + type: SchemaType.OBJECT, + properties: { + query: { + type: SchemaType.STRING, + description: "The search query", + }, + }, + required: ["query"], }, }, ], @@ -208,6 +229,54 @@ let toolsDict: Record< } }, }, + groundSearch: { + shortName: "Ground Search", + previewFn: (args: any) => { + displayBoxedText(`Searching the web for ${args.query}`); + }, + toolFn: async (args: any) => { + const query = args.query as string; + console.log(`Searching the web for ${query}`); + + try { + const response = await aiNew.models.generateContent({ + model: "gemini-2.0-flash", + contents: [query], + config: { + tools: [{ googleSearch: {} }], + httpOptions: { + baseUrl: "https://sf.chenyong.life", + }, + }, + }); + let result = response.text; + console.log(chalk.gray(result)); + // To get grounding metadata as web content. + // response?.candidates?.[0].groundingMetadata?.searchEntryPoint + // ?.renderedContent; + if (result) { + return { + stdout: result, + stderr: "", + success: true, + }; + } else { + return { + stdout: "", + stderr: "No result found.", + success: false, + }; + } + } catch (err) { + console.error("Error:", err); + return { + stdout: "", + stderr: `Error searching the web: ${err.message}`, + success: false, + }; + } + }, + }, }; const main = async () => { diff --git a/yarn.lock b/yarn.lock index 2493d44..6f1e28f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -151,6 +151,14 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz#839f72c2decd378f86b8f525e1979a97b920c67d" integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA== +"@google/genai@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@google/genai/-/genai-0.8.0.tgz#caf753288fe0123ab5cfca1f5f8613cc840e0029" + integrity sha512-Zs+OGyZKyMbFofGJTR9/jTQSv8kITh735N3tEuIZj4VlMQXTC0soCFahysJ9NaeenRlD7xGb6fyqmX+FwrpU6Q== + dependencies: + google-auth-library "^9.14.2" + ws "^8.18.0" + "@google/generative-ai@^0.24.0": version "0.24.0" resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.0.tgz#4d27af7d944c924a27a593c17ad1336535d53846" @@ -268,11 +276,26 @@ dependencies: undici-types "~6.21.0" +agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + ansi-regex@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== +base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bignumber.js@^9.0.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.2.1.tgz#3ad0854ad933560a25bbc7c93bc3b7ea6edcad85" + integrity sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw== + bottom-tip@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/bottom-tip/-/bottom-tip-0.1.5.tgz#ca81e738fba6ae956a5b4c55a78a127820c9b99e" @@ -286,6 +309,11 @@ browser-split@0.0.1: resolved "https://registry.yarnpkg.com/browser-split/-/browser-split-0.0.1.tgz#7b097574f8e3ead606fb4664e64adfdda2981a93" integrity sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + camelize@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" @@ -296,11 +324,25 @@ chalk@^5.4.1: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== +debug@4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + emoji-regex@^10.3.0: version "10.4.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" @@ -353,11 +395,36 @@ ev-store@^7.0.0: dependencies: individual "^3.0.0" +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + +gcp-metadata@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" + integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== + dependencies: + gaxios "^6.1.1" + google-logging-utils "^0.0.2" + json-bigint "^1.0.0" + get-east-asian-width@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" @@ -371,6 +438,39 @@ global@^4.3.0: min-document "^2.19.0" process "^0.11.10" +google-auth-library@^9.14.2: + version "9.15.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + +google-logging-utils@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" + integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== + +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + individual@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/individual/-/individual-3.0.0.tgz#e7ca4f85f8957b018734f285750dc22ec2f9862d" @@ -381,6 +481,35 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -388,6 +517,11 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nanoid@^3.3.8: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" @@ -403,6 +537,13 @@ next-tick@^0.2.2: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-0.2.2.tgz#75da4a927ee5887e39065880065b7336413b310d" integrity sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q== +node-fetch@^2.6.9: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -461,6 +602,11 @@ rollup@^4.30.1: "@rollup/rollup-win32-x64-msvc" "4.39.0" fsevents "~2.3.2" +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" @@ -487,6 +633,11 @@ strip-ansi@^7.1.0: dependencies: ansi-regex "^6.0.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + typescript@^5.8.3: version "5.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" @@ -505,6 +656,11 @@ url-parse@^1.5.10: querystringify "^2.1.1" requires-port "^1.0.0" +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + virtual-dom@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/virtual-dom/-/virtual-dom-2.1.1.tgz#80eda2d481b9ede0c049118cefcb4a05f21d1375" @@ -530,6 +686,24 @@ vite@^6.2.5: optionalDependencies: fsevents "~2.3.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +ws@^8.18.0: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + x-is-array@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/x-is-array/-/x-is-array-0.1.0.tgz#de520171d47b3f416f5587d629b89d26b12dc29d" From 87dfe83140b45bc2d254606458df5be86dc013a0 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 10 Apr 2025 01:34:46 +0800 Subject: [PATCH 12/12] clear exec tmp file earlier --- src/exec.mts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/exec.mts b/src/exec.mts index 868dd05..8b47d93 100644 --- a/src/exec.mts +++ b/src/exec.mts @@ -45,6 +45,11 @@ export const executeNodeJsCode = async ( const result = await new Promise<{ stdout: string; stderr: string }>( (resolve, reject) => { child.on("close", (code) => { + // Clean up the temporary file, asynchronously + fs.unlink(tempFilePath).catch((err) => + console.error("Failed to delete temp file:", err) + ); + if (code === 0 || code === null) { resolve({ stdout, stderr }); } else { @@ -56,11 +61,6 @@ export const executeNodeJsCode = async ( } ); - // Clean up the temporary file - await fs - .unlink(tempFilePath) - .catch((err) => console.error("Failed to delete temp file:", err)); - return result; };