From 059f16a9e8d89618f58f065c046cbbfabf0212a9 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Thu, 4 Dec 2025 07:01:17 -0600 Subject: [PATCH] # CHANGES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added ZIP export/import functionality for playbooks sharing 🎁 • Implemented per-tab token accumulation for accurate context tracking 📊 • Added loop summary history entries with detailed metrics 🔄 • Fixed markdown paragraph rendering in nested list items 📝 • Added export/import buttons to playbook management UI 💾 • Enhanced batch processor with per-loop token tracking 📈 • Added human-readable duration formatting for loop summaries ⏱️ • Improved debug logging for document reset-on-completion flow 🐛 • Added "Save as New" option for modified playbooks 💫 • Fixed context window percentage calculation using accumulated tokens 🎯 --- package-lock.json | 694 ++++++++++++++++--- package.json | 4 + src/main/index.ts | 175 +++++ src/main/preload.ts | 4 + src/renderer/App.tsx | 49 +- src/renderer/components/BatchRunnerModal.tsx | 71 +- src/renderer/components/HistoryPanel.tsx | 32 +- src/renderer/components/TerminalOutput.tsx | 30 +- src/renderer/global.d.ts | 2 + src/renderer/hooks/useBatchProcessor.ts | 95 ++- src/renderer/types/index.ts | 2 +- 11 files changed, 1018 insertions(+), 140 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed69f823..84aeee62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,9 @@ "@fastify/static": "^7.0.4", "@fastify/websocket": "^9.0.0", "@types/dompurify": "^3.0.5", + "adm-zip": "^0.5.16", "ansi-to-html": "^0.7.2", + "archiver": "^7.0.1", "canvas-confetti": "^1.9.4", "diff": "^8.0.2", "dompurify": "^3.3.0", @@ -36,6 +38,8 @@ "ws": "^8.16.0" }, "devDependencies": { + "@types/adm-zip": "^0.5.7", + "@types/archiver": "^7.0.0", "@types/canvas-confetti": "^1.9.0", "@types/node": "^20.10.6", "@types/qrcode": "^1.5.6", @@ -2045,6 +2049,26 @@ "node": ">= 10" } }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2528,6 +2552,16 @@ "@types/react": "*" } }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -2630,6 +2664,18 @@ "dev": true, "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -2648,6 +2694,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2940,77 +2995,197 @@ "license": "ISC" }, "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "license": "MIT", "dependencies": { - "archiver-utils": "^2.1.0", + "archiver-utils": "^5.0.2", "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "license": "MIT", "dependencies": { - "glob": "^7.1.4", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", + "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" + "node_modules/archiver/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, "node_modules/are-we-there-yet": { @@ -3068,7 +3243,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { @@ -3180,6 +3354,20 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3994,19 +4182,59 @@ } }, "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "license": "MIT", "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/concat-map": { @@ -4221,7 +4449,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/cose-base": { @@ -4248,7 +4475,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" @@ -4258,17 +4484,56 @@ } }, "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "license": "MIT", "dependencies": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/cross-spawn": { @@ -5369,6 +5634,93 @@ "fs-extra": "^10.1.0" } }, + "node_modules/electron-builder-squirrel-windows/node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -5397,6 +5749,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/electron-builder-squirrel-windows/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-builder-squirrel-windows/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -5407,6 +5776,43 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-builder-squirrel-windows/node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -5834,6 +6240,33 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5913,6 +6346,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -6599,7 +7038,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/hachure-fill": { @@ -6919,7 +7357,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -7188,6 +7625,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -7205,7 +7654,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { @@ -7451,7 +7899,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, "license": "MIT", "dependencies": { "readable-stream": "^2.0.5" @@ -7464,7 +7911,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -7480,14 +7926,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -8770,7 +9214,6 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -9265,7 +9708,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9945,11 +10387,19 @@ "node": ">=6" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/process-warning": { @@ -10400,7 +10850,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "minimatch": "^5.1.0" @@ -11223,6 +11672,17 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11558,6 +12018,29 @@ "node": ">= 10.0.0" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -12354,40 +12837,57 @@ } }, "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "license": "MIT", "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/zwitch": { diff --git a/package.json b/package.json index 0b9bbd15..3cc5260f 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,9 @@ "@fastify/static": "^7.0.4", "@fastify/websocket": "^9.0.0", "@types/dompurify": "^3.0.5", + "adm-zip": "^0.5.16", "ansi-to-html": "^0.7.2", + "archiver": "^7.0.1", "canvas-confetti": "^1.9.4", "diff": "^8.0.2", "dompurify": "^3.3.0", @@ -120,6 +122,8 @@ "ws": "^8.16.0" }, "devDependencies": { + "@types/adm-zip": "^0.5.7", + "@types/archiver": "^7.0.0", "@types/canvas-confetti": "^1.9.0", "@types/node": "^20.10.6", "@types/qrcode": "^1.5.6", diff --git a/src/main/index.ts b/src/main/index.ts index a88cf757..051802a9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,6 +3,9 @@ import path from 'path'; import os from 'os'; import crypto from 'crypto'; import fs from 'fs/promises'; +import { createWriteStream } from 'fs'; +import archiver from 'archiver'; +import AdmZip from 'adm-zip'; import { ProcessManager } from './process-manager'; import { WebServer } from './web-server'; import { AgentDetector } from './agent-detector'; @@ -4038,6 +4041,178 @@ function setupIpcHandlers() { return { success: false, error: String(error) }; } }); + + // Export a playbook as a ZIP file + ipcMain.handle( + 'playbooks:export', + async ( + _event, + sessionId: string, + playbookId: string, + autoRunFolderPath: string + ): Promise<{ success: boolean; filePath?: string; error?: string }> => { + try { + const playbooks = await readPlaybooks(sessionId); + const playbook = playbooks.find((p: any) => p.id === playbookId); + + if (!playbook) { + return { success: false, error: 'Playbook not found' }; + } + + // Show save dialog + const result = await dialog.showSaveDialog(mainWindow!, { + title: 'Export Playbook', + defaultPath: `${playbook.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.maestro-playbook.zip`, + filters: [ + { name: 'Maestro Playbook', extensions: ['maestro-playbook.zip'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (result.canceled || !result.filePath) { + return { success: false, error: 'Export cancelled' }; + } + + const zipPath = result.filePath; + + // Create ZIP archive + const output = createWriteStream(zipPath); + const archive = archiver('zip', { zlib: { level: 9 } }); + + // Wait for archive to finish + const archivePromise = new Promise((resolve, reject) => { + output.on('close', () => resolve()); + archive.on('error', (err) => reject(err)); + }); + + archive.pipe(output); + + // Create manifest JSON (playbook settings without the id - will be regenerated on import) + const manifest = { + version: 1, + name: playbook.name, + documents: playbook.documents, + loopEnabled: playbook.loopEnabled, + maxLoops: playbook.maxLoops, + prompt: playbook.prompt, + worktreeSettings: playbook.worktreeSettings, + exportedAt: Date.now() + }; + + // Add manifest to archive + archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' }); + + // Add each document markdown file + for (const doc of playbook.documents) { + const docPath = path.join(autoRunFolderPath, `${doc.filename}.md`); + try { + const content = await fs.readFile(docPath, 'utf-8'); + archive.append(content, { name: `documents/${doc.filename}.md` }); + } catch (err) { + // Document file doesn't exist, skip it but log warning + logger.warn(`Document ${doc.filename}.md not found during export`, 'Playbooks'); + } + } + + // Finalize archive + await archive.finalize(); + await archivePromise; + + logger.info(`Exported playbook "${playbook.name}" to ${zipPath}`, 'Playbooks'); + return { success: true, filePath: zipPath }; + } catch (error) { + logger.error('Error exporting playbook', 'Playbooks', error); + return { success: false, error: String(error) }; + } + } + ); + + // Import a playbook from a ZIP file + ipcMain.handle( + 'playbooks:import', + async ( + _event, + sessionId: string, + autoRunFolderPath: string + ): Promise<{ success: boolean; playbook?: any; importedDocs?: string[]; error?: string }> => { + try { + // Show open dialog + const result = await dialog.showOpenDialog(mainWindow!, { + title: 'Import Playbook', + filters: [ + { name: 'Maestro Playbook', extensions: ['maestro-playbook.zip', 'zip'] }, + { name: 'All Files', extensions: ['*'] } + ], + properties: ['openFile'] + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, error: 'Import cancelled' }; + } + + const zipPath = result.filePaths[0]; + + // Read ZIP file + const zip = new AdmZip(zipPath); + const zipEntries = zip.getEntries(); + + // Find and parse manifest + const manifestEntry = zipEntries.find(e => e.entryName === 'manifest.json'); + if (!manifestEntry) { + return { success: false, error: 'Invalid playbook file: missing manifest.json' }; + } + + const manifest = JSON.parse(manifestEntry.getData().toString('utf-8')); + + // Validate manifest + if (!manifest.name || !Array.isArray(manifest.documents)) { + return { success: false, error: 'Invalid playbook manifest' }; + } + + // Extract document files to autorun folder + const importedDocs: string[] = []; + for (const entry of zipEntries) { + if (entry.entryName.startsWith('documents/') && entry.entryName.endsWith('.md')) { + const filename = path.basename(entry.entryName); + const destPath = path.join(autoRunFolderPath, filename); + + // Ensure autorun folder exists + await fs.mkdir(autoRunFolderPath, { recursive: true }); + + // Write document file + await fs.writeFile(destPath, entry.getData().toString('utf-8'), 'utf-8'); + importedDocs.push(filename.replace('.md', '')); + } + } + + // Create new playbook entry + const playbooks = await readPlaybooks(sessionId); + const now = Date.now(); + + const newPlaybook = { + id: crypto.randomUUID(), + name: manifest.name, + createdAt: now, + updatedAt: now, + documents: manifest.documents, + loopEnabled: manifest.loopEnabled ?? false, + maxLoops: manifest.maxLoops, + prompt: manifest.prompt || '', + worktreeSettings: manifest.worktreeSettings + }; + + // Add to list and save + playbooks.push(newPlaybook); + await writePlaybooks(sessionId, playbooks); + + logger.info(`Imported playbook "${manifest.name}" with ${importedDocs.length} documents`, 'Playbooks'); + return { success: true, playbook: newPlaybook, importedDocs }; + } catch (error) { + logger.error('Error importing playbook', 'Playbooks', error); + return { success: false, error: String(error) }; + } + } + ); } // Handle process output streaming (set up after initialization) diff --git a/src/main/preload.ts b/src/main/preload.ts index 74def907..7b46ba93 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -543,6 +543,10 @@ contextBridge.exposeInMainWorld('maestro', { ) => ipcRenderer.invoke('playbooks:update', sessionId, playbookId, updates), delete: (sessionId: string, playbookId: string) => ipcRenderer.invoke('playbooks:delete', sessionId, playbookId), + export: (sessionId: string, playbookId: string, autoRunFolderPath: string) => + ipcRenderer.invoke('playbooks:export', sessionId, playbookId, autoRunFolderPath), + import: (sessionId: string, autoRunFolderPath: string) => + ipcRenderer.invoke('playbooks:import', sessionId, autoRunFolderPath), }, }); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9dd1a3cf..960134b8 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1286,46 +1286,55 @@ export default function MaestroConsole() { setSessions(prev => prev.map(s => { if (s.id !== actualSessionId) return s; - // Calculate context window usage percentage - // For a conversation, context contains all inputs and outputs - // inputTokens = full input token count (already includes cache hits) - // outputTokens = response tokens (become part of context in follow-up turns) - // Note: cache tokens are about billing optimization, not context size - // The actual context footprint is input + output tokens - const contextTokens = usageStats.inputTokens + usageStats.outputTokens; - const contextPercentage = Math.min(Math.round((contextTokens / usageStats.contextWindow) * 100), 100); - - // Accumulate cost if there's already usage stats (session-level for backwards compat) - const existingCost = s.usageStats?.totalCostUsd || 0; - // Current cycle tokens = output tokens from this response // (These are the NEW tokens added to the context, not the cumulative total) const cycleTokens = (s.currentCycleTokens || 0) + usageStats.outputTokens; // Update the specific tab's usageStats if we have a tabId + // Token counts need to be accumulated across exchanges for accurate context window tracking + // Claude Code CLI reports per-exchange tokens, not cumulative session tokens let updatedAiTabs = s.aiTabs; + let accumulatedTabStats: typeof usageStats | null = null; if (tabId && s.aiTabs) { updatedAiTabs = s.aiTabs.map(tab => { if (tab.id !== tabId) return tab; - // Accumulate cost for this specific tab - const tabExistingCost = tab.usageStats?.totalCostUsd || 0; + // Accumulate all stats for this specific tab + const existing = tab.usageStats; + accumulatedTabStats = { + inputTokens: (existing?.inputTokens || 0) + usageStats.inputTokens, + outputTokens: (existing?.outputTokens || 0) + usageStats.outputTokens, + cacheReadInputTokens: (existing?.cacheReadInputTokens || 0) + usageStats.cacheReadInputTokens, + cacheCreationInputTokens: (existing?.cacheCreationInputTokens || 0) + usageStats.cacheCreationInputTokens, + totalCostUsd: (existing?.totalCostUsd || 0) + usageStats.totalCostUsd, + contextWindow: usageStats.contextWindow // Use latest context window size + }; return { ...tab, - usageStats: { - ...usageStats, - totalCostUsd: tabExistingCost + usageStats.totalCostUsd - } + usageStats: accumulatedTabStats }; }); } + // Calculate context window usage percentage from accumulated tab stats + // For a conversation, context contains all inputs and outputs accumulated over the session + const effectiveStats = accumulatedTabStats || usageStats; + const contextTokens = effectiveStats.inputTokens + effectiveStats.outputTokens; + const contextPercentage = Math.min(Math.round((contextTokens / effectiveStats.contextWindow) * 100), 100); + + // Accumulate session-level stats for backwards compatibility + const existingSessionStats = s.usageStats; + return { ...s, contextUsage: contextPercentage, currentCycleTokens: cycleTokens, usageStats: { - ...usageStats, - totalCostUsd: existingCost + usageStats.totalCostUsd + inputTokens: (existingSessionStats?.inputTokens || 0) + usageStats.inputTokens, + outputTokens: (existingSessionStats?.outputTokens || 0) + usageStats.outputTokens, + cacheReadInputTokens: (existingSessionStats?.cacheReadInputTokens || 0) + usageStats.cacheReadInputTokens, + cacheCreationInputTokens: (existingSessionStats?.cacheCreationInputTokens || 0) + usageStats.cacheCreationInputTokens, + totalCostUsd: (existingSessionStats?.totalCostUsd || 0) + usageStats.totalCostUsd, + contextWindow: usageStats.contextWindow }, aiTabs: updatedAiTabs }; diff --git a/src/renderer/components/BatchRunnerModal.tsx b/src/renderer/components/BatchRunnerModal.tsx index 94a9bad4..ee5970f6 100644 --- a/src/renderer/components/BatchRunnerModal.tsx +++ b/src/renderer/components/BatchRunnerModal.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { X, RotateCcw, Play, Variable, ChevronDown, ChevronRight, Save, GripVertical, Plus, Repeat, FolderOpen, Bookmark, GitBranch, AlertTriangle, Loader2, Maximize2 } from 'lucide-react'; +import { X, RotateCcw, Play, Variable, ChevronDown, ChevronRight, Save, GripVertical, Plus, Repeat, FolderOpen, Bookmark, GitBranch, AlertTriangle, Loader2, Maximize2, Download, Upload } from 'lucide-react'; import type { Theme, BatchDocumentEntry, BatchRunConfig, Playbook, PlaybookDocumentEntry, WorktreeConfig } from '../types'; import { useLayerStack } from '../contexts/LayerStackContext'; import { MODAL_PRIORITIES } from '../constants/modalPriorities'; @@ -723,6 +723,35 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { setPlaybookToDelete(null); }, []); + // Handle exporting a playbook + const handleExportPlaybook = useCallback(async (playbook: Playbook) => { + try { + const result = await window.maestro.playbooks.export(sessionId, playbook.id, folderPath); + if (!result.success && result.error !== 'Export cancelled') { + console.error('Failed to export playbook:', result.error); + } + } catch (error) { + console.error('Failed to export playbook:', error); + } + }, [sessionId, folderPath]); + + // Handle importing a playbook + const handleImportPlaybook = useCallback(async () => { + try { + const result = await window.maestro.playbooks.import(sessionId, folderPath); + if (result.success && result.playbook) { + // Add to local playbooks list + setPlaybooks(prev => [...prev, result.playbook]); + // Load the imported playbook + loadPlaybook(result.playbook); + } else if (result.error && result.error !== 'Import cancelled') { + console.error('Failed to import playbook:', result.error); + } + } catch (error) { + console.error('Failed to import playbook:', error); + } + }, [sessionId, folderPath, loadPlaybook]); + // Handle saving a new playbook const handleSaveAsPlaybook = useCallback(async (name: string) => { if (savingPlaybook) return; @@ -933,6 +962,17 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { > {pb.documents.length} doc{pb.documents.length !== 1 ? 's' : ''} + + )} @@ -965,7 +1022,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { )} - {/* Save Update and Discard buttons - shown when playbook is loaded and modified */} + {/* Save Update, Save as New, and Discard buttons - shown when playbook is loaded and modified */} {loadedPlaybook && isPlaybookModified && ( <> + ); })} @@ -680,7 +698,7 @@ export const HistoryPanel = React.memo(forwardRef {filteredEntries.map((entry, index) => { const colors = getPillColor(entry.type); - const Icon = entry.type === 'AUTO' ? Bot : User; + const Icon = getEntryIcon(entry.type); const isSelected = index === selectedIndex; return ( @@ -739,7 +757,7 @@ export const HistoryPanel = React.memo(forwardRef - {entry.type} + {entry.type === 'LOOP_SUMMARY' ? 'LOOP' : entry.type} {/* Session Name or ID Octet (clickable) - opens session as new tab */} diff --git a/src/renderer/components/TerminalOutput.tsx b/src/renderer/components/TerminalOutput.tsx index 59e95497..b2e44f60 100644 --- a/src/renderer/components/TerminalOutput.tsx +++ b/src/renderer/components/TerminalOutput.tsx @@ -601,8 +601,13 @@ const LogItemComponent = memo(({ li: ({ node, children, ...props }: any) => { // Process children to convert p tags to spans for inline rendering const processedChildren = React.Children.map(children, (child: any) => { - if (child?.type === 'p') { - return {child.props.children}; + // Check various ways the p element might appear + const isParagraph = child?.type === 'p' || + child?.type?.displayName === 'p' || + child?.props?.node?.tagName === 'p' || + (typeof child?.type === 'function' && child?.props?.node?.tagName === 'p'); + if (isParagraph) { + return {child.props.children}; } return child; }); @@ -726,8 +731,13 @@ const LogItemComponent = memo(({ li: ({ node, children, ...props }: any) => { // Process children to convert p tags to spans for inline rendering const processedChildren = React.Children.map(children, (child: any) => { - if (child?.type === 'p') { - return {child.props.children}; + // Check various ways the p element might appear + const isParagraph = child?.type === 'p' || + child?.type?.displayName === 'p' || + child?.props?.node?.tagName === 'p' || + (typeof child?.type === 'function' && child?.props?.node?.tagName === 'p'); + if (isParagraph) { + return {child.props.children}; } return child; }); @@ -832,8 +842,13 @@ const LogItemComponent = memo(({ li: ({ node, children, ...props }: any) => { // Convert

children to to prevent block display in list items const processedChildren = React.Children.map(children, (child: any) => { - if (child?.type === 'p') { - return {child.props.children}; + // Check various ways the p element might appear + const isParagraph = child?.type === 'p' || + child?.type?.displayName === 'p' || + child?.props?.node?.tagName === 'p' || + (typeof child?.type === 'function' && child?.props?.node?.tagName === 'p'); + if (isParagraph) { + return {child.props.children}; } return child; }); @@ -1570,7 +1585,8 @@ export const TerminalOutput = forwardRef((p .prose > ul, .prose > ol { color: ${theme.colors.textMain}; margin: 0.25em 0 !important; padding-left: 2em; list-style-position: outside; } .prose li ul, .prose li ol { margin: 0 !important; padding-left: 1.5em; list-style-position: outside; } .prose li { margin: 0 !important; padding: 0; line-height: 1.4; display: list-item; } - .prose li > p { margin: 0 !important; display: contents !important; } + .prose li > p { margin: 0 !important; display: inline !important; } + .prose li > p:first-child { display: inline !important; } .prose li > p + ul, .prose li > p + ol { margin-top: 0 !important; } .prose li:has(> input[type="checkbox"]) { list-style: none; margin-left: -1.5em; } .prose code { background-color: ${theme.colors.bgSidebar}; color: ${theme.colors.textMain}; padding: 0.15em 0.3em; border-radius: 3px; font-size: 0.9em; } diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index d6cfb1ae..9ba11835 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -404,6 +404,8 @@ interface MaestroAPI { prompt: string; }>) => Promise<{ success: boolean; playbook?: any; error?: string }>; delete: (sessionId: string, playbookId: string) => Promise<{ success: boolean; error?: string }>; + export: (sessionId: string, playbookId: string, autoRunFolderPath: string) => Promise<{ success: boolean; filePath?: string; error?: string }>; + import: (sessionId: string, autoRunFolderPath: string) => Promise<{ success: boolean; playbook?: any; importedDocs?: string[]; error?: string }>; }; } diff --git a/src/renderer/hooks/useBatchProcessor.ts b/src/renderer/hooks/useBatchProcessor.ts index 0e14c3c0..a7154013 100644 --- a/src/renderer/hooks/useBatchProcessor.ts +++ b/src/renderer/hooks/useBatchProcessor.ts @@ -84,6 +84,21 @@ interface UseBatchProcessorReturn { setCustomPrompt: (sessionId: string, prompt: string) => void; } +/** + * Format duration in human-readable format for loop summaries + */ +function formatLoopDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + const seconds = Math.floor(ms / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + if (minutes < 60) return `${minutes}m ${remainingSeconds}s`; + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + return `${hours}h ${remainingMinutes}m`; +} + /** * Count unchecked tasks in markdown content * Matches lines like: - [ ] task description @@ -244,6 +259,12 @@ ${docList} return; } + // Debug log: show document configuration + console.log('[BatchProcessor] Starting batch with documents:', documents.map(d => ({ + filename: d.filename, + resetOnCompletion: d.resetOnCompletion + }))); + // Track batch start time for completion notification const batchStartTime = Date.now(); @@ -313,8 +334,10 @@ ${docList} let initialTotalTasks = 0; for (const doc of documents) { const { taskCount } = await readDocAndCountTasks(folderPath, doc.filename); + console.log(`[BatchProcessor] Document ${doc.filename}: ${taskCount} tasks`); initialTotalTasks += taskCount; } + console.log(`[BatchProcessor] Initial total tasks: ${initialTotalTasks}`); if (initialTotalTasks === 0) { console.warn('No unchecked tasks found across all documents for session:', sessionId); @@ -363,6 +386,14 @@ ${docList} let totalCompletedTasks = 0; let loopIteration = 0; + // Per-loop tracking for loop summary + let loopStartTime = Date.now(); + let loopTasksCompleted = 0; + let loopTasksDiscovered = 0; + let loopTotalInputTokens = 0; + let loopTotalOutputTokens = 0; + let loopTotalCost = 0; + // Main processing loop (handles loop mode) while (true) { // Check for stop request @@ -449,6 +480,14 @@ ${docList} // Update counters docTasksCompleted += tasksCompletedThisRun; totalCompletedTasks += tasksCompletedThisRun; + loopTasksCompleted += tasksCompletedThisRun; + + // Track token usage for loop summary + if (result.usageStats) { + loopTotalInputTokens += result.usageStats.inputTokens || 0; + loopTotalOutputTokens += result.usageStats.outputTokens || 0; + loopTotalCost += result.usageStats.totalCostUsd || 0; + } // Track non-reset document completions for loop exit logic if (!docEntry.resetOnCompletion) { @@ -533,6 +572,7 @@ ${docList} } // Document complete - handle reset-on-completion if enabled + console.log(`[BatchProcessor] Document ${docEntry.filename} complete. resetOnCompletion=${docEntry.resetOnCompletion}, docTasksCompleted=${docTasksCompleted}`); if (docEntry.resetOnCompletion && docTasksCompleted > 0) { console.log(`[BatchProcessor] Resetting document ${docEntry.filename} (reset-on-completion enabled)`); @@ -613,17 +653,60 @@ ${docList} break; } - // Continue looping - loopIteration++; - console.log(`[BatchProcessor] Starting loop iteration ${loopIteration + 1}`); - - // Re-scan all documents to get fresh task counts (tasks may have been added/removed) + // Re-scan all documents to get fresh task counts for next loop (tasks may have been added/removed) let newTotalTasks = 0; for (const doc of documents) { const { taskCount } = await readDocAndCountTasks(folderPath, doc.filename); newTotalTasks += taskCount; } - console.log(`[BatchProcessor] Loop ${loopIteration + 1}: ${newTotalTasks} tasks across all documents`); + + // Calculate loop elapsed time + const loopElapsedMs = Date.now() - loopStartTime; + + // Add loop summary history entry + const loopSummary = `Loop ${loopIteration + 1} completed: ${loopTasksCompleted} task${loopTasksCompleted !== 1 ? 's' : ''} accomplished`; + const loopDetails = [ + `**Loop ${loopIteration + 1} Summary**`, + '', + `- **Tasks Accomplished:** ${loopTasksCompleted}`, + `- **Duration:** ${formatLoopDuration(loopElapsedMs)}`, + loopTotalInputTokens > 0 || loopTotalOutputTokens > 0 + ? `- **Tokens:** ${(loopTotalInputTokens + loopTotalOutputTokens).toLocaleString()} (${loopTotalInputTokens.toLocaleString()} in / ${loopTotalOutputTokens.toLocaleString()} out)` + : '', + loopTotalCost > 0 ? `- **Cost:** $${loopTotalCost.toFixed(4)}` : '', + `- **Tasks Discovered for Next Loop:** ${newTotalTasks}`, + ].filter(line => line !== '').join('\n'); + + onAddHistoryEntry({ + type: 'LOOP_SUMMARY', + timestamp: Date.now(), + summary: loopSummary, + fullResponse: loopDetails, + projectPath: session.cwd, + sessionId: sessionId, + success: true, + elapsedTimeMs: loopElapsedMs, + usageStats: loopTotalInputTokens > 0 || loopTotalOutputTokens > 0 ? { + inputTokens: loopTotalInputTokens, + outputTokens: loopTotalOutputTokens, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + totalCostUsd: loopTotalCost, + contextWindow: 0 + } : undefined + }); + + // Reset per-loop tracking for next iteration + loopStartTime = Date.now(); + loopTasksCompleted = 0; + loopTasksDiscovered = newTotalTasks; + loopTotalInputTokens = 0; + loopTotalOutputTokens = 0; + loopTotalCost = 0; + + // Continue looping + loopIteration++; + console.log(`[BatchProcessor] Starting loop iteration ${loopIteration + 1}: ${newTotalTasks} tasks across all documents`); setBatchRunStates(prev => ({ ...prev, diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index d311b6e0..224f2457 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -73,7 +73,7 @@ export interface WorkLogItem { } // History entry types for the History panel -export type HistoryEntryType = 'AUTO' | 'USER'; +export type HistoryEntryType = 'AUTO' | 'USER' | 'LOOP_SUMMARY'; export interface HistoryEntry { id: string;