cascade-ai 0.66.1 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cli.cjs +1342 -484
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1335 -477
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +70407 -1980
- package/dist/index.cjs +1315 -430
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +278 -1
- package/dist/index.d.ts +278 -1
- package/dist/index.js +1287 -424
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/cli.cjs
CHANGED
|
@@ -9,8 +9,8 @@ var https = require('https');
|
|
|
9
9
|
var zlib = require('zlib');
|
|
10
10
|
var stream = require('stream');
|
|
11
11
|
var Database2 = require('better-sqlite3');
|
|
12
|
-
var
|
|
13
|
-
var
|
|
12
|
+
var path33 = require('path');
|
|
13
|
+
var fs30 = require('fs');
|
|
14
14
|
var os10 = require('os');
|
|
15
15
|
var crypto = require('crypto');
|
|
16
16
|
var ink = require('ink');
|
|
@@ -19,7 +19,7 @@ var React2 = require('react');
|
|
|
19
19
|
var chalk7 = require('chalk');
|
|
20
20
|
var dotenv = require('dotenv');
|
|
21
21
|
var url = require('url');
|
|
22
|
-
var
|
|
22
|
+
var fs11 = require('fs/promises');
|
|
23
23
|
var _ignoreModule = require('ignore');
|
|
24
24
|
var zod = require('zod');
|
|
25
25
|
var child_process = require('child_process');
|
|
@@ -77,14 +77,14 @@ var http__default = /*#__PURE__*/_interopDefault(http);
|
|
|
77
77
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
78
78
|
var zlib__default = /*#__PURE__*/_interopDefault(zlib);
|
|
79
79
|
var Database2__default = /*#__PURE__*/_interopDefault(Database2);
|
|
80
|
-
var
|
|
81
|
-
var
|
|
80
|
+
var path33__default = /*#__PURE__*/_interopDefault(path33);
|
|
81
|
+
var fs30__default = /*#__PURE__*/_interopDefault(fs30);
|
|
82
82
|
var os10__default = /*#__PURE__*/_interopDefault(os10);
|
|
83
83
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
84
84
|
var React2__default = /*#__PURE__*/_interopDefault(React2);
|
|
85
85
|
var chalk7__default = /*#__PURE__*/_interopDefault(chalk7);
|
|
86
86
|
var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
|
|
87
|
-
var
|
|
87
|
+
var fs11__default = /*#__PURE__*/_interopDefault(fs11);
|
|
88
88
|
var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
|
|
89
89
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
90
90
|
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
@@ -1810,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1810
1810
|
var init_constants = __esm({
|
|
1811
1811
|
"src/constants.ts"() {
|
|
1812
1812
|
init_pricing();
|
|
1813
|
-
CASCADE_VERSION = "0.
|
|
1813
|
+
CASCADE_VERSION = "0.67.0";
|
|
1814
1814
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1815
1815
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1816
1816
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -3932,12 +3932,12 @@ var init_audit_logger = __esm({
|
|
|
3932
3932
|
constructor(workspacePath, debugMode = false) {
|
|
3933
3933
|
this.workspacePath = workspacePath;
|
|
3934
3934
|
this.debugMode = debugMode;
|
|
3935
|
-
const cascadeDir =
|
|
3936
|
-
if (!
|
|
3937
|
-
|
|
3935
|
+
const cascadeDir = path33__default.default.join(workspacePath, ".cascade");
|
|
3936
|
+
if (!fs30__default.default.existsSync(cascadeDir)) {
|
|
3937
|
+
fs30__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
3938
3938
|
}
|
|
3939
|
-
this.keyPath =
|
|
3940
|
-
this.dbPath =
|
|
3939
|
+
this.keyPath = path33__default.default.join(cascadeDir, "audit_log.key");
|
|
3940
|
+
this.dbPath = path33__default.default.join(cascadeDir, "audit_log.db");
|
|
3941
3941
|
this.initEncryptionKey();
|
|
3942
3942
|
this.db = new Database2__default.default(this.dbPath);
|
|
3943
3943
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -3967,11 +3967,11 @@ var init_audit_logger = __esm({
|
|
|
3967
3967
|
return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
3968
3968
|
}
|
|
3969
3969
|
initEncryptionKey() {
|
|
3970
|
-
if (
|
|
3971
|
-
this.encryptionKey =
|
|
3970
|
+
if (fs30__default.default.existsSync(this.keyPath)) {
|
|
3971
|
+
this.encryptionKey = fs30__default.default.readFileSync(this.keyPath);
|
|
3972
3972
|
} else {
|
|
3973
3973
|
this.encryptionKey = crypto__default.default.randomBytes(32);
|
|
3974
|
-
|
|
3974
|
+
fs30__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
3975
3975
|
}
|
|
3976
3976
|
}
|
|
3977
3977
|
encrypt(text) {
|
|
@@ -4049,9 +4049,9 @@ var init_audit_logger = __esm({
|
|
|
4049
4049
|
dumpDebugIfNeeded() {
|
|
4050
4050
|
if (!this.debugMode) return;
|
|
4051
4051
|
try {
|
|
4052
|
-
const dumpPath =
|
|
4052
|
+
const dumpPath = path33__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
4053
4053
|
const entries = this.getAllLogs();
|
|
4054
|
-
|
|
4054
|
+
fs30__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
4055
4055
|
} catch (err) {
|
|
4056
4056
|
console.error("Failed to dump debug audit logs", err);
|
|
4057
4057
|
}
|
|
@@ -4106,7 +4106,7 @@ var Keystore = class {
|
|
|
4106
4106
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
4107
4107
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
4108
4108
|
this.backend = "keytar";
|
|
4109
|
-
if (password &&
|
|
4109
|
+
if (password && fs30__default.default.existsSync(this.storePath)) {
|
|
4110
4110
|
try {
|
|
4111
4111
|
const fileEntries = this.decryptFile(password);
|
|
4112
4112
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -4125,7 +4125,7 @@ var Keystore = class {
|
|
|
4125
4125
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
4126
4126
|
);
|
|
4127
4127
|
}
|
|
4128
|
-
if (!
|
|
4128
|
+
if (!fs30__default.default.existsSync(this.storePath)) {
|
|
4129
4129
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
4130
4130
|
this.masterKey = this.deriveKey(password, salt);
|
|
4131
4131
|
this.writeWithSalt({}, salt);
|
|
@@ -4139,7 +4139,7 @@ var Keystore = class {
|
|
|
4139
4139
|
}
|
|
4140
4140
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
4141
4141
|
unlockSync(password) {
|
|
4142
|
-
if (!
|
|
4142
|
+
if (!fs30__default.default.existsSync(this.storePath)) {
|
|
4143
4143
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
4144
4144
|
this.masterKey = this.deriveKey(password, salt);
|
|
4145
4145
|
this.writeWithSalt({}, salt);
|
|
@@ -4197,7 +4197,7 @@ var Keystore = class {
|
|
|
4197
4197
|
}
|
|
4198
4198
|
}
|
|
4199
4199
|
decryptFile(password, knownSalt) {
|
|
4200
|
-
if (!
|
|
4200
|
+
if (!fs30__default.default.existsSync(this.storePath)) return {};
|
|
4201
4201
|
try {
|
|
4202
4202
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
4203
4203
|
const useSalt = knownSalt ?? salt;
|
|
@@ -4219,8 +4219,8 @@ var Keystore = class {
|
|
|
4219
4219
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4220
4220
|
const tag = cipher.getAuthTag();
|
|
4221
4221
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
4222
|
-
|
|
4223
|
-
|
|
4222
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(this.storePath), { recursive: true });
|
|
4223
|
+
fs30__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4224
4224
|
}
|
|
4225
4225
|
writeWithSalt(data, salt) {
|
|
4226
4226
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -4230,11 +4230,11 @@ var Keystore = class {
|
|
|
4230
4230
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4231
4231
|
const tag = cipher.getAuthTag();
|
|
4232
4232
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
4233
|
-
|
|
4234
|
-
|
|
4233
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(this.storePath), { recursive: true });
|
|
4234
|
+
fs30__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4235
4235
|
}
|
|
4236
4236
|
readRaw() {
|
|
4237
|
-
const buf =
|
|
4237
|
+
const buf = fs30__default.default.readFileSync(this.storePath);
|
|
4238
4238
|
let offset = 0;
|
|
4239
4239
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
4240
4240
|
offset += SALT_LEN;
|
|
@@ -4267,9 +4267,9 @@ var CascadeIgnore = class {
|
|
|
4267
4267
|
]);
|
|
4268
4268
|
}
|
|
4269
4269
|
async load(workspacePath) {
|
|
4270
|
-
const filePath =
|
|
4270
|
+
const filePath = path33__default.default.join(workspacePath, ".cascadeignore");
|
|
4271
4271
|
try {
|
|
4272
|
-
const content = await
|
|
4272
|
+
const content = await fs11__default.default.readFile(filePath, "utf-8");
|
|
4273
4273
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
4274
4274
|
this.ig.add(lines);
|
|
4275
4275
|
this.loaded = true;
|
|
@@ -4278,7 +4278,7 @@ var CascadeIgnore = class {
|
|
|
4278
4278
|
}
|
|
4279
4279
|
isIgnored(filePath, workspacePath) {
|
|
4280
4280
|
try {
|
|
4281
|
-
const relative = workspacePath ?
|
|
4281
|
+
const relative = workspacePath ? path33__default.default.relative(workspacePath, filePath) : filePath;
|
|
4282
4282
|
return this.ig.ignores(relative);
|
|
4283
4283
|
} catch {
|
|
4284
4284
|
return false;
|
|
@@ -4289,7 +4289,7 @@ var CascadeIgnore = class {
|
|
|
4289
4289
|
}
|
|
4290
4290
|
};
|
|
4291
4291
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
4292
|
-
const filePath =
|
|
4292
|
+
const filePath = path33__default.default.join(workspacePath, ".cascadeignore");
|
|
4293
4293
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
4294
4294
|
# Syntax identical to .gitignore
|
|
4295
4295
|
|
|
@@ -4316,12 +4316,12 @@ build/
|
|
|
4316
4316
|
.DS_Store
|
|
4317
4317
|
Thumbs.db
|
|
4318
4318
|
`;
|
|
4319
|
-
await
|
|
4319
|
+
await fs11__default.default.writeFile(filePath, content, "utf-8");
|
|
4320
4320
|
}
|
|
4321
4321
|
async function loadCascadeMd(workspacePath) {
|
|
4322
|
-
const filePath =
|
|
4322
|
+
const filePath = path33__default.default.join(workspacePath, "CASCADE.md");
|
|
4323
4323
|
try {
|
|
4324
|
-
const raw = await
|
|
4324
|
+
const raw = await fs11__default.default.readFile(filePath, "utf-8");
|
|
4325
4325
|
return parseCascadeMd(raw);
|
|
4326
4326
|
} catch {
|
|
4327
4327
|
return null;
|
|
@@ -4348,7 +4348,7 @@ ${raw.trim()}`;
|
|
|
4348
4348
|
return { raw, sections, systemPrompt };
|
|
4349
4349
|
}
|
|
4350
4350
|
async function createDefaultCascadeMd(workspacePath) {
|
|
4351
|
-
const filePath =
|
|
4351
|
+
const filePath = path33__default.default.join(workspacePath, "CASCADE.md");
|
|
4352
4352
|
const content = `# Cascade Project Instructions
|
|
4353
4353
|
|
|
4354
4354
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -4378,12 +4378,12 @@ All tools are allowed unless specified otherwise.
|
|
|
4378
4378
|
|
|
4379
4379
|
List any areas the agent should not touch.
|
|
4380
4380
|
`;
|
|
4381
|
-
await
|
|
4381
|
+
await fs11__default.default.writeFile(filePath, content, "utf-8");
|
|
4382
4382
|
}
|
|
4383
4383
|
var MemoryStore = class _MemoryStore {
|
|
4384
4384
|
db;
|
|
4385
4385
|
constructor(dbPath) {
|
|
4386
|
-
|
|
4386
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
|
|
4387
4387
|
try {
|
|
4388
4388
|
this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
|
|
4389
4389
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -5635,7 +5635,7 @@ function validateConfig(raw) {
|
|
|
5635
5635
|
// src/config/global-credentials.ts
|
|
5636
5636
|
init_constants();
|
|
5637
5637
|
function credentialsPath(globalDir2) {
|
|
5638
|
-
return
|
|
5638
|
+
return path33__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
5639
5639
|
}
|
|
5640
5640
|
function providerKey(p) {
|
|
5641
5641
|
if (p.type === "azure") {
|
|
@@ -5648,7 +5648,7 @@ function isPersistable(p) {
|
|
|
5648
5648
|
}
|
|
5649
5649
|
function loadGlobalCredentials(globalDir2) {
|
|
5650
5650
|
try {
|
|
5651
|
-
const raw =
|
|
5651
|
+
const raw = fs30__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
5652
5652
|
const parsed = JSON.parse(raw);
|
|
5653
5653
|
if (!Array.isArray(parsed.providers)) return [];
|
|
5654
5654
|
return parsed.providers.filter(
|
|
@@ -5660,13 +5660,13 @@ function loadGlobalCredentials(globalDir2) {
|
|
|
5660
5660
|
}
|
|
5661
5661
|
function saveGlobalCredentials(globalDir2, providers) {
|
|
5662
5662
|
const filePath = credentialsPath(globalDir2);
|
|
5663
|
-
|
|
5663
|
+
fs30__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
5664
5664
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
5665
5665
|
const tmp = `${filePath}.tmp`;
|
|
5666
|
-
|
|
5667
|
-
|
|
5666
|
+
fs30__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
5667
|
+
fs30__default.default.renameSync(tmp, filePath);
|
|
5668
5668
|
try {
|
|
5669
|
-
|
|
5669
|
+
fs30__default.default.chmodSync(filePath, 384);
|
|
5670
5670
|
} catch {
|
|
5671
5671
|
}
|
|
5672
5672
|
}
|
|
@@ -5884,7 +5884,7 @@ var ConfigManager = class {
|
|
|
5884
5884
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
5885
5885
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
5886
5886
|
this.workspacePath = workspacePath;
|
|
5887
|
-
this.globalDir = globalDirOverride ??
|
|
5887
|
+
this.globalDir = globalDirOverride ?? path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
5888
5888
|
}
|
|
5889
5889
|
async load() {
|
|
5890
5890
|
this.config = await this.loadConfig();
|
|
@@ -5898,8 +5898,8 @@ var ConfigManager = class {
|
|
|
5898
5898
|
this.ignore = new CascadeIgnore();
|
|
5899
5899
|
await this.ignore.load(this.workspacePath);
|
|
5900
5900
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
5901
|
-
this.keystore = new Keystore(
|
|
5902
|
-
this.store = new MemoryStore(
|
|
5901
|
+
this.keystore = new Keystore(path33__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
5902
|
+
this.store = new MemoryStore(path33__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
5903
5903
|
await this.injectEnvKeys();
|
|
5904
5904
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
5905
5905
|
await this.ensureDefaultIdentity();
|
|
@@ -5924,9 +5924,9 @@ var ConfigManager = class {
|
|
|
5924
5924
|
return this.workspacePath;
|
|
5925
5925
|
}
|
|
5926
5926
|
async save() {
|
|
5927
|
-
const configPath =
|
|
5928
|
-
await
|
|
5929
|
-
await
|
|
5927
|
+
const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5928
|
+
await fs11__default.default.mkdir(path33__default.default.dirname(configPath), { recursive: true });
|
|
5929
|
+
await fs11__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
5930
5930
|
try {
|
|
5931
5931
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
5932
5932
|
} catch (err) {
|
|
@@ -5961,9 +5961,9 @@ var ConfigManager = class {
|
|
|
5961
5961
|
return configProvider?.apiKey;
|
|
5962
5962
|
}
|
|
5963
5963
|
async loadConfig() {
|
|
5964
|
-
const configPath =
|
|
5964
|
+
const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5965
5965
|
try {
|
|
5966
|
-
const raw = await
|
|
5966
|
+
const raw = await fs11__default.default.readFile(configPath, "utf-8");
|
|
5967
5967
|
return validateConfig(JSON.parse(raw));
|
|
5968
5968
|
} catch (err) {
|
|
5969
5969
|
if (err.code === "ENOENT") {
|
|
@@ -6963,13 +6963,13 @@ var DeadModelStore = class {
|
|
|
6963
6963
|
function fileDeadModelPersistence(filePath) {
|
|
6964
6964
|
return {
|
|
6965
6965
|
load() {
|
|
6966
|
-
if (!
|
|
6967
|
-
const raw = JSON.parse(
|
|
6966
|
+
if (!fs30.existsSync(filePath)) return [];
|
|
6967
|
+
const raw = JSON.parse(fs30.readFileSync(filePath, "utf-8"));
|
|
6968
6968
|
return Array.isArray(raw) ? raw : [];
|
|
6969
6969
|
},
|
|
6970
6970
|
save(records) {
|
|
6971
|
-
|
|
6972
|
-
|
|
6971
|
+
fs30.mkdirSync(path33.dirname(filePath), { recursive: true });
|
|
6972
|
+
fs30.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
6973
6973
|
}
|
|
6974
6974
|
};
|
|
6975
6975
|
}
|
|
@@ -7128,7 +7128,7 @@ init_pricing();
|
|
|
7128
7128
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
7129
7129
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
7130
7130
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
7131
|
-
var DEFAULT_CACHE_FILE =
|
|
7131
|
+
var DEFAULT_CACHE_FILE = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
7132
7132
|
function normalizeModelId2(id) {
|
|
7133
7133
|
let s = id.toLowerCase();
|
|
7134
7134
|
const slash = s.lastIndexOf("/");
|
|
@@ -7163,7 +7163,7 @@ var LiveDataProvider = class {
|
|
|
7163
7163
|
if (this.loaded) return;
|
|
7164
7164
|
this.loaded = true;
|
|
7165
7165
|
try {
|
|
7166
|
-
const raw = await
|
|
7166
|
+
const raw = await fs11__default.default.readFile(this.opts.cacheFile, "utf-8");
|
|
7167
7167
|
const cache = JSON.parse(raw);
|
|
7168
7168
|
if (cache.snapshot?.families) {
|
|
7169
7169
|
this.snapshot = cache.snapshot;
|
|
@@ -7275,14 +7275,14 @@ var LiveDataProvider = class {
|
|
|
7275
7275
|
}
|
|
7276
7276
|
async saveCache() {
|
|
7277
7277
|
try {
|
|
7278
|
-
await
|
|
7278
|
+
await fs11__default.default.mkdir(path33__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
7279
7279
|
const cache = {
|
|
7280
7280
|
fetchedAt: this.fetchedAt,
|
|
7281
7281
|
snapshot: this.snapshot ?? void 0,
|
|
7282
7282
|
prices: Object.fromEntries(this.prices),
|
|
7283
7283
|
capabilities: Object.fromEntries(this.capabilities)
|
|
7284
7284
|
};
|
|
7285
|
-
await
|
|
7285
|
+
await fs11__default.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
7286
7286
|
} catch {
|
|
7287
7287
|
}
|
|
7288
7288
|
}
|
|
@@ -9102,7 +9102,16 @@ var KNOWN_TOOLS = [
|
|
|
9102
9102
|
"glob",
|
|
9103
9103
|
"grep",
|
|
9104
9104
|
"web_fetch",
|
|
9105
|
-
|
|
9105
|
+
// Every media tool buildMediaTools can register, not just the image one.
|
|
9106
|
+
// Omitting three of the four meant a run whose ONLY tools were media ones
|
|
9107
|
+
// (a video-generation subtask on an account with no file/web tools) counted
|
|
9108
|
+
// as "no tools registered" and dropped all tool guidance — the worker was
|
|
9109
|
+
// then told nothing about using tools at all, and wrote the video as prose.
|
|
9110
|
+
"generate_image",
|
|
9111
|
+
"generate_video",
|
|
9112
|
+
"generate_speech",
|
|
9113
|
+
"transcribe_audio",
|
|
9114
|
+
"generate_document"
|
|
9106
9115
|
];
|
|
9107
9116
|
var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
|
|
9108
9117
|
"generate_image",
|
|
@@ -9119,19 +9128,55 @@ function buildWorkerRules(has) {
|
|
|
9119
9128
|
canWriteFiles && "- If the task asks for a file or artifact, you must actually create it in the workspace, verify that it exists, and inspect it before claiming success.",
|
|
9120
9129
|
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
9121
9130
|
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
9131
|
+
// A .docx/.pptx/.xlsx is a ZIP of OOXML parts, not text with a suffix.
|
|
9132
|
+
// file_write does exactly what it promises — writes the model's characters
|
|
9133
|
+
// verbatim — so without this rule the worker "wrote" a Word document that
|
|
9134
|
+
// Word correctly reported as corrupted. Stated as an explicit prohibition
|
|
9135
|
+
// on the tool it would otherwise reach for, because "use generate_document"
|
|
9136
|
+
// alone competes with a file_write habit the model already has.
|
|
9137
|
+
has("generate_document") && '- For a Word (.docx), PowerPoint (.pptx) or Excel (.xlsx) deliverable you MUST call the "generate_document" tool and NEVER "file_write" or "run_code" \u2014 those formats are ZIP archives of XML, so text written straight to the path opens as a corrupted file. Pass the target path plus the source: Markdown for .docx, Markdown slides separated by --- rules for .pptx (each slide starts with a heading), CSV for .xlsx.',
|
|
9138
|
+
// The alternative to this rule is not "a worse chart" — it is a paragraph
|
|
9139
|
+
// describing a chart that does not exist, which is what real decks came
|
|
9140
|
+
// back with. A `chart:` block is checkable, carries the exact numbers, and
|
|
9141
|
+
// becomes a genuine editable PowerPoint chart object.
|
|
9142
|
+
has("generate_document") && '- For any data-driven visualization (a chart, graph, trend, breakdown or comparison of numbers), emit a fenced ```chart:bar block \u2014 also chart:line, chart:pie, chart:doughnut, chart:area, chart:scatter \u2014 whose body is an optional "title: ..." line followed by CSV: a header row of "<category label>,<series name>,<series name>", then one row per category. In a PowerPoint deliverable that becomes a REAL, editable chart carrying your exact numbers; in Word and Excel the same block keeps every value as a table or worksheet. Never write prose describing a chart in place of emitting one.',
|
|
9122
9143
|
// Without this the model "writes the image" as prose — a bracketed
|
|
9123
9144
|
// description sitting in the deck where the picture should be — even with a
|
|
9124
9145
|
// working image model registered. Naming the reference syntax matters as
|
|
9125
9146
|
// much as naming the tool: a generated image nobody embeds is still a
|
|
9126
|
-
// missing image.
|
|
9147
|
+
// missing image. It also has to say the reference must stand ALONE on its
|
|
9148
|
+
// line, because only a standalone reference is parsed as a picture (see
|
|
9149
|
+
// core/documents/blocks.ts matchImageLine) — one embedded mid-sentence
|
|
9150
|
+
// silently degrades to caption text.
|
|
9127
9151
|
//
|
|
9128
|
-
// Scoped to PowerPoint/Word specifically: those are the only
|
|
9129
|
-
//
|
|
9130
|
-
//
|
|
9131
|
-
//
|
|
9132
|
-
//
|
|
9133
|
-
has("generate_image") &&
|
|
9134
|
-
|
|
9152
|
+
// Scoped to PowerPoint/Word specifically: those are the only renderers that
|
|
9153
|
+
// actually embed a Markdown image reference. PDF and plain-text/Markdown
|
|
9154
|
+
// deliverables flatten a reference straight to caption text — telling the
|
|
9155
|
+
// model to call generate_image for THOSE just pays for an image nobody
|
|
9156
|
+
// ever sees.
|
|
9157
|
+
has("generate_image") && // Supersedes the earlier wording of this same rule: it now names the
|
|
9158
|
+
// standalone-line parser constraint, says to generate BEFORE writing the
|
|
9159
|
+
// document, and points data-driven visuals at a ```chart: block rather
|
|
9160
|
+
// than a Markdown table, which is only the right advice now that chart
|
|
9161
|
+
// blocks render as real chart objects.
|
|
9162
|
+
'- When a slide deck (PowerPoint) or Word document deliverable calls for a decorative image, photo or illustration, you MUST call the "generate_image" tool \u2014 once per image the deliverable needs, BEFORE you write the document \u2014 and then put each result on a line of its OWN using Markdown image syntax: , where "location" is exactly the string the tool reported back. A reference inside a sentence stays prose and no picture appears. NEVER write a text placeholder or a bracketed description such as [image: a cat] in its place, and never state that an image is included when you did not generate one. Do NOT use "generate_image" for a chart, graph or diagram that must show exact data (numbers, axes, labels) \u2014 an image model cannot guarantee those values are correct; emit a ```chart: block instead. For any OTHER deliverable format (PDF, plain text, code, etc.) do NOT call "generate_image" \u2014 those cannot embed the result, so describe the visual in words instead.',
|
|
9163
|
+
// The other half of image reliability, and the half no amount of
|
|
9164
|
+
// instruction-tightening could fix: generate_image only exists when an
|
|
9165
|
+
// OpenAI or Gemini key is configured (multimodal/registry.ts CAPABILITIES).
|
|
9166
|
+
// With neither, the model was never given a choice — and a model that
|
|
9167
|
+
// doesn't know that writes "[illustration of a cat]" and moves on. Said out
|
|
9168
|
+
// loud, up front, so the fallback is a real chart or honest prose.
|
|
9169
|
+
!has("generate_image") && (has("generate_document") || canWriteFiles) && "- No image-generation model is available on this run, so there is NO tool that can draw a picture. Do not emit a Markdown image reference, a bracketed placeholder such as [image: a cat], or any claim that an illustration is included. If the request needs a data visualization use a ```chart: block, which needs no image model; otherwise describe the visual in words and say plainly that no image could be generated.",
|
|
9170
|
+
// The video counterpart of the image rule above, and the fix for the
|
|
9171
|
+
// reported "it writes scripts forever and never makes the video" run. The
|
|
9172
|
+
// failure mode is identical — the model narrates the deliverable instead of
|
|
9173
|
+
// producing it — but the stakes are not: a video subtask that ends in prose
|
|
9174
|
+
// has burned the whole pre-production plan for nothing. Unlike the image
|
|
9175
|
+
// rule this is NOT scoped to a document format, because the clip itself is
|
|
9176
|
+
// the deliverable rather than an illustration inside one, and the reference
|
|
9177
|
+
// syntax is a plain link: Markdown image syntax does not embed a video.
|
|
9178
|
+
has("generate_video") && '- When the subtask deliverable is a video, you MUST call the "generate_video" tool \u2014 that call IS the deliverable, and the subtask is not done until it has returned. NEVER deliver the video as prose, a script, a storyboard, or a bracketed placeholder such as [video: a cat skating], and NEVER claim a video exists unless the tool reported a location for it. Call it exactly ONCE: it is billed per second of output and renders for minutes, so a second call charges the user again. Then report the location string the tool returned VERBATIM as your result, referencing it as [description](location) if it goes inside a document. If the tool reports a failure or a timeout, report that failure verbatim and stop \u2014 do NOT call it again, and do NOT substitute a written description of a video that was never made.',
|
|
9179
|
+
has("run_code") && `- Use the "run_code" tool for data processing, archives, and file formats not covered by a dedicated tool. ${has("generate_document") ? 'Do NOT use it to build a .docx, .pptx or .xlsx \u2014 "generate_document" already produces those correctly. ' : ""}${has("pdf_create") ? 'Do NOT use it to build a PDF \u2014 "pdf_create" does that. ' : ""}Always cleanup after code execution.`,
|
|
9135
9180
|
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
9136
9181
|
has("peer_message") && '- Use the "peer_message" tool to communicate with other T3 workers if your tasks have dependencies or shared state. You can send updates or wait for signals.',
|
|
9137
9182
|
hasAnyTool && "- Only use tools directly relevant to THIS subtask. Do not reach for an unrelated connected-service action (e.g. creating, deleting, or modifying a repository, issue, or PR; sending a message) unless the subtask explicitly calls for it.",
|
|
@@ -9142,13 +9187,34 @@ function buildWorkerRules(has) {
|
|
|
9142
9187
|
Rules:
|
|
9143
9188
|
${rules.filter((r) => r !== false).join("\n")}`;
|
|
9144
9189
|
}
|
|
9145
|
-
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell"]);
|
|
9190
|
+
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell", "generate_document"]);
|
|
9191
|
+
var ARTIFACT_FILE_RE = /\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|pptx?|xlsx?|png|jpg|jpeg|svg|gif)\b/i;
|
|
9146
9192
|
function shouldRequireArtifact(assignment, toolNames) {
|
|
9147
9193
|
if (!toolNames.some((n) => ARTIFACT_TOOLS.has(n))) return false;
|
|
9148
9194
|
if (assignment?.files?.length) return true;
|
|
9149
9195
|
const haystack = `${assignment?.description ?? ""}
|
|
9150
9196
|
${assignment?.expectedOutput ?? ""}`;
|
|
9151
|
-
return
|
|
9197
|
+
return ARTIFACT_FILE_RE.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
|
|
9198
|
+
}
|
|
9199
|
+
var VISUAL_REQUEST_RE = /\b(image|images|picture|pictures|photo|photos|illustration|illustrations|visual|visuals|visualisation|visualization|visualizations|chart|charts|graph|graphs|plot|plots|diagram|diagrams|infographic)\b/i;
|
|
9200
|
+
var IMAGE_REF_RE = /!\[[^\]]*\]\([^)]+\)/;
|
|
9201
|
+
var CHART_BLOCK_RE = /^\s*```+\s*chart\s*:/im;
|
|
9202
|
+
function missingVisualEvidence(assignment, output, calledTools, toolNames) {
|
|
9203
|
+
const canVisualize = toolNames.includes("generate_image") || toolNames.includes("generate_document");
|
|
9204
|
+
if (!canVisualize) return null;
|
|
9205
|
+
const haystack = `${assignment?.description ?? ""}
|
|
9206
|
+
${assignment?.expectedOutput ?? ""}`;
|
|
9207
|
+
if (!VISUAL_REQUEST_RE.test(haystack)) return null;
|
|
9208
|
+
if (calledTools.includes("generate_image") || calledTools.includes("generate_document")) return null;
|
|
9209
|
+
if (IMAGE_REF_RE.test(output) || CHART_BLOCK_RE.test(output)) return null;
|
|
9210
|
+
const options = [];
|
|
9211
|
+
if (toolNames.includes("generate_image")) {
|
|
9212
|
+
options.push('call "generate_image" and reference the result on its own line as ');
|
|
9213
|
+
}
|
|
9214
|
+
if (toolNames.includes("generate_document")) {
|
|
9215
|
+
options.push("emit a fenced ```chart:bar (or chart:line / chart:pie) block whose body is CSV, for anything data-driven");
|
|
9216
|
+
}
|
|
9217
|
+
return `The subtask asks for a visual, but the output contains no embedded image and no chart block \u2014 only prose. ${options.join(", or ")}. Describing a picture is not producing one.`;
|
|
9152
9218
|
}
|
|
9153
9219
|
var T3Worker = class extends BaseTier {
|
|
9154
9220
|
router;
|
|
@@ -9329,6 +9395,18 @@ Now execute your subtask using this context where relevant.`
|
|
|
9329
9395
|
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
9330
9396
|
}
|
|
9331
9397
|
}
|
|
9398
|
+
const visualIssue = missingVisualEvidence(
|
|
9399
|
+
assignment,
|
|
9400
|
+
output,
|
|
9401
|
+
toolCalls.map((t) => t.name),
|
|
9402
|
+
this.tools.map((t) => t.name)
|
|
9403
|
+
);
|
|
9404
|
+
if (visualIssue) {
|
|
9405
|
+
correctionAttempts = 1;
|
|
9406
|
+
issues.push(visualIssue);
|
|
9407
|
+
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
9408
|
+
output = await this.correctOutput(output, [visualIssue]);
|
|
9409
|
+
}
|
|
9332
9410
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9333
9411
|
const testResult = await this.selfTest(assignment, output);
|
|
9334
9412
|
checksRun.push(...testResult.checksRun);
|
|
@@ -9643,8 +9721,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9643
9721
|
// Media generation can run for a minute; without this a cancelled run
|
|
9644
9722
|
// still pays for an image nobody will see.
|
|
9645
9723
|
...this.signal ? { signal: this.signal } : {},
|
|
9646
|
-
saveSnapshot: async (
|
|
9647
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
9724
|
+
saveSnapshot: async (path38, content) => {
|
|
9725
|
+
this.store?.addFileSnapshot(this.taskId, path38, content);
|
|
9648
9726
|
},
|
|
9649
9727
|
sendPeerSync: (to, syncType, content) => {
|
|
9650
9728
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -9673,6 +9751,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9673
9751
|
if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
|
|
9674
9752
|
throw new CriticalToolError(errMsg, tc.name);
|
|
9675
9753
|
}
|
|
9754
|
+
if (isProviderBacked) {
|
|
9755
|
+
throw new CriticalToolError(
|
|
9756
|
+
`${tc.name} failed and was not retried \u2014 media generation is billed per attempt, so Cascade stops on the first failure instead of paying for a second. Reason: ${errMsg}`,
|
|
9757
|
+
tc.name
|
|
9758
|
+
);
|
|
9759
|
+
}
|
|
9676
9760
|
return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
|
|
9677
9761
|
}
|
|
9678
9762
|
}
|
|
@@ -9682,6 +9766,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9682
9766
|
* 1. Find a semantically similar registered tool and retry with same input
|
|
9683
9767
|
* 2. Synthesize a new tool via ToolCreator (if available) and run it
|
|
9684
9768
|
* 3. Return the original error so the agent loop can decide what to do next
|
|
9769
|
+
*
|
|
9770
|
+
* Scoped to CHEAP, task-scoped failures — the class of problem it was built
|
|
9771
|
+
* for is a wrong or missing tool name, where a name-similar sibling plausibly
|
|
9772
|
+
* does the same job for the same (near-zero) cost. PROVIDER_BACKED_TOOLS never
|
|
9773
|
+
* reach it: their siblings are keyword-similar but semantically different
|
|
9774
|
+
* (generate_image is not a stand-in for generate_video), and every attempt —
|
|
9775
|
+
* theirs, or a synthesized replacement's — is separately billed. See the
|
|
9776
|
+
* fast-fail branch in executeTool().
|
|
9685
9777
|
*/
|
|
9686
9778
|
async adaptiveFallback(tc, originalError) {
|
|
9687
9779
|
const altTool = this.findAlternativeTool(tc.name);
|
|
@@ -9806,7 +9898,7 @@ ${assignment.expectedOutput}`;
|
|
|
9806
9898
|
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
9807
9899
|
const haystack = `${assignment.description}
|
|
9808
9900
|
${assignment.expectedOutput}`;
|
|
9809
|
-
const matches = haystack.match(
|
|
9901
|
+
const matches = haystack.match(new RegExp(ARTIFACT_FILE_RE.source, "gi")) ?? [];
|
|
9810
9902
|
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
9811
9903
|
}
|
|
9812
9904
|
async verifyArtifacts(assignment) {
|
|
@@ -9817,9 +9909,9 @@ ${assignment.expectedOutput}`;
|
|
|
9817
9909
|
const { promisify: promisify4 } = await import('util');
|
|
9818
9910
|
const execAsync3 = promisify4(exec3);
|
|
9819
9911
|
for (const artifactPath of artifactPaths) {
|
|
9820
|
-
const absolutePath =
|
|
9912
|
+
const absolutePath = path33__default.default.resolve(process.cwd(), artifactPath);
|
|
9821
9913
|
try {
|
|
9822
|
-
const stat = await
|
|
9914
|
+
const stat = await fs11__default.default.stat(absolutePath);
|
|
9823
9915
|
if (!stat.isFile()) {
|
|
9824
9916
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
9825
9917
|
continue;
|
|
@@ -9828,8 +9920,22 @@ ${assignment.expectedOutput}`;
|
|
|
9828
9920
|
issues.push(`Artifact is empty: ${artifactPath}`);
|
|
9829
9921
|
continue;
|
|
9830
9922
|
}
|
|
9831
|
-
if (
|
|
9832
|
-
const
|
|
9923
|
+
if (/\.(docx|pptx|xlsx)$/i.test(artifactPath)) {
|
|
9924
|
+
const head = Buffer.alloc(4);
|
|
9925
|
+
const fh = await fs11__default.default.open(absolutePath, "r");
|
|
9926
|
+
try {
|
|
9927
|
+
await fh.read(head, 0, 4, 0);
|
|
9928
|
+
} finally {
|
|
9929
|
+
await fh.close();
|
|
9930
|
+
}
|
|
9931
|
+
if (head.toString("latin1") !== "PK") {
|
|
9932
|
+
issues.push(
|
|
9933
|
+
`${artifactPath} is not a valid Office file \u2014 it must be written with the generate_document tool, which renders the real OOXML archive. Plain text saved under that extension opens as corrupted.`
|
|
9934
|
+
);
|
|
9935
|
+
continue;
|
|
9936
|
+
}
|
|
9937
|
+
} else if (!/\.pdf$/i.test(artifactPath)) {
|
|
9938
|
+
const content = await fs11__default.default.readFile(absolutePath, "utf-8");
|
|
9833
9939
|
if (!content.trim()) {
|
|
9834
9940
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
9835
9941
|
continue;
|
|
@@ -9838,7 +9944,7 @@ ${assignment.expectedOutput}`;
|
|
|
9838
9944
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
9839
9945
|
continue;
|
|
9840
9946
|
}
|
|
9841
|
-
const ext =
|
|
9947
|
+
const ext = path33__default.default.extname(absolutePath).toLowerCase();
|
|
9842
9948
|
try {
|
|
9843
9949
|
if (ext === ".ts" || ext === ".tsx") {
|
|
9844
9950
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -10434,15 +10540,245 @@ _(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
|
10434
10540
|
return output;
|
|
10435
10541
|
}
|
|
10436
10542
|
|
|
10543
|
+
// src/core/multimodal/registry.ts
|
|
10544
|
+
init_pricing();
|
|
10545
|
+
var CAPABILITIES = [
|
|
10546
|
+
// ── Image ──
|
|
10547
|
+
{
|
|
10548
|
+
modality: "image",
|
|
10549
|
+
provider: "openai",
|
|
10550
|
+
modelId: "dall-e-3",
|
|
10551
|
+
api: "openai-images"
|
|
10552
|
+
},
|
|
10553
|
+
{
|
|
10554
|
+
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
10555
|
+
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
10556
|
+
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
10557
|
+
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
10558
|
+
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
10559
|
+
// 'gemini-generate-content' rather than just the modelId moving.
|
|
10560
|
+
// https://ai.google.dev/gemini-api/docs/imagen
|
|
10561
|
+
modality: "image",
|
|
10562
|
+
provider: "gemini",
|
|
10563
|
+
modelId: "gemini-2.5-flash-image",
|
|
10564
|
+
api: "gemini-generate-content"
|
|
10565
|
+
},
|
|
10566
|
+
// ── Speech (text → audio) ──
|
|
10567
|
+
{
|
|
10568
|
+
modality: "speech",
|
|
10569
|
+
provider: "openai",
|
|
10570
|
+
modelId: "tts-1",
|
|
10571
|
+
api: "openai-speech"
|
|
10572
|
+
},
|
|
10573
|
+
{
|
|
10574
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
10575
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
10576
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
10577
|
+
modality: "speech",
|
|
10578
|
+
provider: "openai",
|
|
10579
|
+
modelId: "tts-1-hd",
|
|
10580
|
+
api: "openai-speech",
|
|
10581
|
+
quality: 70,
|
|
10582
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
10583
|
+
},
|
|
10584
|
+
// ── Transcription (audio → text) ──
|
|
10585
|
+
{
|
|
10586
|
+
modality: "transcription",
|
|
10587
|
+
provider: "openai",
|
|
10588
|
+
modelId: "whisper-1",
|
|
10589
|
+
api: "openai-transcriptions"
|
|
10590
|
+
},
|
|
10591
|
+
// ── Video ──
|
|
10592
|
+
{
|
|
10593
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
10594
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
10595
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
10596
|
+
//
|
|
10597
|
+
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
10598
|
+
// not the Gemini Developer API's — this file calls
|
|
10599
|
+
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
10600
|
+
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
10601
|
+
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
10602
|
+
// there is `veo-3.1-generate-preview`.
|
|
10603
|
+
// https://ai.google.dev/gemini-api/docs/veo
|
|
10604
|
+
modality: "video",
|
|
10605
|
+
provider: "gemini",
|
|
10606
|
+
modelId: "veo-3.1-generate-preview",
|
|
10607
|
+
api: "gemini-predict-lro"
|
|
10608
|
+
}
|
|
10609
|
+
];
|
|
10610
|
+
function capabilityCost(cap) {
|
|
10611
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
10612
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
10613
|
+
});
|
|
10614
|
+
const rate = match?.entry.rates?.[0];
|
|
10615
|
+
if (!rate) return null;
|
|
10616
|
+
const unitLabel = {
|
|
10617
|
+
per_image: "per image",
|
|
10618
|
+
per_second: "per second of video",
|
|
10619
|
+
per_character: "per character",
|
|
10620
|
+
per_audio_minute: "per audio minute"
|
|
10621
|
+
};
|
|
10622
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
10623
|
+
const amount = rate.amount;
|
|
10624
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
10625
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
10626
|
+
}
|
|
10627
|
+
function explainChoice(c, cost, modality, isBest) {
|
|
10628
|
+
const priced = cost ? ` ${cost.label}.` : "";
|
|
10629
|
+
if (c.quality !== void 0) {
|
|
10630
|
+
return isBest ? `${c.modelId} (${c.provider}) \u2014 ranked highest for ${modality}: ${c.qualityBasis}.${priced}` : `${c.modelId} (${c.provider}) \u2014 ranked alternative for ${modality}: ${c.qualityBasis}.${priced}`;
|
|
10631
|
+
}
|
|
10632
|
+
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
10633
|
+
return isBest ? `${c.modelId} (${c.provider}) \u2014 cheapest usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking : `${c.modelId} (${c.provider}) \u2014 alternative usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking;
|
|
10634
|
+
}
|
|
10635
|
+
var MultimodalRegistry = class {
|
|
10636
|
+
/** Provider types the user has actually configured a key for. */
|
|
10637
|
+
available;
|
|
10638
|
+
constructor(availableProviders) {
|
|
10639
|
+
this.available = new Set(availableProviders);
|
|
10640
|
+
}
|
|
10641
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
10642
|
+
usable(modality) {
|
|
10643
|
+
return CAPABILITIES.filter(
|
|
10644
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
10645
|
+
);
|
|
10646
|
+
}
|
|
10647
|
+
/** Modalities this account can actually generate right now. */
|
|
10648
|
+
availableModalities() {
|
|
10649
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
10650
|
+
}
|
|
10651
|
+
/**
|
|
10652
|
+
* Every usable capability for a modality, best first.
|
|
10653
|
+
*
|
|
10654
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
10655
|
+
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
10656
|
+
* the user naming a model is a stronger signal than any prior.
|
|
10657
|
+
*
|
|
10658
|
+
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
10659
|
+
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
10660
|
+
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
10661
|
+
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
10662
|
+
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
10663
|
+
*/
|
|
10664
|
+
rank(modality, opts = {}) {
|
|
10665
|
+
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
10666
|
+
const qa = a.c.quality ?? -1;
|
|
10667
|
+
const qb = b.c.quality ?? -1;
|
|
10668
|
+
if (qa !== qb) return qb - qa;
|
|
10669
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
10670
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
10671
|
+
return pa - pb;
|
|
10672
|
+
});
|
|
10673
|
+
const results = scored.map(({ c, cost }, i) => ({
|
|
10674
|
+
capability: c,
|
|
10675
|
+
cost,
|
|
10676
|
+
reason: explainChoice(c, cost, modality, i === 0)
|
|
10677
|
+
}));
|
|
10678
|
+
if (opts.preferModel) {
|
|
10679
|
+
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
10680
|
+
if (i > -1) {
|
|
10681
|
+
const [wanted] = results.splice(i, 1);
|
|
10682
|
+
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
10683
|
+
}
|
|
10684
|
+
}
|
|
10685
|
+
return results;
|
|
10686
|
+
}
|
|
10687
|
+
/**
|
|
10688
|
+
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
10689
|
+
*/
|
|
10690
|
+
select(modality, opts = {}) {
|
|
10691
|
+
return this.rank(modality, opts)[0] ?? null;
|
|
10692
|
+
}
|
|
10693
|
+
/**
|
|
10694
|
+
* A plain-language account of what this account can and cannot generate —
|
|
10695
|
+
* including the things it cannot, and why.
|
|
10696
|
+
*
|
|
10697
|
+
* This is the USER-facing inventory (model ids and unit prices), keyed on
|
|
10698
|
+
* which providers are configured. It is deliberately NOT what the planner
|
|
10699
|
+
* reads: see `describeGenerationForPlanner()` below for that, and the note
|
|
10700
|
+
* there for why the two cannot be the same string.
|
|
10701
|
+
*/
|
|
10702
|
+
describe() {
|
|
10703
|
+
const lines = [];
|
|
10704
|
+
const usable = this.usable();
|
|
10705
|
+
if (usable.length) {
|
|
10706
|
+
lines.push("Generation models available on your configured providers:");
|
|
10707
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
10708
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
10709
|
+
if (!forModality.length) continue;
|
|
10710
|
+
const shown = forModality.map((c) => {
|
|
10711
|
+
const cost = capabilityCost(c);
|
|
10712
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
10713
|
+
});
|
|
10714
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
10715
|
+
}
|
|
10716
|
+
} else {
|
|
10717
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
10718
|
+
}
|
|
10719
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
10720
|
+
for (const c of blocked) {
|
|
10721
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
10722
|
+
}
|
|
10723
|
+
lines.push(
|
|
10724
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
10725
|
+
);
|
|
10726
|
+
return lines.join("\n");
|
|
10727
|
+
}
|
|
10728
|
+
};
|
|
10729
|
+
var PLANNER_FACTS = [
|
|
10730
|
+
{ tool: "generate_image", modality: "image", shape: "one call returns one finished image" },
|
|
10731
|
+
{ tool: "generate_speech", modality: "speech", shape: "one call returns one finished audio file" },
|
|
10732
|
+
{
|
|
10733
|
+
tool: "generate_video",
|
|
10734
|
+
modality: "video",
|
|
10735
|
+
shape: "one call returns one finished clip, after rendering for 1-3 minutes (Cascade stops waiting at 8)"
|
|
10736
|
+
},
|
|
10737
|
+
{
|
|
10738
|
+
tool: "transcribe_audio",
|
|
10739
|
+
modality: "transcription",
|
|
10740
|
+
shape: "one call returns the transcript of one audio file"
|
|
10741
|
+
}
|
|
10742
|
+
];
|
|
10743
|
+
function plannerPriceNote(modality) {
|
|
10744
|
+
const costs = CAPABILITIES.filter((c) => c.modality === modality && !c.unsupported).map((c) => capabilityCost(c)).filter((c) => c !== null);
|
|
10745
|
+
if (!costs.length) return "";
|
|
10746
|
+
const labels = new Set(costs.map((c) => c.label));
|
|
10747
|
+
if (labels.size === 1) return ` Billed ${costs[0].label}.`;
|
|
10748
|
+
const units = new Set(costs.map((c) => c.unit));
|
|
10749
|
+
return units.size === 1 ? ` Billed ${[...units][0]}.` : "";
|
|
10750
|
+
}
|
|
10751
|
+
function describeGenerationForPlanner(has) {
|
|
10752
|
+
const present = PLANNER_FACTS.filter((f) => has(f.tool));
|
|
10753
|
+
if (!present.length) return "";
|
|
10754
|
+
const lines = ["MEDIA GENERATION \u2014 the only generation tools this run can call:"];
|
|
10755
|
+
for (const f of present) {
|
|
10756
|
+
lines.push(`- "${f.tool}" (${f.modality}): ${f.shape}.${plannerPriceNote(f.modality)}`);
|
|
10757
|
+
}
|
|
10758
|
+
lines.push(
|
|
10759
|
+
"Each one is a single ATOMIC tool call, made by ONE T3 worker, that produces the finished file. It is never a multi-step pipeline, and no worker can satisfy it by describing the result in words.",
|
|
10760
|
+
"No other modality has a tool here \u2014 never plan a step that generates music, 3D or anything not listed above."
|
|
10761
|
+
);
|
|
10762
|
+
if (has("generate_video")) {
|
|
10763
|
+
lines.push(
|
|
10764
|
+
'VIDEO PLANS MUST END IN THE TOOL CALL. Creative pre-production \u2014 a script, a shot list, direction notes \u2014 is expected and worth planning, but it is NOT the video. Any plan that involves video MUST contain exactly ONE subtask whose deliverable is the "generate_video" call itself; it MUST be the last subtask on that path (dependsOn the pre-production ones), and nothing after it may be required to produce the clip. Never end a video plan on a script, a storyboard or a review step. Never split one clip across several "generate_video" subtasks, and never plan a render \u2192 critique \u2192 re-render loop: every render is billed again and takes minutes.'
|
|
10765
|
+
);
|
|
10766
|
+
}
|
|
10767
|
+
return lines.join("\n");
|
|
10768
|
+
}
|
|
10769
|
+
|
|
10437
10770
|
// src/core/tiers/t2-manager.ts
|
|
10438
|
-
function buildT2SystemPrompt(
|
|
10771
|
+
function buildT2SystemPrompt(has) {
|
|
10772
|
+
const generation = describeGenerationForPlanner(has);
|
|
10439
10773
|
return [
|
|
10440
10774
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
10441
10775
|
"Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
|
|
10442
10776
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
10443
|
-
|
|
10444
|
-
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text."
|
|
10445
|
-
|
|
10777
|
+
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
10778
|
+
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
10779
|
+
generation && `
|
|
10780
|
+
${generation}`
|
|
10781
|
+
].filter((l) => l !== false && l !== "").join("\n");
|
|
10446
10782
|
}
|
|
10447
10783
|
var T2Manager = class extends BaseTier {
|
|
10448
10784
|
router;
|
|
@@ -10739,7 +11075,7 @@ Return ONLY the JSON array.`;
|
|
|
10739
11075
|
const messages = [{ role: "user", content: prompt }];
|
|
10740
11076
|
const result = await this.router.generate("T2", {
|
|
10741
11077
|
messages,
|
|
10742
|
-
systemPrompt: this.systemPromptOverride + buildT2SystemPrompt(this.toolRegistry.hasTool(
|
|
11078
|
+
systemPrompt: this.systemPromptOverride + buildT2SystemPrompt((name) => this.toolRegistry.hasTool(name)) + (this.hierarchyContext ? `
|
|
10743
11079
|
|
|
10744
11080
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
10745
11081
|
maxTokens: 2e3,
|
|
@@ -11292,6 +11628,7 @@ function isStrongKeywordOverlap(a = [], b = []) {
|
|
|
11292
11628
|
}
|
|
11293
11629
|
function buildT1SystemPrompt(has) {
|
|
11294
11630
|
const canWriteArtifacts = has("file_write") || has("file_edit") || has("run_code") || has("pdf_create");
|
|
11631
|
+
const generation = describeGenerationForPlanner(has);
|
|
11295
11632
|
const rules = [
|
|
11296
11633
|
"- Simple \u2192 1 T3, Moderate \u2192 2-3 T2s, Complex \u2192 3-5 T2s, Highly Complex \u2192 5+ T2s",
|
|
11297
11634
|
"- Return ONLY valid JSON \u2014 no other text",
|
|
@@ -11326,7 +11663,9 @@ DEPENDENCY GUIDANCE:
|
|
|
11326
11663
|
QUALITY RULES:
|
|
11327
11664
|
- Each section must have a clear, testable "expectedOutput" so T2 knows when it is done.
|
|
11328
11665
|
- Do NOT create trivial sections that only move files or print summaries \u2014 fold those into adjacent sections.
|
|
11329
|
-
- If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2)
|
|
11666
|
+
- If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2).${generation ? `
|
|
11667
|
+
|
|
11668
|
+
${generation}` : ""}`;
|
|
11330
11669
|
}
|
|
11331
11670
|
var T1Administrator = class extends BaseTier {
|
|
11332
11671
|
router;
|
|
@@ -11988,225 +12327,42 @@ Instructions:
|
|
|
11988
12327
|
}
|
|
11989
12328
|
/**
|
|
11990
12329
|
* T1-level permission evaluator.
|
|
11991
|
-
* Uses T1's model with full task context.
|
|
11992
|
-
* Returns null only when the model explicitly says UNSURE (triggers user prompt).
|
|
11993
|
-
*/
|
|
11994
|
-
async evaluatePermissionAtT1(req) {
|
|
11995
|
-
const prompt = `You are T1 Administrator. Overall task goal:
|
|
11996
|
-
${this.taskGoal}
|
|
11997
|
-
|
|
11998
|
-
A T3 Worker (inside section "${req.sectionContext}") wants to:
|
|
11999
|
-
Tool: ${req.toolName}
|
|
12000
|
-
Target: ${JSON.stringify(req.input)}
|
|
12001
|
-
Reason: ${req.subtaskContext}
|
|
12002
|
-
|
|
12003
|
-
T2 Manager was uncertain about this. Given the overall task goal, should this be allowed?
|
|
12004
|
-
Reply with exactly one word: YES, NO, or UNSURE.
|
|
12005
|
-
(UNSURE = escalate to the human user for a final decision.)`;
|
|
12006
|
-
try {
|
|
12007
|
-
const result = await this.router.generate("T1", {
|
|
12008
|
-
messages: [{ role: "user", content: prompt }],
|
|
12009
|
-
systemPrompt: this.systemPromptOverride + "You are a T1 Administrator evaluating permissions.",
|
|
12010
|
-
maxTokens: 10,
|
|
12011
|
-
temperature: 0
|
|
12012
|
-
});
|
|
12013
|
-
const answer = result.content.trim().toUpperCase();
|
|
12014
|
-
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
12015
|
-
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
12016
|
-
return null;
|
|
12017
|
-
} catch {
|
|
12018
|
-
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
12019
|
-
return null;
|
|
12020
|
-
}
|
|
12021
|
-
}
|
|
12022
|
-
};
|
|
12023
|
-
|
|
12024
|
-
// src/core/cascade.ts
|
|
12025
|
-
init_cost();
|
|
12026
|
-
|
|
12027
|
-
// src/core/multimodal/registry.ts
|
|
12028
|
-
init_pricing();
|
|
12029
|
-
var CAPABILITIES = [
|
|
12030
|
-
// ── Image ──
|
|
12031
|
-
{
|
|
12032
|
-
modality: "image",
|
|
12033
|
-
provider: "openai",
|
|
12034
|
-
modelId: "dall-e-3",
|
|
12035
|
-
api: "openai-images"
|
|
12036
|
-
},
|
|
12037
|
-
{
|
|
12038
|
-
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
12039
|
-
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
12040
|
-
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
12041
|
-
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
12042
|
-
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
12043
|
-
// 'gemini-generate-content' rather than just the modelId moving.
|
|
12044
|
-
// https://ai.google.dev/gemini-api/docs/imagen
|
|
12045
|
-
modality: "image",
|
|
12046
|
-
provider: "gemini",
|
|
12047
|
-
modelId: "gemini-2.5-flash-image",
|
|
12048
|
-
api: "gemini-generate-content"
|
|
12049
|
-
},
|
|
12050
|
-
// ── Speech (text → audio) ──
|
|
12051
|
-
{
|
|
12052
|
-
modality: "speech",
|
|
12053
|
-
provider: "openai",
|
|
12054
|
-
modelId: "tts-1",
|
|
12055
|
-
api: "openai-speech"
|
|
12056
|
-
},
|
|
12057
|
-
{
|
|
12058
|
-
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
12059
|
-
// ships these as the same voice at two fidelities and charges double for
|
|
12060
|
-
// this one. That is the vendor's own ordering, not an inference.
|
|
12061
|
-
modality: "speech",
|
|
12062
|
-
provider: "openai",
|
|
12063
|
-
modelId: "tts-1-hd",
|
|
12064
|
-
api: "openai-speech",
|
|
12065
|
-
quality: 70,
|
|
12066
|
-
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
12067
|
-
},
|
|
12068
|
-
// ── Transcription (audio → text) ──
|
|
12069
|
-
{
|
|
12070
|
-
modality: "transcription",
|
|
12071
|
-
provider: "openai",
|
|
12072
|
-
modelId: "whisper-1",
|
|
12073
|
-
api: "openai-transcriptions"
|
|
12074
|
-
},
|
|
12075
|
-
// ── Video ──
|
|
12076
|
-
{
|
|
12077
|
-
// Long-running: submit returns an operation name, the video lands minutes
|
|
12078
|
-
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
12079
|
-
// that polling loop; the registry only records the shape via `api`.
|
|
12080
|
-
//
|
|
12081
|
-
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
12082
|
-
// not the Gemini Developer API's — this file calls
|
|
12083
|
-
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
12084
|
-
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
12085
|
-
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
12086
|
-
// there is `veo-3.1-generate-preview`.
|
|
12087
|
-
// https://ai.google.dev/gemini-api/docs/veo
|
|
12088
|
-
modality: "video",
|
|
12089
|
-
provider: "gemini",
|
|
12090
|
-
modelId: "veo-3.1-generate-preview",
|
|
12091
|
-
api: "gemini-predict-lro"
|
|
12092
|
-
}
|
|
12093
|
-
];
|
|
12094
|
-
function capabilityCost(cap) {
|
|
12095
|
-
const match = findPricing(cap.modelId, cap.provider, {
|
|
12096
|
-
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
12097
|
-
});
|
|
12098
|
-
const rate = match?.entry.rates?.[0];
|
|
12099
|
-
if (!rate) return null;
|
|
12100
|
-
const unitLabel = {
|
|
12101
|
-
per_image: "per image",
|
|
12102
|
-
per_second: "per second of video",
|
|
12103
|
-
per_character: "per character",
|
|
12104
|
-
per_audio_minute: "per audio minute"
|
|
12105
|
-
};
|
|
12106
|
-
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
12107
|
-
const amount = rate.amount;
|
|
12108
|
-
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
12109
|
-
return { label: `$${shown} ${unit}`, amount, unit };
|
|
12110
|
-
}
|
|
12111
|
-
function explainChoice(c, cost, modality, isBest) {
|
|
12112
|
-
const priced = cost ? ` ${cost.label}.` : "";
|
|
12113
|
-
if (c.quality !== void 0) {
|
|
12114
|
-
return isBest ? `${c.modelId} (${c.provider}) \u2014 ranked highest for ${modality}: ${c.qualityBasis}.${priced}` : `${c.modelId} (${c.provider}) \u2014 ranked alternative for ${modality}: ${c.qualityBasis}.${priced}`;
|
|
12115
|
-
}
|
|
12116
|
-
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
12117
|
-
return isBest ? `${c.modelId} (${c.provider}) \u2014 cheapest usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking : `${c.modelId} (${c.provider}) \u2014 alternative usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking;
|
|
12118
|
-
}
|
|
12119
|
-
var MultimodalRegistry = class {
|
|
12120
|
-
/** Provider types the user has actually configured a key for. */
|
|
12121
|
-
available;
|
|
12122
|
-
constructor(availableProviders) {
|
|
12123
|
-
this.available = new Set(availableProviders);
|
|
12124
|
-
}
|
|
12125
|
-
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
12126
|
-
usable(modality) {
|
|
12127
|
-
return CAPABILITIES.filter(
|
|
12128
|
-
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
12129
|
-
);
|
|
12130
|
-
}
|
|
12131
|
-
/** Modalities this account can actually generate right now. */
|
|
12132
|
-
availableModalities() {
|
|
12133
|
-
return [...new Set(this.usable().map((c) => c.modality))];
|
|
12134
|
-
}
|
|
12135
|
-
/**
|
|
12136
|
-
* Every usable capability for a modality, best first.
|
|
12137
|
-
*
|
|
12138
|
-
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
12139
|
-
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
12140
|
-
* the user naming a model is a stronger signal than any prior.
|
|
12141
|
-
*
|
|
12142
|
-
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
12143
|
-
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
12144
|
-
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
12145
|
-
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
12146
|
-
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
12147
|
-
*/
|
|
12148
|
-
rank(modality, opts = {}) {
|
|
12149
|
-
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
12150
|
-
const qa = a.c.quality ?? -1;
|
|
12151
|
-
const qb = b.c.quality ?? -1;
|
|
12152
|
-
if (qa !== qb) return qb - qa;
|
|
12153
|
-
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
12154
|
-
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
12155
|
-
return pa - pb;
|
|
12156
|
-
});
|
|
12157
|
-
const results = scored.map(({ c, cost }, i) => ({
|
|
12158
|
-
capability: c,
|
|
12159
|
-
cost,
|
|
12160
|
-
reason: explainChoice(c, cost, modality, i === 0)
|
|
12161
|
-
}));
|
|
12162
|
-
if (opts.preferModel) {
|
|
12163
|
-
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
12164
|
-
if (i > -1) {
|
|
12165
|
-
const [wanted] = results.splice(i, 1);
|
|
12166
|
-
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
12167
|
-
}
|
|
12168
|
-
}
|
|
12169
|
-
return results;
|
|
12170
|
-
}
|
|
12171
|
-
/**
|
|
12172
|
-
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
12173
|
-
*/
|
|
12174
|
-
select(modality, opts = {}) {
|
|
12175
|
-
return this.rank(modality, opts)[0] ?? null;
|
|
12176
|
-
}
|
|
12177
|
-
/**
|
|
12178
|
-
* A plain-language account of what this account can and cannot generate —
|
|
12179
|
-
* including the things it cannot, and why. The planner sees this, so being
|
|
12180
|
-
* candid here is what stops it inventing a `generate_music` call.
|
|
12181
|
-
*/
|
|
12182
|
-
describe() {
|
|
12183
|
-
const lines = [];
|
|
12184
|
-
const usable = this.usable();
|
|
12185
|
-
if (usable.length) {
|
|
12186
|
-
lines.push("Generation models available on your configured providers:");
|
|
12187
|
-
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
12188
|
-
const forModality = usable.filter((c) => c.modality === modality);
|
|
12189
|
-
if (!forModality.length) continue;
|
|
12190
|
-
const shown = forModality.map((c) => {
|
|
12191
|
-
const cost = capabilityCost(c);
|
|
12192
|
-
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
12193
|
-
});
|
|
12194
|
-
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
12195
|
-
}
|
|
12196
|
-
} else {
|
|
12197
|
-
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
12198
|
-
}
|
|
12199
|
-
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
12200
|
-
for (const c of blocked) {
|
|
12201
|
-
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
12330
|
+
* Uses T1's model with full task context.
|
|
12331
|
+
* Returns null only when the model explicitly says UNSURE (triggers user prompt).
|
|
12332
|
+
*/
|
|
12333
|
+
async evaluatePermissionAtT1(req) {
|
|
12334
|
+
const prompt = `You are T1 Administrator. Overall task goal:
|
|
12335
|
+
${this.taskGoal}
|
|
12336
|
+
|
|
12337
|
+
A T3 Worker (inside section "${req.sectionContext}") wants to:
|
|
12338
|
+
Tool: ${req.toolName}
|
|
12339
|
+
Target: ${JSON.stringify(req.input)}
|
|
12340
|
+
Reason: ${req.subtaskContext}
|
|
12341
|
+
|
|
12342
|
+
T2 Manager was uncertain about this. Given the overall task goal, should this be allowed?
|
|
12343
|
+
Reply with exactly one word: YES, NO, or UNSURE.
|
|
12344
|
+
(UNSURE = escalate to the human user for a final decision.)`;
|
|
12345
|
+
try {
|
|
12346
|
+
const result = await this.router.generate("T1", {
|
|
12347
|
+
messages: [{ role: "user", content: prompt }],
|
|
12348
|
+
systemPrompt: this.systemPromptOverride + "You are a T1 Administrator evaluating permissions.",
|
|
12349
|
+
maxTokens: 10,
|
|
12350
|
+
temperature: 0
|
|
12351
|
+
});
|
|
12352
|
+
const answer = result.content.trim().toUpperCase();
|
|
12353
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
12354
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
12355
|
+
return null;
|
|
12356
|
+
} catch {
|
|
12357
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
12358
|
+
return null;
|
|
12202
12359
|
}
|
|
12203
|
-
lines.push(
|
|
12204
|
-
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
12205
|
-
);
|
|
12206
|
-
return lines.join("\n");
|
|
12207
12360
|
}
|
|
12208
12361
|
};
|
|
12209
12362
|
|
|
12363
|
+
// src/core/cascade.ts
|
|
12364
|
+
init_cost();
|
|
12365
|
+
|
|
12210
12366
|
// src/tools/base.ts
|
|
12211
12367
|
var BaseTool = class {
|
|
12212
12368
|
workspaceRoot = process.cwd();
|
|
@@ -12227,10 +12383,17 @@ var BaseTool = class {
|
|
|
12227
12383
|
|
|
12228
12384
|
// src/core/multimodal/generate.ts
|
|
12229
12385
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
12386
|
+
var GenerationGaveUpError = class extends Error {
|
|
12387
|
+
constructor(message) {
|
|
12388
|
+
super(message);
|
|
12389
|
+
this.name = "GenerationGaveUpError";
|
|
12390
|
+
}
|
|
12391
|
+
};
|
|
12230
12392
|
async function callProvider(fn, modelId) {
|
|
12231
12393
|
try {
|
|
12232
12394
|
return await fn();
|
|
12233
12395
|
} catch (err) {
|
|
12396
|
+
if (err instanceof GenerationGaveUpError) throw err;
|
|
12234
12397
|
const classified = classifyProviderError(err);
|
|
12235
12398
|
throw new Error(describeProviderError(classified, modelId));
|
|
12236
12399
|
}
|
|
@@ -12428,8 +12591,8 @@ async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
|
12428
12591
|
onProgress?.("Still rendering\u2026");
|
|
12429
12592
|
}
|
|
12430
12593
|
if (!operation.done) {
|
|
12431
|
-
throw new
|
|
12432
|
-
|
|
12594
|
+
throw new GenerationGaveUpError(
|
|
12595
|
+
`${cap.modelId} timed out after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes \u2014 no video was produced. Cascade stopped waiting; the render may still finish on the provider side, but nothing was saved. Try again with a shorter clip or a simpler prompt.`
|
|
12433
12596
|
);
|
|
12434
12597
|
}
|
|
12435
12598
|
if (operation.error?.message) throw new Error(operation.error.message);
|
|
@@ -12735,6 +12898,700 @@ function buildMediaTools(deps, readFile) {
|
|
|
12735
12898
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
12736
12899
|
return tools;
|
|
12737
12900
|
}
|
|
12901
|
+
var WorkspaceSandboxError = class extends Error {
|
|
12902
|
+
constructor(attempted, workspaceRoot) {
|
|
12903
|
+
super(`Refusing access to "${attempted}" \u2014 outside workspace root "${workspaceRoot}".`);
|
|
12904
|
+
this.name = "WorkspaceSandboxError";
|
|
12905
|
+
}
|
|
12906
|
+
};
|
|
12907
|
+
function resolveInWorkspace(workspaceRoot, input) {
|
|
12908
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
12909
|
+
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12910
|
+
}
|
|
12911
|
+
const root = path33__default.default.resolve(workspaceRoot);
|
|
12912
|
+
const abs = path33__default.default.isAbsolute(input) ? path33__default.default.resolve(input) : path33__default.default.resolve(root, input);
|
|
12913
|
+
const rel = path33__default.default.relative(root, abs);
|
|
12914
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path33__default.default.isAbsolute(rel)) {
|
|
12915
|
+
throw new WorkspaceSandboxError(input, root);
|
|
12916
|
+
}
|
|
12917
|
+
try {
|
|
12918
|
+
const real = fs30__default.default.realpathSync(abs);
|
|
12919
|
+
const realRel = path33__default.default.relative(root, real);
|
|
12920
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path33__default.default.isAbsolute(realRel))) {
|
|
12921
|
+
throw new WorkspaceSandboxError(input, root);
|
|
12922
|
+
}
|
|
12923
|
+
} catch (e) {
|
|
12924
|
+
if (e instanceof WorkspaceSandboxError) throw e;
|
|
12925
|
+
}
|
|
12926
|
+
return abs;
|
|
12927
|
+
}
|
|
12928
|
+
|
|
12929
|
+
// src/core/documents/blocks.ts
|
|
12930
|
+
function fileExt(name) {
|
|
12931
|
+
const base = name.split(/[\\/]/).pop() ?? name;
|
|
12932
|
+
const dot = base.lastIndexOf(".");
|
|
12933
|
+
return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
12934
|
+
}
|
|
12935
|
+
function parseDelimited(text, name = "") {
|
|
12936
|
+
const delim = fileExt(name) === "tsv" || text.indexOf(" ") !== -1 && text.indexOf(",") === -1 ? " " : ",";
|
|
12937
|
+
const rows = [];
|
|
12938
|
+
let row = [];
|
|
12939
|
+
let cell = "";
|
|
12940
|
+
let quoted = false;
|
|
12941
|
+
for (let i = 0; i < text.length; i++) {
|
|
12942
|
+
const ch = text[i];
|
|
12943
|
+
if (quoted) {
|
|
12944
|
+
if (ch === '"') {
|
|
12945
|
+
if (text[i + 1] === '"') {
|
|
12946
|
+
cell += '"';
|
|
12947
|
+
i++;
|
|
12948
|
+
} else quoted = false;
|
|
12949
|
+
} else cell += ch;
|
|
12950
|
+
} else if (ch === '"') {
|
|
12951
|
+
quoted = true;
|
|
12952
|
+
} else if (ch === delim) {
|
|
12953
|
+
row.push(cell);
|
|
12954
|
+
cell = "";
|
|
12955
|
+
} else if (ch === "\n") {
|
|
12956
|
+
row.push(cell);
|
|
12957
|
+
rows.push(row);
|
|
12958
|
+
row = [];
|
|
12959
|
+
cell = "";
|
|
12960
|
+
} else if (ch === "\r") ; else {
|
|
12961
|
+
cell += ch;
|
|
12962
|
+
}
|
|
12963
|
+
if (rows.length > 5e3) break;
|
|
12964
|
+
}
|
|
12965
|
+
if (cell !== "" || row.length) {
|
|
12966
|
+
row.push(cell);
|
|
12967
|
+
rows.push(row);
|
|
12968
|
+
}
|
|
12969
|
+
return rows.filter((r) => r.length > 1 || (r[0] ?? "") !== "");
|
|
12970
|
+
}
|
|
12971
|
+
var CHART_ALIASES = {
|
|
12972
|
+
bar: "bar",
|
|
12973
|
+
column: "bar",
|
|
12974
|
+
bars: "bar",
|
|
12975
|
+
barchart: "bar",
|
|
12976
|
+
horizontalbar: "bar",
|
|
12977
|
+
line: "line",
|
|
12978
|
+
lines: "line",
|
|
12979
|
+
linechart: "line",
|
|
12980
|
+
pie: "pie",
|
|
12981
|
+
piechart: "pie",
|
|
12982
|
+
doughnut: "doughnut",
|
|
12983
|
+
donut: "doughnut",
|
|
12984
|
+
area: "area",
|
|
12985
|
+
areachart: "area",
|
|
12986
|
+
scatter: "scatter",
|
|
12987
|
+
xy: "scatter"
|
|
12988
|
+
};
|
|
12989
|
+
function chartKind(raw) {
|
|
12990
|
+
return CHART_ALIASES[raw.trim().toLowerCase().replace(/[\s_-]/g, "")] ?? null;
|
|
12991
|
+
}
|
|
12992
|
+
function toNumber(cell) {
|
|
12993
|
+
const cleaned = cell.replace(/[$£€¥,\s%]/g, "").replace(/^\((.*)\)$/, "-$1");
|
|
12994
|
+
const n = Number.parseFloat(cleaned);
|
|
12995
|
+
return Number.isFinite(n) ? n : 0;
|
|
12996
|
+
}
|
|
12997
|
+
function parseChartSpec(kind, body) {
|
|
12998
|
+
const lines = body.replace(/\r\n/g, "\n").split("\n");
|
|
12999
|
+
let title;
|
|
13000
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
13001
|
+
const titleMatch = lines[0]?.match(/^\s*title\s*:\s*(.+)$/i);
|
|
13002
|
+
if (titleMatch) {
|
|
13003
|
+
title = titleMatch[1].trim();
|
|
13004
|
+
lines.shift();
|
|
13005
|
+
}
|
|
13006
|
+
const rows = parseDelimited(lines.join("\n").trim());
|
|
13007
|
+
if (rows.length < 2) return null;
|
|
13008
|
+
const header = rows[0].map((c) => c.trim());
|
|
13009
|
+
if (header.length < 2) return null;
|
|
13010
|
+
const dataRows = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ""));
|
|
13011
|
+
if (!dataRows.length) return null;
|
|
13012
|
+
const categories = dataRows.map((r, i) => (r[0] ?? "").trim() || `Item ${i + 1}`);
|
|
13013
|
+
const series = header.slice(1).map((name, col2) => ({
|
|
13014
|
+
name: name || `Series ${col2 + 1}`,
|
|
13015
|
+
values: dataRows.map((r) => toNumber((r[col2 + 1] ?? "").trim()))
|
|
13016
|
+
}));
|
|
13017
|
+
if (!series.length) return null;
|
|
13018
|
+
return {
|
|
13019
|
+
kind,
|
|
13020
|
+
...title ? { title } : {},
|
|
13021
|
+
categoryName: header[0] || "Category",
|
|
13022
|
+
categories,
|
|
13023
|
+
series
|
|
13024
|
+
};
|
|
13025
|
+
}
|
|
13026
|
+
function chartToTableRows(spec) {
|
|
13027
|
+
return [
|
|
13028
|
+
[spec.categoryName, ...spec.series.map((s) => s.name)],
|
|
13029
|
+
...spec.categories.map((cat, i) => [cat, ...spec.series.map((s) => String(s.values[i] ?? 0))])
|
|
13030
|
+
];
|
|
13031
|
+
}
|
|
13032
|
+
function parseBlocks(md) {
|
|
13033
|
+
const out = [];
|
|
13034
|
+
const lines = md.replace(/\r\n/g, "\n").split("\n");
|
|
13035
|
+
let i = 0;
|
|
13036
|
+
let ordinal = 0;
|
|
13037
|
+
const reset = () => {
|
|
13038
|
+
ordinal = 0;
|
|
13039
|
+
};
|
|
13040
|
+
while (i < lines.length) {
|
|
13041
|
+
const line = lines[i] ?? "";
|
|
13042
|
+
const fence = line.match(/^```+\s*(.*)$/);
|
|
13043
|
+
if (fence) {
|
|
13044
|
+
const info = (fence[1] ?? "").trim();
|
|
13045
|
+
i++;
|
|
13046
|
+
const code = [];
|
|
13047
|
+
while (i < lines.length && !/^```/.test(lines[i] ?? "")) {
|
|
13048
|
+
code.push(lines[i] ?? "");
|
|
13049
|
+
i++;
|
|
13050
|
+
}
|
|
13051
|
+
i++;
|
|
13052
|
+
const chartInfo = info.match(/^chart\s*:\s*(.+)$/i);
|
|
13053
|
+
const kind = chartInfo ? chartKind(chartInfo[1]) : null;
|
|
13054
|
+
const spec = kind ? parseChartSpec(kind, code.join("\n")) : null;
|
|
13055
|
+
out.push(spec ? { t: "chart", spec } : { t: "code", lines: code });
|
|
13056
|
+
reset();
|
|
13057
|
+
continue;
|
|
13058
|
+
}
|
|
13059
|
+
if (!line.trim()) {
|
|
13060
|
+
i++;
|
|
13061
|
+
reset();
|
|
13062
|
+
continue;
|
|
13063
|
+
}
|
|
13064
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
13065
|
+
if (h) {
|
|
13066
|
+
out.push({ t: "heading", level: h[1].length, text: h[2] });
|
|
13067
|
+
i++;
|
|
13068
|
+
reset();
|
|
13069
|
+
continue;
|
|
13070
|
+
}
|
|
13071
|
+
if (/^\s*([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
13072
|
+
out.push({ t: "hr" });
|
|
13073
|
+
i++;
|
|
13074
|
+
reset();
|
|
13075
|
+
continue;
|
|
13076
|
+
}
|
|
13077
|
+
const li = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
13078
|
+
if (li) {
|
|
13079
|
+
const ordered = /\d/.test(li[2]);
|
|
13080
|
+
ordinal = ordered ? ordinal + 1 : 0;
|
|
13081
|
+
out.push({ t: "bullet", ordered, index: ordered ? ordinal : 0, text: li[3] });
|
|
13082
|
+
i++;
|
|
13083
|
+
continue;
|
|
13084
|
+
}
|
|
13085
|
+
if (/^>\s?/.test(line)) {
|
|
13086
|
+
out.push({ t: "quote", text: line.replace(/^>\s?/, "") });
|
|
13087
|
+
i++;
|
|
13088
|
+
reset();
|
|
13089
|
+
continue;
|
|
13090
|
+
}
|
|
13091
|
+
if (/^\|.*\|/.test(line)) {
|
|
13092
|
+
const rows = [];
|
|
13093
|
+
while (i < lines.length && /^\|.*\|/.test(lines[i] ?? "")) {
|
|
13094
|
+
const r = lines[i];
|
|
13095
|
+
if (!/^\|[\s:|-]+\|?\s*$/.test(r)) rows.push(r.replace(/^\||\|\s*$/g, "").split("|").map((c) => c.trim()));
|
|
13096
|
+
i++;
|
|
13097
|
+
}
|
|
13098
|
+
out.push({ t: "table", rows });
|
|
13099
|
+
reset();
|
|
13100
|
+
continue;
|
|
13101
|
+
}
|
|
13102
|
+
out.push({ t: "para", text: line });
|
|
13103
|
+
i++;
|
|
13104
|
+
reset();
|
|
13105
|
+
}
|
|
13106
|
+
return out;
|
|
13107
|
+
}
|
|
13108
|
+
var IMAGE_LINE = /^!\[([^\]]*)\]\(([^)]+)\)$/;
|
|
13109
|
+
function matchImageLine(s) {
|
|
13110
|
+
const m = s.trim().match(IMAGE_LINE);
|
|
13111
|
+
return m ? { alt: m[1].trim(), url: m[2].trim() } : null;
|
|
13112
|
+
}
|
|
13113
|
+
function sniffImage(b) {
|
|
13114
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
13115
|
+
if (b.length > 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
|
|
13116
|
+
return { type: "png", mime: "image/png", width: dv.getUint32(16), height: dv.getUint32(20) };
|
|
13117
|
+
}
|
|
13118
|
+
if (b.length > 10 && b[0] === 71 && b[1] === 73 && b[2] === 70) {
|
|
13119
|
+
return { type: "gif", mime: "image/gif", width: dv.getUint16(6, true), height: dv.getUint16(8, true) };
|
|
13120
|
+
}
|
|
13121
|
+
if (b.length > 26 && b[0] === 66 && b[1] === 77) {
|
|
13122
|
+
return { type: "bmp", mime: "image/bmp", width: Math.abs(dv.getInt32(18, true)), height: Math.abs(dv.getInt32(22, true)) };
|
|
13123
|
+
}
|
|
13124
|
+
if (b.length > 4 && b[0] === 255 && b[1] === 216) {
|
|
13125
|
+
let i = 2;
|
|
13126
|
+
while (i + 9 < b.length) {
|
|
13127
|
+
if (b[i] !== 255) {
|
|
13128
|
+
i++;
|
|
13129
|
+
continue;
|
|
13130
|
+
}
|
|
13131
|
+
const marker = b[i + 1];
|
|
13132
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
13133
|
+
return { type: "jpg", mime: "image/jpeg", height: dv.getUint16(i + 5), width: dv.getUint16(i + 7) };
|
|
13134
|
+
}
|
|
13135
|
+
i += 2 + dv.getUint16(i + 2);
|
|
13136
|
+
}
|
|
13137
|
+
}
|
|
13138
|
+
return null;
|
|
13139
|
+
}
|
|
13140
|
+
function bytesToBase64(bytes) {
|
|
13141
|
+
const g = globalThis;
|
|
13142
|
+
if (g.Buffer) return g.Buffer.from(bytes).toString("base64");
|
|
13143
|
+
const CHUNK = 32768;
|
|
13144
|
+
let binary = "";
|
|
13145
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
13146
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
13147
|
+
}
|
|
13148
|
+
return g.btoa(binary);
|
|
13149
|
+
}
|
|
13150
|
+
function stripInline(s) {
|
|
13151
|
+
return s.replace(/\*\*(.+?)\*\*/g, "$1").replace(/(^|[^*])\*(?!\s)(.+?)\*/g, "$1$2").replace(/`([^`]+)`/g, "$1").replace(/\[(.+?)\]\((.+?)\)/g, "$1 ($2)");
|
|
13152
|
+
}
|
|
13153
|
+
function inlineRuns(text) {
|
|
13154
|
+
const acc = [];
|
|
13155
|
+
const re = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]{1,500}\]\([^)]{1,2000}\)|\*[^*\s][^*]*\*)/g;
|
|
13156
|
+
let last = 0;
|
|
13157
|
+
let m;
|
|
13158
|
+
while ((m = re.exec(text)) !== null) {
|
|
13159
|
+
if (m.index > last) acc.push({ text: text.slice(last, m.index) });
|
|
13160
|
+
const tok = m[0];
|
|
13161
|
+
if (tok.startsWith("**")) acc.push({ text: tok.slice(2, -2), bold: true });
|
|
13162
|
+
else if (tok.startsWith("`")) acc.push({ text: tok.slice(1, -1), code: true });
|
|
13163
|
+
else if (tok.startsWith("[")) {
|
|
13164
|
+
const l = tok.match(/\[([^\]]{1,500})\]\(([^)]{1,2000})\)/);
|
|
13165
|
+
acc.push({ text: l ? `${l[1]} (${l[2]})` : tok });
|
|
13166
|
+
} else acc.push({ text: tok.slice(1, -1), italic: true });
|
|
13167
|
+
last = re.lastIndex;
|
|
13168
|
+
}
|
|
13169
|
+
if (last < text.length) acc.push({ text: text.slice(last) });
|
|
13170
|
+
return acc.length ? acc : [{ text }];
|
|
13171
|
+
}
|
|
13172
|
+
function splitSlides(md) {
|
|
13173
|
+
const norm = md.replace(/\r\n/g, "\n").trim();
|
|
13174
|
+
let chunks = norm.split(/\n\s*-{3,}\s*\n/);
|
|
13175
|
+
if (chunks.length === 1) {
|
|
13176
|
+
const lines = norm.split("\n");
|
|
13177
|
+
const sections = [];
|
|
13178
|
+
let cur = [];
|
|
13179
|
+
for (const ln of lines) {
|
|
13180
|
+
if (/^#{1,2}\s+/.test(ln) && cur.some((l) => l.trim())) {
|
|
13181
|
+
sections.push(cur.join("\n"));
|
|
13182
|
+
cur = [];
|
|
13183
|
+
}
|
|
13184
|
+
cur.push(ln);
|
|
13185
|
+
}
|
|
13186
|
+
if (cur.length) sections.push(cur.join("\n"));
|
|
13187
|
+
if (sections.length > 1) chunks = sections;
|
|
13188
|
+
}
|
|
13189
|
+
return chunks.map(parseSlide).filter((s) => s.title || s.body.length || s.images.length || s.charts.length);
|
|
13190
|
+
}
|
|
13191
|
+
function parseSlide(chunk) {
|
|
13192
|
+
let title = "";
|
|
13193
|
+
const body = [];
|
|
13194
|
+
const images = [];
|
|
13195
|
+
const charts = [];
|
|
13196
|
+
const lines = chunk.split("\n");
|
|
13197
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13198
|
+
const raw = lines[i] ?? "";
|
|
13199
|
+
const fence = raw.match(/^[ \t]*```+[ \t]*chart[ \t]*:(.*)/i);
|
|
13200
|
+
if (fence) {
|
|
13201
|
+
const kind = chartKind(fence[1]);
|
|
13202
|
+
const bodyLines = [];
|
|
13203
|
+
i++;
|
|
13204
|
+
while (i < lines.length && !/^\s*```/.test(lines[i] ?? "")) {
|
|
13205
|
+
bodyLines.push(lines[i] ?? "");
|
|
13206
|
+
i++;
|
|
13207
|
+
}
|
|
13208
|
+
const spec = kind ? parseChartSpec(kind, bodyLines.join("\n")) : null;
|
|
13209
|
+
if (spec) charts.push(spec);
|
|
13210
|
+
else for (const bl of bodyLines) if (bl.trim()) body.push(stripInline(bl.trim()));
|
|
13211
|
+
continue;
|
|
13212
|
+
}
|
|
13213
|
+
const line = raw.trim();
|
|
13214
|
+
if (!line) continue;
|
|
13215
|
+
const h = line.match(/^#{1,6}[ \t]+(.*)/);
|
|
13216
|
+
if (h) {
|
|
13217
|
+
if (!title) title = stripInline(h[1]);
|
|
13218
|
+
else body.push(stripInline(h[1]));
|
|
13219
|
+
continue;
|
|
13220
|
+
}
|
|
13221
|
+
const img = matchImageLine(line);
|
|
13222
|
+
if (img) {
|
|
13223
|
+
images.push(img);
|
|
13224
|
+
continue;
|
|
13225
|
+
}
|
|
13226
|
+
const li = line.match(/^([-*+]|\d+[.)])[ \t]+(.*)/);
|
|
13227
|
+
if (li) {
|
|
13228
|
+
const bulletImg = matchImageLine(li[2]);
|
|
13229
|
+
if (bulletImg) {
|
|
13230
|
+
images.push(bulletImg);
|
|
13231
|
+
continue;
|
|
13232
|
+
}
|
|
13233
|
+
body.push(stripInline(li[2]));
|
|
13234
|
+
continue;
|
|
13235
|
+
}
|
|
13236
|
+
if (/^>\s?/.test(line)) {
|
|
13237
|
+
body.push(stripInline(line.replace(/^>\s?/, "")));
|
|
13238
|
+
continue;
|
|
13239
|
+
}
|
|
13240
|
+
body.push(stripInline(line));
|
|
13241
|
+
}
|
|
13242
|
+
return { title, body, images, charts };
|
|
13243
|
+
}
|
|
13244
|
+
function extractCharts(source) {
|
|
13245
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
13246
|
+
const charts = [];
|
|
13247
|
+
const rest = [];
|
|
13248
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13249
|
+
const fence = lines[i].match(/^[ \t]*```+[ \t]*chart[ \t]*:(.*)/i);
|
|
13250
|
+
if (!fence) {
|
|
13251
|
+
rest.push(lines[i]);
|
|
13252
|
+
continue;
|
|
13253
|
+
}
|
|
13254
|
+
const kind = chartKind(fence[1]);
|
|
13255
|
+
const body = [];
|
|
13256
|
+
i++;
|
|
13257
|
+
while (i < lines.length && !/^\s*```/.test(lines[i] ?? "")) {
|
|
13258
|
+
body.push(lines[i] ?? "");
|
|
13259
|
+
i++;
|
|
13260
|
+
}
|
|
13261
|
+
const spec = kind ? parseChartSpec(kind, body.join("\n")) : null;
|
|
13262
|
+
if (spec) charts.push(spec);
|
|
13263
|
+
else rest.push(...body);
|
|
13264
|
+
}
|
|
13265
|
+
return { charts, rest: rest.join("\n") };
|
|
13266
|
+
}
|
|
13267
|
+
|
|
13268
|
+
// src/core/documents/render.ts
|
|
13269
|
+
function isDocumentFormat(ext) {
|
|
13270
|
+
return ext === "docx" || ext === "pptx" || ext === "xlsx";
|
|
13271
|
+
}
|
|
13272
|
+
async function loadImage(ref, load) {
|
|
13273
|
+
if (!load) return null;
|
|
13274
|
+
try {
|
|
13275
|
+
const bytes = await load(ref.url);
|
|
13276
|
+
if (!bytes || !bytes.length) return null;
|
|
13277
|
+
const info = sniffImage(bytes);
|
|
13278
|
+
if (!info || info.width <= 0 || info.height <= 0) return null;
|
|
13279
|
+
return { ...info, bytes, alt: ref.alt };
|
|
13280
|
+
} catch {
|
|
13281
|
+
return null;
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
async function renderXlsx(source, opts = {}) {
|
|
13285
|
+
const XLSX = await import('xlsx');
|
|
13286
|
+
const { charts, rest } = extractCharts(source);
|
|
13287
|
+
const name = opts.name ?? "";
|
|
13288
|
+
const rows = parseDelimited(rest, name.toLowerCase().endsWith(".tsv") ? "x.tsv" : "x.csv");
|
|
13289
|
+
const wb = XLSX.utils.book_new();
|
|
13290
|
+
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(rows.length ? rows : [[""]]), "Sheet1");
|
|
13291
|
+
const used = /* @__PURE__ */ new Set(["Sheet1"]);
|
|
13292
|
+
charts.forEach((spec, i) => {
|
|
13293
|
+
const base = (spec.title || `Chart ${i + 1}`).replace(/[:\\/?*[\]]/g, " ").trim().slice(0, 28) || `Chart ${i + 1}`;
|
|
13294
|
+
let sheetName = base;
|
|
13295
|
+
let n = 2;
|
|
13296
|
+
while (used.has(sheetName)) sheetName = `${base} ${n++}`.slice(0, 31);
|
|
13297
|
+
used.add(sheetName);
|
|
13298
|
+
const aoa = [
|
|
13299
|
+
[spec.categoryName, ...spec.series.map((s) => s.name)],
|
|
13300
|
+
...spec.categories.map((cat, r) => [cat, ...spec.series.map((s) => s.values[r] ?? 0)])
|
|
13301
|
+
];
|
|
13302
|
+
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sheetName);
|
|
13303
|
+
});
|
|
13304
|
+
return new Uint8Array(XLSX.write(wb, { type: "array", bookType: "xlsx" }));
|
|
13305
|
+
}
|
|
13306
|
+
var DOCX_MAX_W = 600;
|
|
13307
|
+
var DOCX_MAX_H = 864;
|
|
13308
|
+
async function renderDocx(md, opts = {}) {
|
|
13309
|
+
const d = await import('docx');
|
|
13310
|
+
const {
|
|
13311
|
+
Document,
|
|
13312
|
+
Packer,
|
|
13313
|
+
Paragraph,
|
|
13314
|
+
TextRun,
|
|
13315
|
+
ImageRun,
|
|
13316
|
+
HeadingLevel,
|
|
13317
|
+
Table,
|
|
13318
|
+
TableRow,
|
|
13319
|
+
TableCell,
|
|
13320
|
+
WidthType,
|
|
13321
|
+
BorderStyle,
|
|
13322
|
+
AlignmentType
|
|
13323
|
+
} = d;
|
|
13324
|
+
const HEADINGS = [
|
|
13325
|
+
HeadingLevel.HEADING_1,
|
|
13326
|
+
HeadingLevel.HEADING_2,
|
|
13327
|
+
HeadingLevel.HEADING_3,
|
|
13328
|
+
HeadingLevel.HEADING_4,
|
|
13329
|
+
HeadingLevel.HEADING_5,
|
|
13330
|
+
HeadingLevel.HEADING_6
|
|
13331
|
+
];
|
|
13332
|
+
const runs = (text) => inlineRuns(text).map((r) => new TextRun({
|
|
13333
|
+
text: r.text,
|
|
13334
|
+
...r.bold ? { bold: true } : {},
|
|
13335
|
+
...r.italic ? { italics: true } : {},
|
|
13336
|
+
...r.code ? { font: "Courier New" } : {}
|
|
13337
|
+
}));
|
|
13338
|
+
const tableOf = (rows) => new Table({
|
|
13339
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
13340
|
+
rows: rows.map((r) => new TableRow({
|
|
13341
|
+
children: r.map((c) => new TableCell({ children: [new Paragraph({ children: runs(c) })] }))
|
|
13342
|
+
}))
|
|
13343
|
+
});
|
|
13344
|
+
const imageParagraph = (img) => {
|
|
13345
|
+
const scale = Math.min(1, DOCX_MAX_W / img.width, DOCX_MAX_H / img.height);
|
|
13346
|
+
return new Paragraph({
|
|
13347
|
+
children: [new ImageRun({
|
|
13348
|
+
type: img.type,
|
|
13349
|
+
data: img.bytes,
|
|
13350
|
+
transformation: { width: Math.round(img.width * scale), height: Math.round(img.height * scale) },
|
|
13351
|
+
...img.alt ? { altText: { name: img.alt, description: img.alt, title: img.alt } } : {}
|
|
13352
|
+
})]
|
|
13353
|
+
});
|
|
13354
|
+
};
|
|
13355
|
+
const asImage = async (text) => {
|
|
13356
|
+
const ref = matchImageLine(text);
|
|
13357
|
+
return ref ? await loadImage(ref, opts.loadImageBytes) : null;
|
|
13358
|
+
};
|
|
13359
|
+
const children = [];
|
|
13360
|
+
for (const b of parseBlocks(md)) {
|
|
13361
|
+
switch (b.t) {
|
|
13362
|
+
case "heading":
|
|
13363
|
+
children.push(new Paragraph({ heading: HEADINGS[Math.min(b.level, 6) - 1], children: runs(b.text) }));
|
|
13364
|
+
break;
|
|
13365
|
+
case "para": {
|
|
13366
|
+
const img = await asImage(b.text);
|
|
13367
|
+
children.push(img ? imageParagraph(img) : new Paragraph({ children: runs(b.text) }));
|
|
13368
|
+
break;
|
|
13369
|
+
}
|
|
13370
|
+
case "bullet": {
|
|
13371
|
+
const img = await asImage(b.text);
|
|
13372
|
+
children.push(img ? imageParagraph(img) : new Paragraph({
|
|
13373
|
+
children: runs(b.text),
|
|
13374
|
+
...b.ordered ? { numbering: { reference: "ol", level: 0 } } : { bullet: { level: 0 } }
|
|
13375
|
+
}));
|
|
13376
|
+
break;
|
|
13377
|
+
}
|
|
13378
|
+
case "code":
|
|
13379
|
+
for (const cl of b.lines) {
|
|
13380
|
+
children.push(new Paragraph({ children: [new TextRun({ text: cl || " ", font: "Courier New", size: 18 })] }));
|
|
13381
|
+
}
|
|
13382
|
+
break;
|
|
13383
|
+
case "quote":
|
|
13384
|
+
children.push(new Paragraph({ indent: { left: 360 }, children: [new TextRun({ text: stripInline(b.text), italics: true })] }));
|
|
13385
|
+
break;
|
|
13386
|
+
case "table":
|
|
13387
|
+
children.push(tableOf(b.rows));
|
|
13388
|
+
break;
|
|
13389
|
+
case "chart":
|
|
13390
|
+
if (b.spec.title) {
|
|
13391
|
+
children.push(new Paragraph({ children: [new TextRun({ text: b.spec.title, bold: true })] }));
|
|
13392
|
+
}
|
|
13393
|
+
children.push(tableOf(chartToTableRows(b.spec)));
|
|
13394
|
+
break;
|
|
13395
|
+
case "hr":
|
|
13396
|
+
children.push(new Paragraph({
|
|
13397
|
+
children: [],
|
|
13398
|
+
border: { bottom: { color: "CCCCCC", space: 1, style: BorderStyle.SINGLE, size: 6 } }
|
|
13399
|
+
}));
|
|
13400
|
+
break;
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
const doc = new Document({
|
|
13404
|
+
numbering: {
|
|
13405
|
+
config: [{ reference: "ol", levels: [{ level: 0, format: "decimal", text: "%1.", alignment: AlignmentType.START }] }]
|
|
13406
|
+
},
|
|
13407
|
+
sections: [{ children: children.length ? children : [new Paragraph({ children: [new TextRun("")] })] }]
|
|
13408
|
+
});
|
|
13409
|
+
return new Uint8Array(await Packer.toArrayBuffer(doc));
|
|
13410
|
+
}
|
|
13411
|
+
function pptxConstructor(mod) {
|
|
13412
|
+
let c = mod;
|
|
13413
|
+
for (let i = 0; i < 3 && c && typeof c !== "function"; i++) c = c.default;
|
|
13414
|
+
if (typeof c !== "function") throw new Error("pptxgenjs did not export a constructor.");
|
|
13415
|
+
return c;
|
|
13416
|
+
}
|
|
13417
|
+
var SLIDE_H = 7.5;
|
|
13418
|
+
var CONTENT_X = 0.6;
|
|
13419
|
+
var CONTENT_W = 12.1;
|
|
13420
|
+
var CONTENT_BOTTOM = 0.4;
|
|
13421
|
+
async function renderPptx(md, opts = {}) {
|
|
13422
|
+
const PptxGen = pptxConstructor(await import('pptxgenjs'));
|
|
13423
|
+
const pptx = new PptxGen();
|
|
13424
|
+
pptx.layout = "LAYOUT_WIDE";
|
|
13425
|
+
const CHART_TYPES = {
|
|
13426
|
+
bar: pptx.ChartType["bar"],
|
|
13427
|
+
line: pptx.ChartType["line"],
|
|
13428
|
+
pie: pptx.ChartType["pie"],
|
|
13429
|
+
doughnut: pptx.ChartType["doughnut"],
|
|
13430
|
+
area: pptx.ChartType["area"],
|
|
13431
|
+
scatter: pptx.ChartType["scatter"]
|
|
13432
|
+
};
|
|
13433
|
+
const slides = splitSlides(md);
|
|
13434
|
+
const blank = { title: "", body: [""], images: [], charts: [] };
|
|
13435
|
+
for (const s of slides.length ? slides : [blank]) {
|
|
13436
|
+
const slide = pptx.addSlide();
|
|
13437
|
+
if (s.title) slide.addText(s.title, { x: 0.5, y: 0.35, w: 12.3, h: 0.9, fontSize: 28, bold: true, color: "1F2937" });
|
|
13438
|
+
const top = s.title ? 1.5 : 0.5;
|
|
13439
|
+
const pics = (await Promise.all(s.images.map((r) => loadImage(r, opts.loadImageBytes)))).filter((p) => p !== null);
|
|
13440
|
+
const visuals = [
|
|
13441
|
+
...s.charts.map((spec) => ({ kind: "chart", spec })),
|
|
13442
|
+
...pics.map((img) => ({ kind: "image", img }))
|
|
13443
|
+
];
|
|
13444
|
+
const bodyH = visuals.length ? 2.2 : 5.4;
|
|
13445
|
+
if (s.body.length) {
|
|
13446
|
+
slide.addText(
|
|
13447
|
+
s.body.map((t) => ({ text: t, options: { bullet: true, breakLine: true } })),
|
|
13448
|
+
{ x: CONTENT_X, y: top, w: CONTENT_W, h: bodyH, fontSize: 16, color: "374151", valign: "top" }
|
|
13449
|
+
);
|
|
13450
|
+
}
|
|
13451
|
+
if (visuals.length) {
|
|
13452
|
+
const y = top + (s.body.length ? bodyH + 0.2 : 0);
|
|
13453
|
+
const h = Math.max(1, SLIDE_H - CONTENT_BOTTOM - y);
|
|
13454
|
+
const gap = 0.3;
|
|
13455
|
+
const w = (CONTENT_W - gap * (visuals.length - 1)) / visuals.length;
|
|
13456
|
+
visuals.forEach((v, i) => {
|
|
13457
|
+
const x = CONTENT_X + i * (w + gap);
|
|
13458
|
+
if (v.kind === "image") {
|
|
13459
|
+
slide.addImage({
|
|
13460
|
+
data: `data:${v.img.mime};base64,${bytesToBase64(v.img.bytes)}`,
|
|
13461
|
+
x,
|
|
13462
|
+
y,
|
|
13463
|
+
w,
|
|
13464
|
+
h,
|
|
13465
|
+
// 'contain' keeps the aspect ratio inside the box — a stretched
|
|
13466
|
+
// render looks worse than a smaller correct one.
|
|
13467
|
+
sizing: { type: "contain", w, h },
|
|
13468
|
+
...v.img.alt ? { altText: v.img.alt } : {}
|
|
13469
|
+
});
|
|
13470
|
+
} else {
|
|
13471
|
+
slide.addChart(CHART_TYPES[v.spec.kind], chartData(v.spec), {
|
|
13472
|
+
x,
|
|
13473
|
+
y,
|
|
13474
|
+
w,
|
|
13475
|
+
h,
|
|
13476
|
+
showLegend: v.spec.series.length > 1 || v.spec.kind === "pie" || v.spec.kind === "doughnut",
|
|
13477
|
+
legendPos: "b",
|
|
13478
|
+
showTitle: !!v.spec.title,
|
|
13479
|
+
...v.spec.title ? { title: v.spec.title } : {},
|
|
13480
|
+
showValue: v.spec.kind === "pie" || v.spec.kind === "doughnut"
|
|
13481
|
+
});
|
|
13482
|
+
}
|
|
13483
|
+
});
|
|
13484
|
+
}
|
|
13485
|
+
}
|
|
13486
|
+
return new Uint8Array(await pptx.write({ outputType: "arraybuffer" }));
|
|
13487
|
+
}
|
|
13488
|
+
function chartData(spec) {
|
|
13489
|
+
const series = spec.kind === "pie" || spec.kind === "doughnut" ? spec.series.slice(0, 1) : spec.series;
|
|
13490
|
+
return series.map((s) => ({ name: s.name, labels: spec.categories, values: s.values }));
|
|
13491
|
+
}
|
|
13492
|
+
async function renderDocument(format, source, opts = {}) {
|
|
13493
|
+
if (format === "docx") return renderDocx(source, opts);
|
|
13494
|
+
if (format === "pptx") return renderPptx(source, opts);
|
|
13495
|
+
return renderXlsx(source, opts);
|
|
13496
|
+
}
|
|
13497
|
+
|
|
13498
|
+
// src/tools/generate-document.ts
|
|
13499
|
+
var SOURCE_HINT = {
|
|
13500
|
+
docx: "Markdown",
|
|
13501
|
+
pptx: "Markdown slides (separate slides with a --- rule; each slide starts with a heading)",
|
|
13502
|
+
xlsx: "CSV (one header row, then data rows)"
|
|
13503
|
+
};
|
|
13504
|
+
var GenerateDocumentTool = class extends BaseTool {
|
|
13505
|
+
/**
|
|
13506
|
+
* `readFile` is injected so the image loader is testable and so a host with a
|
|
13507
|
+
* different workspace (not plain disk) can supply its own reader.
|
|
13508
|
+
*/
|
|
13509
|
+
constructor(readFile = async (p) => new Uint8Array(await fs11__default.default.readFile(p))) {
|
|
13510
|
+
super();
|
|
13511
|
+
this.readFile = readFile;
|
|
13512
|
+
}
|
|
13513
|
+
readFile;
|
|
13514
|
+
name = "generate_document";
|
|
13515
|
+
description = "Create a REAL Microsoft Office file (.docx Word, .pptx PowerPoint, .xlsx Excel) in the workspace. You write the SOURCE and this tool renders the actual binary: Markdown for .docx, Markdown slides (separated by --- rules) for .pptx, CSV for .xlsx. ALWAYS use this instead of file_write for those three extensions \u2014 file_write stores your text verbatim, which Office reports as a corrupted file. Supports embedded images (reference a generated image as  on its own line) and real, data-accurate charts (a ```chart:bar / chart:line / chart:pie fenced block whose body is CSV). Use pdf_create for PDFs.";
|
|
13516
|
+
inputSchema = {
|
|
13517
|
+
type: "object",
|
|
13518
|
+
properties: {
|
|
13519
|
+
path: {
|
|
13520
|
+
type: "string",
|
|
13521
|
+
description: "Where to write the file, relative to the workspace root. The extension decides the format: .docx, .pptx or .xlsx."
|
|
13522
|
+
},
|
|
13523
|
+
content: {
|
|
13524
|
+
type: "string",
|
|
13525
|
+
description: 'The document source. Markdown for .docx; Markdown slides separated by --- rules for .pptx (each slide starts with a heading); CSV for .xlsx. A standalone  line embeds that image. A fenced ```chart:bar (or chart:line, chart:pie, chart:area, chart:doughnut, chart:scatter) block whose body is CSV \u2014 an optional "title: \u2026" line, then a header row of "<category>,<series1>,<series2>", then data rows \u2014 becomes a real chart.'
|
|
13526
|
+
},
|
|
13527
|
+
format: {
|
|
13528
|
+
type: "string",
|
|
13529
|
+
description: 'Optional override: "docx", "pptx" or "xlsx". Normally inferred from the path extension.'
|
|
13530
|
+
}
|
|
13531
|
+
},
|
|
13532
|
+
required: ["path", "content"]
|
|
13533
|
+
};
|
|
13534
|
+
isDangerous() {
|
|
13535
|
+
return true;
|
|
13536
|
+
}
|
|
13537
|
+
async execute(input, options) {
|
|
13538
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
13539
|
+
if (!filePath) return 'Provide a "path" for the document, e.g. "report.docx".';
|
|
13540
|
+
const content = typeof input["content"] === "string" ? input["content"] : "";
|
|
13541
|
+
if (!content.trim()) return 'Provide "content" \u2014 the Markdown (docx/pptx) or CSV (xlsx) source for the document.';
|
|
13542
|
+
const requested = String(input["format"] ?? "").trim().toLowerCase().replace(/^\./, "");
|
|
13543
|
+
const ext = requested || fileExt(filePath);
|
|
13544
|
+
if (!isDocumentFormat(ext)) {
|
|
13545
|
+
return `generate_document renders .docx, .pptx and .xlsx. "${filePath}" is not one of those \u2014 use pdf_create for a PDF, or file_write for a text file.`;
|
|
13546
|
+
}
|
|
13547
|
+
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
13548
|
+
const embedded = [];
|
|
13549
|
+
const skipped = [];
|
|
13550
|
+
const loadImageBytes = async (url) => {
|
|
13551
|
+
if (/^(https?|ftp):\/\//i.test(url)) {
|
|
13552
|
+
skipped.push(`${url} (remote URL \u2014 download it into the workspace first)`);
|
|
13553
|
+
return null;
|
|
13554
|
+
}
|
|
13555
|
+
const local = url.replace(/^file:\/\//i, "").split("?")[0].split("#")[0];
|
|
13556
|
+
try {
|
|
13557
|
+
const bytes2 = await this.readFile(resolveInWorkspace(this.workspaceRoot, decodeURIComponent(local)));
|
|
13558
|
+
embedded.push(local);
|
|
13559
|
+
return bytes2;
|
|
13560
|
+
} catch {
|
|
13561
|
+
skipped.push(`${local} (not found in the workspace)`);
|
|
13562
|
+
return null;
|
|
13563
|
+
}
|
|
13564
|
+
};
|
|
13565
|
+
if (options.saveSnapshot) {
|
|
13566
|
+
try {
|
|
13567
|
+
const old = await fs11__default.default.readFile(absPath, "utf-8");
|
|
13568
|
+
await options.saveSnapshot(absPath, old);
|
|
13569
|
+
} catch {
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13572
|
+
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
13573
|
+
await fs11__default.default.mkdir(path33__default.default.dirname(absPath), { recursive: true });
|
|
13574
|
+
await fs11__default.default.writeFile(absPath, bytes);
|
|
13575
|
+
const charts = countCharts(content);
|
|
13576
|
+
return [
|
|
13577
|
+
`Wrote a real ${LABEL[ext]} file to ${filePath} (${bytes.length} bytes) from ${SOURCE_HINT[ext]}.`,
|
|
13578
|
+
embedded.length ? `Embedded ${embedded.length} image(s): ${embedded.join(", ")}.` : "",
|
|
13579
|
+
// Named, not counted: a dropped image is the failure the user actually
|
|
13580
|
+
// notices, and the model can only fix it if it is told which reference died.
|
|
13581
|
+
skipped.length ? `Could NOT embed ${skipped.length} image reference(s): ${skipped.join("; ")}.` : "",
|
|
13582
|
+
charts && ext === "pptx" ? `Rendered ${charts} chart block(s) as real, editable PowerPoint charts.` : "",
|
|
13583
|
+
charts && ext === "docx" ? `Rendered ${charts} chart block(s) as data tables \u2014 Word documents cannot hold a native chart object.` : "",
|
|
13584
|
+
charts && ext === "xlsx" ? `Wrote ${charts} chart block(s) onto their own worksheets as real rows the user can chart in Excel.` : ""
|
|
13585
|
+
].filter(Boolean).join("\n");
|
|
13586
|
+
}
|
|
13587
|
+
};
|
|
13588
|
+
var LABEL = { docx: "Word", pptx: "PowerPoint", xlsx: "Excel" };
|
|
13589
|
+
function countCharts(source) {
|
|
13590
|
+
return (source.match(/^\s*```+\s*chart\s*:/gim) ?? []).length;
|
|
13591
|
+
}
|
|
13592
|
+
function buildDocumentTools(readFile) {
|
|
13593
|
+
return readFile ? [new GenerateDocumentTool(readFile)] : [];
|
|
13594
|
+
}
|
|
12738
13595
|
|
|
12739
13596
|
// src/core/run-breaker.ts
|
|
12740
13597
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
@@ -12883,35 +13740,6 @@ ${[e.stdout, e.stderr].filter(Boolean).join("\n").trim()}`;
|
|
|
12883
13740
|
}
|
|
12884
13741
|
}
|
|
12885
13742
|
};
|
|
12886
|
-
var WorkspaceSandboxError = class extends Error {
|
|
12887
|
-
constructor(attempted, workspaceRoot) {
|
|
12888
|
-
super(`Refusing access to "${attempted}" \u2014 outside workspace root "${workspaceRoot}".`);
|
|
12889
|
-
this.name = "WorkspaceSandboxError";
|
|
12890
|
-
}
|
|
12891
|
-
};
|
|
12892
|
-
function resolveInWorkspace(workspaceRoot, input) {
|
|
12893
|
-
if (typeof input !== "string" || input.length === 0) {
|
|
12894
|
-
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12895
|
-
}
|
|
12896
|
-
const root = path32__default.default.resolve(workspaceRoot);
|
|
12897
|
-
const abs = path32__default.default.isAbsolute(input) ? path32__default.default.resolve(input) : path32__default.default.resolve(root, input);
|
|
12898
|
-
const rel = path32__default.default.relative(root, abs);
|
|
12899
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path32__default.default.isAbsolute(rel)) {
|
|
12900
|
-
throw new WorkspaceSandboxError(input, root);
|
|
12901
|
-
}
|
|
12902
|
-
try {
|
|
12903
|
-
const real = fs29__default.default.realpathSync(abs);
|
|
12904
|
-
const realRel = path32__default.default.relative(root, real);
|
|
12905
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path32__default.default.isAbsolute(realRel))) {
|
|
12906
|
-
throw new WorkspaceSandboxError(input, root);
|
|
12907
|
-
}
|
|
12908
|
-
} catch (e) {
|
|
12909
|
-
if (e instanceof WorkspaceSandboxError) throw e;
|
|
12910
|
-
}
|
|
12911
|
-
return abs;
|
|
12912
|
-
}
|
|
12913
|
-
|
|
12914
|
-
// src/tools/file.ts
|
|
12915
13743
|
var FileReadTool = class extends BaseTool {
|
|
12916
13744
|
name = "file_read";
|
|
12917
13745
|
description = "Read the contents of a file. Returns the file content as a string.";
|
|
@@ -12929,7 +13757,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
12929
13757
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
12930
13758
|
const offset = input["offset"] ?? 1;
|
|
12931
13759
|
const limit = input["limit"];
|
|
12932
|
-
const content = await
|
|
13760
|
+
const content = await fs11__default.default.readFile(absPath, "utf-8");
|
|
12933
13761
|
const lines = content.split("\n");
|
|
12934
13762
|
const start = Math.max(0, offset - 1);
|
|
12935
13763
|
const end = limit ? start + limit : lines.length;
|
|
@@ -12958,13 +13786,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
12958
13786
|
const content = input["content"];
|
|
12959
13787
|
if (options.saveSnapshot) {
|
|
12960
13788
|
try {
|
|
12961
|
-
const oldContent = await
|
|
13789
|
+
const oldContent = await fs11__default.default.readFile(absPath, "utf-8");
|
|
12962
13790
|
await options.saveSnapshot(absPath, oldContent);
|
|
12963
13791
|
} catch {
|
|
12964
13792
|
}
|
|
12965
13793
|
}
|
|
12966
|
-
await
|
|
12967
|
-
await
|
|
13794
|
+
await fs11__default.default.mkdir(path33__default.default.dirname(absPath), { recursive: true });
|
|
13795
|
+
await fs11__default.default.writeFile(absPath, content, "utf-8");
|
|
12968
13796
|
return `Written ${content.length} characters to ${filePath}`;
|
|
12969
13797
|
}
|
|
12970
13798
|
};
|
|
@@ -12990,7 +13818,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
12990
13818
|
const oldString = input["old_string"];
|
|
12991
13819
|
const newString = input["new_string"];
|
|
12992
13820
|
const replaceAll = input["replace_all"] ?? false;
|
|
12993
|
-
const rawContent = await
|
|
13821
|
+
const rawContent = await fs11__default.default.readFile(absPath, "utf-8");
|
|
12994
13822
|
if (options.saveSnapshot) {
|
|
12995
13823
|
await options.saveSnapshot(absPath, rawContent);
|
|
12996
13824
|
}
|
|
@@ -13002,7 +13830,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
13002
13830
|
);
|
|
13003
13831
|
}
|
|
13004
13832
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
13005
|
-
await
|
|
13833
|
+
await fs11__default.default.writeFile(absPath, updated, "utf-8");
|
|
13006
13834
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
13007
13835
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
13008
13836
|
}
|
|
@@ -13025,12 +13853,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
13025
13853
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
13026
13854
|
if (options.saveSnapshot) {
|
|
13027
13855
|
try {
|
|
13028
|
-
const oldContent = await
|
|
13856
|
+
const oldContent = await fs11__default.default.readFile(absPath, "utf-8");
|
|
13029
13857
|
await options.saveSnapshot(absPath, oldContent);
|
|
13030
13858
|
} catch {
|
|
13031
13859
|
}
|
|
13032
13860
|
}
|
|
13033
|
-
await
|
|
13861
|
+
await fs11__default.default.rm(absPath, { recursive: false });
|
|
13034
13862
|
return `Deleted ${filePath}`;
|
|
13035
13863
|
}
|
|
13036
13864
|
};
|
|
@@ -13047,7 +13875,7 @@ var FileListTool = class extends BaseTool {
|
|
|
13047
13875
|
async execute(input, _options) {
|
|
13048
13876
|
const inputPath = input["path"] || ".";
|
|
13049
13877
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
13050
|
-
const entries = await
|
|
13878
|
+
const entries = await fs11__default.default.readdir(absPath, { withFileTypes: true });
|
|
13051
13879
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
13052
13880
|
}
|
|
13053
13881
|
};
|
|
@@ -13460,8 +14288,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
13460
14288
|
}
|
|
13461
14289
|
};
|
|
13462
14290
|
async function fileToImageAttachment(filePath) {
|
|
13463
|
-
const data = await
|
|
13464
|
-
const ext =
|
|
14291
|
+
const data = await fs11__default.default.readFile(filePath);
|
|
14292
|
+
const ext = path33__default.default.extname(filePath).toLowerCase();
|
|
13465
14293
|
const mimeMap = {
|
|
13466
14294
|
".jpg": "image/jpeg",
|
|
13467
14295
|
".jpeg": "image/jpeg",
|
|
@@ -13495,14 +14323,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
13495
14323
|
const filePath = input["path"];
|
|
13496
14324
|
const content = input["content"];
|
|
13497
14325
|
const title = input["title"];
|
|
13498
|
-
const dir =
|
|
13499
|
-
if (!
|
|
13500
|
-
|
|
14326
|
+
const dir = path33__default.default.dirname(filePath);
|
|
14327
|
+
if (!fs30__default.default.existsSync(dir)) {
|
|
14328
|
+
fs30__default.default.mkdirSync(dir, { recursive: true });
|
|
13501
14329
|
}
|
|
13502
14330
|
return new Promise((resolve2, reject) => {
|
|
13503
14331
|
try {
|
|
13504
14332
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
13505
|
-
const stream =
|
|
14333
|
+
const stream = fs30__default.default.createWriteStream(filePath);
|
|
13506
14334
|
doc.pipe(stream);
|
|
13507
14335
|
if (title) {
|
|
13508
14336
|
doc.info["Title"] = title;
|
|
@@ -13580,22 +14408,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
13580
14408
|
}
|
|
13581
14409
|
cmdPrefix = NODE_CMD;
|
|
13582
14410
|
}
|
|
13583
|
-
const tmpDir =
|
|
13584
|
-
if (!
|
|
13585
|
-
|
|
14411
|
+
const tmpDir = path33__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
14412
|
+
if (!fs30__default.default.existsSync(tmpDir)) {
|
|
14413
|
+
fs30__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
13586
14414
|
}
|
|
13587
14415
|
const extension = language === "python" ? "py" : "js";
|
|
13588
14416
|
const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
|
|
13589
|
-
const filePath =
|
|
13590
|
-
|
|
14417
|
+
const filePath = path33__default.default.join(tmpDir, fileName);
|
|
14418
|
+
fs30__default.default.writeFileSync(filePath, code, "utf-8");
|
|
13591
14419
|
const execArgs = [filePath, ...args];
|
|
13592
14420
|
return new Promise((resolve2) => {
|
|
13593
14421
|
const startMs = Date.now();
|
|
13594
14422
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
13595
14423
|
const duration = Date.now() - startMs;
|
|
13596
14424
|
try {
|
|
13597
|
-
if (
|
|
13598
|
-
|
|
14425
|
+
if (fs30__default.default.existsSync(filePath)) {
|
|
14426
|
+
fs30__default.default.unlinkSync(filePath);
|
|
13599
14427
|
}
|
|
13600
14428
|
} catch (cleanupErr) {
|
|
13601
14429
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -13907,7 +14735,7 @@ var GlobTool = class extends BaseTool {
|
|
|
13907
14735
|
};
|
|
13908
14736
|
async execute(input, _options) {
|
|
13909
14737
|
const pattern = input["pattern"];
|
|
13910
|
-
const searchPath = input["path"] ?
|
|
14738
|
+
const searchPath = input["path"] ? path33__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
13911
14739
|
const matches = await glob.glob(pattern, {
|
|
13912
14740
|
cwd: searchPath,
|
|
13913
14741
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -13920,7 +14748,7 @@ var GlobTool = class extends BaseTool {
|
|
|
13920
14748
|
const withMtime = await Promise.all(
|
|
13921
14749
|
matches.map(async (rel) => {
|
|
13922
14750
|
try {
|
|
13923
|
-
const stat = await
|
|
14751
|
+
const stat = await fs11__default.default.stat(path33__default.default.join(searchPath, rel));
|
|
13924
14752
|
return { rel, mtime: stat.mtimeMs };
|
|
13925
14753
|
} catch {
|
|
13926
14754
|
return { rel, mtime: 0 };
|
|
@@ -13969,7 +14797,7 @@ var GrepTool = class extends BaseTool {
|
|
|
13969
14797
|
};
|
|
13970
14798
|
async execute(input, _options) {
|
|
13971
14799
|
const pattern = input["pattern"];
|
|
13972
|
-
const searchPath = input["path"] ?
|
|
14800
|
+
const searchPath = input["path"] ? path33__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
13973
14801
|
const globPattern = input["glob"];
|
|
13974
14802
|
const outputMode = input["output_mode"] ?? "content";
|
|
13975
14803
|
const context = input["context"] ?? 0;
|
|
@@ -14023,15 +14851,15 @@ var GrepTool = class extends BaseTool {
|
|
|
14023
14851
|
nodir: true
|
|
14024
14852
|
});
|
|
14025
14853
|
} catch {
|
|
14026
|
-
files = [
|
|
14854
|
+
files = [path33__default.default.relative(searchPath, searchPath) || "."];
|
|
14027
14855
|
}
|
|
14028
14856
|
const results = [];
|
|
14029
14857
|
let totalCount = 0;
|
|
14030
14858
|
for (const rel of files) {
|
|
14031
|
-
const abs =
|
|
14859
|
+
const abs = path33__default.default.join(searchPath, rel);
|
|
14032
14860
|
let content;
|
|
14033
14861
|
try {
|
|
14034
|
-
content = await
|
|
14862
|
+
content = await fs11__default.default.readFile(abs, "utf-8");
|
|
14035
14863
|
} catch {
|
|
14036
14864
|
continue;
|
|
14037
14865
|
}
|
|
@@ -14415,10 +15243,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
14415
15243
|
}
|
|
14416
15244
|
isIgnored(filePath) {
|
|
14417
15245
|
if (!filePath) return false;
|
|
14418
|
-
const abs =
|
|
14419
|
-
const rel =
|
|
14420
|
-
if (!rel || rel.startsWith("..") ||
|
|
14421
|
-
const posixRel = rel.split(
|
|
15246
|
+
const abs = path33__default.default.resolve(this.workspaceRoot, filePath);
|
|
15247
|
+
const rel = path33__default.default.relative(this.workspaceRoot, abs);
|
|
15248
|
+
if (!rel || rel.startsWith("..") || path33__default.default.isAbsolute(rel)) return true;
|
|
15249
|
+
const posixRel = rel.split(path33__default.default.sep).join("/");
|
|
14422
15250
|
return this.ignoreMatcher.ignores(posixRel);
|
|
14423
15251
|
}
|
|
14424
15252
|
};
|
|
@@ -14652,22 +15480,22 @@ var FileMcpOAuthStore = class {
|
|
|
14652
15480
|
filePath;
|
|
14653
15481
|
load() {
|
|
14654
15482
|
try {
|
|
14655
|
-
return JSON.parse(
|
|
15483
|
+
return JSON.parse(fs30__default.default.readFileSync(this.filePath, "utf-8"));
|
|
14656
15484
|
} catch {
|
|
14657
15485
|
return void 0;
|
|
14658
15486
|
}
|
|
14659
15487
|
}
|
|
14660
15488
|
save(state) {
|
|
14661
|
-
|
|
14662
|
-
|
|
15489
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
15490
|
+
fs30__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
14663
15491
|
try {
|
|
14664
|
-
|
|
15492
|
+
fs30__default.default.chmodSync(this.filePath, 384);
|
|
14665
15493
|
} catch {
|
|
14666
15494
|
}
|
|
14667
15495
|
}
|
|
14668
15496
|
clear() {
|
|
14669
15497
|
try {
|
|
14670
|
-
|
|
15498
|
+
fs30__default.default.rmSync(this.filePath);
|
|
14671
15499
|
} catch {
|
|
14672
15500
|
}
|
|
14673
15501
|
}
|
|
@@ -15275,7 +16103,7 @@ var TaskAnalyzer = class {
|
|
|
15275
16103
|
|
|
15276
16104
|
// src/core/router/model-performance-tracker.ts
|
|
15277
16105
|
init_pricing();
|
|
15278
|
-
var DEFAULT_STATS_FILE =
|
|
16106
|
+
var DEFAULT_STATS_FILE = path33__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
|
|
15279
16107
|
var ModelPerformanceTracker = class {
|
|
15280
16108
|
stats = /* @__PURE__ */ new Map();
|
|
15281
16109
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -15295,7 +16123,7 @@ var ModelPerformanceTracker = class {
|
|
|
15295
16123
|
if (this.loaded) return;
|
|
15296
16124
|
this.loaded = true;
|
|
15297
16125
|
try {
|
|
15298
|
-
const raw = await
|
|
16126
|
+
const raw = await fs11__default.default.readFile(this.statsFile, "utf-8");
|
|
15299
16127
|
const parsed = JSON.parse(raw);
|
|
15300
16128
|
if (parsed.models) {
|
|
15301
16129
|
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
@@ -15315,15 +16143,15 @@ var ModelPerformanceTracker = class {
|
|
|
15315
16143
|
async save() {
|
|
15316
16144
|
if (this.readOnly) return;
|
|
15317
16145
|
try {
|
|
15318
|
-
await
|
|
16146
|
+
await fs11__default.default.mkdir(path33__default.default.dirname(this.statsFile), { recursive: true });
|
|
15319
16147
|
const modelsObj = {};
|
|
15320
16148
|
const featuresObj = {};
|
|
15321
16149
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
15322
16150
|
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
15323
16151
|
const json = JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2);
|
|
15324
16152
|
const tmp = `${this.statsFile}.${process.pid}.${Date.now()}.tmp`;
|
|
15325
|
-
await
|
|
15326
|
-
await
|
|
16153
|
+
await fs11__default.default.writeFile(tmp, json, "utf-8");
|
|
16154
|
+
await fs11__default.default.rename(tmp, this.statsFile);
|
|
15327
16155
|
} catch {
|
|
15328
16156
|
}
|
|
15329
16157
|
}
|
|
@@ -15863,9 +16691,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
15863
16691
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
15864
16692
|
async loadPersistedTools() {
|
|
15865
16693
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
15866
|
-
const file =
|
|
16694
|
+
const file = path33__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
15867
16695
|
try {
|
|
15868
|
-
const raw = await
|
|
16696
|
+
const raw = await fs11__default.default.readFile(file, "utf-8");
|
|
15869
16697
|
const specs = JSON.parse(raw);
|
|
15870
16698
|
if (!Array.isArray(specs)) return;
|
|
15871
16699
|
let loaded = 0;
|
|
@@ -15887,11 +16715,11 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
15887
16715
|
}
|
|
15888
16716
|
async persist() {
|
|
15889
16717
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
15890
|
-
const dir =
|
|
15891
|
-
const file =
|
|
16718
|
+
const dir = path33__default.default.join(this.workspacePath, ".cascade");
|
|
16719
|
+
const file = path33__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
15892
16720
|
try {
|
|
15893
|
-
await
|
|
15894
|
-
await
|
|
16721
|
+
await fs11__default.default.mkdir(dir, { recursive: true });
|
|
16722
|
+
await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
15895
16723
|
} catch (err) {
|
|
15896
16724
|
this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
15897
16725
|
}
|
|
@@ -15908,12 +16736,12 @@ var WorldStateDB = class {
|
|
|
15908
16736
|
constructor(workspacePath, debugMode = false) {
|
|
15909
16737
|
this.workspacePath = workspacePath;
|
|
15910
16738
|
this.debugMode = debugMode;
|
|
15911
|
-
const cascadeDir =
|
|
15912
|
-
if (!
|
|
15913
|
-
|
|
16739
|
+
const cascadeDir = path33__default.default.join(workspacePath, ".cascade");
|
|
16740
|
+
if (!fs30__default.default.existsSync(cascadeDir)) {
|
|
16741
|
+
fs30__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
15914
16742
|
}
|
|
15915
|
-
this.keyPath =
|
|
15916
|
-
this.dbPath =
|
|
16743
|
+
this.keyPath = path33__default.default.join(cascadeDir, "world_state.key");
|
|
16744
|
+
this.dbPath = path33__default.default.join(cascadeDir, "world_state.db");
|
|
15917
16745
|
this.initEncryptionKey();
|
|
15918
16746
|
this.db = new Database2__default.default(this.dbPath);
|
|
15919
16747
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -15970,11 +16798,11 @@ var WorldStateDB = class {
|
|
|
15970
16798
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
15971
16799
|
}
|
|
15972
16800
|
initEncryptionKey() {
|
|
15973
|
-
if (
|
|
15974
|
-
this.encryptionKey =
|
|
16801
|
+
if (fs30__default.default.existsSync(this.keyPath)) {
|
|
16802
|
+
this.encryptionKey = fs30__default.default.readFileSync(this.keyPath);
|
|
15975
16803
|
} else {
|
|
15976
16804
|
this.encryptionKey = crypto__default.default.randomBytes(32);
|
|
15977
|
-
|
|
16805
|
+
fs30__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
15978
16806
|
}
|
|
15979
16807
|
}
|
|
15980
16808
|
encrypt(text) {
|
|
@@ -16260,9 +17088,9 @@ var WorldStateDB = class {
|
|
|
16260
17088
|
dumpDebugIfNeeded() {
|
|
16261
17089
|
if (!this.debugMode) return;
|
|
16262
17090
|
try {
|
|
16263
|
-
const dumpPath =
|
|
17091
|
+
const dumpPath = path33__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
16264
17092
|
const entries = this.getAllEntries();
|
|
16265
|
-
|
|
17093
|
+
fs30__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
16266
17094
|
} catch (err) {
|
|
16267
17095
|
console.error("Failed to dump debug world state", err);
|
|
16268
17096
|
}
|
|
@@ -16632,7 +17460,7 @@ var WorkspaceIndex = class {
|
|
|
16632
17460
|
isIgnored;
|
|
16633
17461
|
chunker;
|
|
16634
17462
|
constructor(opts) {
|
|
16635
|
-
this.root =
|
|
17463
|
+
this.root = path33__default.default.resolve(opts.root);
|
|
16636
17464
|
this.db = opts.db;
|
|
16637
17465
|
this.namespace = opts.namespace ?? "code";
|
|
16638
17466
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -16659,19 +17487,19 @@ var WorkspaceIndex = class {
|
|
|
16659
17487
|
const walk = async (dir) => {
|
|
16660
17488
|
let entries;
|
|
16661
17489
|
try {
|
|
16662
|
-
entries = await
|
|
17490
|
+
entries = await fs11__default.default.readdir(dir, { withFileTypes: true });
|
|
16663
17491
|
} catch {
|
|
16664
17492
|
return;
|
|
16665
17493
|
}
|
|
16666
17494
|
for (const entry of entries) {
|
|
16667
|
-
const abs =
|
|
17495
|
+
const abs = path33__default.default.join(dir, entry.name);
|
|
16668
17496
|
if (entry.isDirectory()) {
|
|
16669
17497
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
16670
17498
|
await walk(abs);
|
|
16671
17499
|
} else if (entry.isFile()) {
|
|
16672
|
-
const ext =
|
|
17500
|
+
const ext = path33__default.default.extname(entry.name).slice(1).toLowerCase();
|
|
16673
17501
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
16674
|
-
out.push(
|
|
17502
|
+
out.push(path33__default.default.relative(this.root, abs));
|
|
16675
17503
|
}
|
|
16676
17504
|
}
|
|
16677
17505
|
};
|
|
@@ -16701,10 +17529,10 @@ var WorkspaceIndex = class {
|
|
|
16701
17529
|
const entries = [];
|
|
16702
17530
|
for (const rel of rels) {
|
|
16703
17531
|
try {
|
|
16704
|
-
const abs =
|
|
16705
|
-
const stat = await
|
|
17532
|
+
const abs = path33__default.default.join(this.root, rel);
|
|
17533
|
+
const stat = await fs11__default.default.stat(abs);
|
|
16706
17534
|
if (stat.size > this.maxFileBytes) continue;
|
|
16707
|
-
const buf = await
|
|
17535
|
+
const buf = await fs11__default.default.readFile(abs);
|
|
16708
17536
|
if (buf.includes(0)) continue;
|
|
16709
17537
|
entries.push({ path: rel, hash: hashContent(buf), text: buf.toString("utf8") });
|
|
16710
17538
|
} catch {
|
|
@@ -17273,7 +18101,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
17273
18101
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
17274
18102
|
try {
|
|
17275
18103
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
17276
|
-
fileDeadModelPersistence(
|
|
18104
|
+
fileDeadModelPersistence(path33__default.default.join(workspacePath, ".cascade", "dead-models.json"))
|
|
17277
18105
|
));
|
|
17278
18106
|
} catch {
|
|
17279
18107
|
}
|
|
@@ -17281,6 +18109,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
17281
18109
|
(this.config.providers ?? []).map((p) => p.type)
|
|
17282
18110
|
);
|
|
17283
18111
|
this.registerMediaTools(workspacePath);
|
|
18112
|
+
this.registerDocumentTools(workspacePath);
|
|
17284
18113
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
17285
18114
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
17286
18115
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -17360,8 +18189,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
17360
18189
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
17361
18190
|
return;
|
|
17362
18191
|
}
|
|
17363
|
-
const dbPath = ci.dbPath ||
|
|
17364
|
-
|
|
18192
|
+
const dbPath = ci.dbPath || path33__default.default.join(this.workspacePath, ".cascade", "code-index.db");
|
|
18193
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
|
|
17365
18194
|
const db = new Database2__default.default(dbPath);
|
|
17366
18195
|
db.pragma("journal_mode = WAL");
|
|
17367
18196
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -17701,6 +18530,35 @@ ${input}` }],
|
|
|
17701
18530
|
this.toolRegistry.register(tool);
|
|
17702
18531
|
}
|
|
17703
18532
|
}
|
|
18533
|
+
/**
|
|
18534
|
+
* Register `generate_document` where there is a real workspace to write into.
|
|
18535
|
+
*
|
|
18536
|
+
* A `.docx`/`.pptx`/`.xlsx` is a ZIP of OOXML parts, so `file_write` — which
|
|
18537
|
+
* does exactly what it says and writes the model's text verbatim — produced
|
|
18538
|
+
* files Word/Excel/PowerPoint correctly reported as corrupted. This tool owns
|
|
18539
|
+
* the conversion (see tools/generate-document.ts for why it isn't folded into
|
|
18540
|
+
* file_write).
|
|
18541
|
+
*
|
|
18542
|
+
* Registered OUTSIDE `enabledTools` for the same reason the media tools are:
|
|
18543
|
+
* that allowlist is a blast-radius control for tools that reach the machine
|
|
18544
|
+
* or the network, and this one only writes the artifact the user asked for,
|
|
18545
|
+
* into the workspace the run already owns. `disabledTools` still removes it.
|
|
18546
|
+
* A host with no filesystem (the cloud) has its own browser-side exporter and
|
|
18547
|
+
* simply never reaches this path — `buildDocumentTools` returns nothing
|
|
18548
|
+
* without a file reader, mirroring `transcribe_audio`.
|
|
18549
|
+
*/
|
|
18550
|
+
registerDocumentTools(workspacePath) {
|
|
18551
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
18552
|
+
const tools = buildDocumentTools(async (p) => {
|
|
18553
|
+
const fsp = await import('fs/promises');
|
|
18554
|
+
return new Uint8Array(await fsp.readFile(p));
|
|
18555
|
+
});
|
|
18556
|
+
for (const tool of tools) {
|
|
18557
|
+
if (denied.has(tool.name)) continue;
|
|
18558
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
18559
|
+
this.toolRegistry.register(tool);
|
|
18560
|
+
}
|
|
18561
|
+
}
|
|
17704
18562
|
/**
|
|
17705
18563
|
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
17706
18564
|
* have actually performed for this user, on top of public benchmarks.
|
|
@@ -18613,14 +19471,14 @@ ${finalOutput}` : ""
|
|
|
18613
19471
|
init_constants();
|
|
18614
19472
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
18615
19473
|
function globalDir(homeDir = os10__default.default.homedir()) {
|
|
18616
|
-
return
|
|
19474
|
+
return path33__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
18617
19475
|
}
|
|
18618
19476
|
function cloudSessionPath(dir = globalDir()) {
|
|
18619
|
-
return
|
|
19477
|
+
return path33__default.default.join(dir, CLOUD_SESSION_FILE);
|
|
18620
19478
|
}
|
|
18621
19479
|
function loadCloudSession(dir = globalDir()) {
|
|
18622
19480
|
try {
|
|
18623
|
-
const raw =
|
|
19481
|
+
const raw = fs30__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
18624
19482
|
const parsed = JSON.parse(raw);
|
|
18625
19483
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
18626
19484
|
return parsed;
|
|
@@ -18629,17 +19487,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
18629
19487
|
}
|
|
18630
19488
|
}
|
|
18631
19489
|
function saveCloudSession(session, dir = globalDir()) {
|
|
18632
|
-
|
|
19490
|
+
fs30__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
18633
19491
|
const file = cloudSessionPath(dir);
|
|
18634
|
-
|
|
19492
|
+
fs30__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
18635
19493
|
try {
|
|
18636
|
-
|
|
19494
|
+
fs30__default.default.chmodSync(file, 384);
|
|
18637
19495
|
} catch {
|
|
18638
19496
|
}
|
|
18639
19497
|
}
|
|
18640
19498
|
function clearCloudSession(dir = globalDir()) {
|
|
18641
19499
|
try {
|
|
18642
|
-
|
|
19500
|
+
fs30__default.default.rmSync(cloudSessionPath(dir));
|
|
18643
19501
|
} catch {
|
|
18644
19502
|
}
|
|
18645
19503
|
}
|
|
@@ -18811,16 +19669,16 @@ var CloudClient = class _CloudClient {
|
|
|
18811
19669
|
return (await this.refresh()).accessToken;
|
|
18812
19670
|
}
|
|
18813
19671
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
18814
|
-
async authed(
|
|
18815
|
-
const call = (token) => fetch(this.url(
|
|
19672
|
+
async authed(path38, init = {}) {
|
|
19673
|
+
const call = (token) => fetch(this.url(path38), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
18816
19674
|
let res = await call(await this.accessToken());
|
|
18817
19675
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
18818
19676
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
18819
19677
|
const text = await res.text();
|
|
18820
19678
|
return text ? JSON.parse(text) : {};
|
|
18821
19679
|
}
|
|
18822
|
-
authedGet(
|
|
18823
|
-
return this.authed(
|
|
19680
|
+
authedGet(path38) {
|
|
19681
|
+
return this.authed(path38);
|
|
18824
19682
|
}
|
|
18825
19683
|
async fetchMe(token) {
|
|
18826
19684
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -20595,10 +21453,10 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20595
21453
|
}
|
|
20596
21454
|
} catch {
|
|
20597
21455
|
}
|
|
20598
|
-
const configPath =
|
|
21456
|
+
const configPath = path33__default.default.join(workspacePath, ".cascade", "config.json");
|
|
20599
21457
|
try {
|
|
20600
|
-
await
|
|
20601
|
-
await
|
|
21458
|
+
await fs11__default.default.mkdir(path33__default.default.dirname(configPath), { recursive: true });
|
|
21459
|
+
await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
20602
21460
|
} catch (err) {
|
|
20603
21461
|
const msg = err instanceof Error ? err.message : String(err);
|
|
20604
21462
|
dispatch({
|
|
@@ -20649,9 +21507,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20649
21507
|
if (msg.includes("non-text parts")) return;
|
|
20650
21508
|
originalLog(...args);
|
|
20651
21509
|
};
|
|
20652
|
-
const store = new MemoryStore(
|
|
21510
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
20653
21511
|
storeRef.current = store;
|
|
20654
|
-
globalStoreRef.current = new MemoryStore(
|
|
21512
|
+
globalStoreRef.current = new MemoryStore(path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
20655
21513
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
20656
21514
|
setIdentities(identityRows);
|
|
20657
21515
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -20719,14 +21577,14 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20719
21577
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
20720
21578
|
onExport: async (fmt) => {
|
|
20721
21579
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20722
|
-
const exportPath =
|
|
21580
|
+
const exportPath = path33__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
20723
21581
|
if (fmt === "json") {
|
|
20724
|
-
await
|
|
21582
|
+
await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
20725
21583
|
} else {
|
|
20726
21584
|
const markdown = state.messages.map((msg) => `## ${msg.role.toUpperCase()} \u2014 ${msg.timestamp}
|
|
20727
21585
|
|
|
20728
21586
|
${msg.content}`).join("\n\n");
|
|
20729
|
-
await
|
|
21587
|
+
await fs11__default.default.writeFile(exportPath, markdown, "utf-8");
|
|
20730
21588
|
}
|
|
20731
21589
|
},
|
|
20732
21590
|
onRollback: async () => {
|
|
@@ -20736,7 +21594,7 @@ ${msg.content}`).join("\n\n");
|
|
|
20736
21594
|
if (!snapshots.length) return "No file snapshots found for this session.";
|
|
20737
21595
|
for (const { filePath, content } of snapshots) {
|
|
20738
21596
|
try {
|
|
20739
|
-
await
|
|
21597
|
+
await fs11__default.default.writeFile(filePath, content, "utf-8");
|
|
20740
21598
|
} catch (err) {
|
|
20741
21599
|
console.error(`Restore failed: ${filePath}`, err);
|
|
20742
21600
|
}
|
|
@@ -21617,9 +22475,9 @@ function stringifySlashOutput(val) {
|
|
|
21617
22475
|
}
|
|
21618
22476
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
21619
22477
|
if (!query) return "Usage: /search <query>";
|
|
21620
|
-
const dbPath =
|
|
22478
|
+
const dbPath = path33__default.default.join(workspacePath, CASCADE_DB_FILE);
|
|
21621
22479
|
try {
|
|
21622
|
-
await
|
|
22480
|
+
await fs11__default.default.access(dbPath);
|
|
21623
22481
|
} catch {
|
|
21624
22482
|
return "No database found. Start a conversation first.";
|
|
21625
22483
|
}
|
|
@@ -21651,7 +22509,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
21651
22509
|
async function diagnoseRuntime(config, workspacePath) {
|
|
21652
22510
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
21653
22511
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
21654
|
-
const store = new MemoryStore(
|
|
22512
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
21655
22513
|
try {
|
|
21656
22514
|
const sessions2 = store.listSessions(void 0, 3);
|
|
21657
22515
|
return [
|
|
@@ -21670,7 +22528,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
21670
22528
|
}
|
|
21671
22529
|
async function showRecentLogs(args, workspacePath) {
|
|
21672
22530
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
21673
|
-
const store = new MemoryStore(
|
|
22531
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
21674
22532
|
try {
|
|
21675
22533
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
21676
22534
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -21682,7 +22540,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
21682
22540
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
21683
22541
|
const sessionId = args[0];
|
|
21684
22542
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
21685
|
-
const store = new MemoryStore(
|
|
22543
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
21686
22544
|
try {
|
|
21687
22545
|
const session = store.getSession(sessionId);
|
|
21688
22546
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -22110,10 +22968,10 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
22110
22968
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
22111
22969
|
};
|
|
22112
22970
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
22113
|
-
const configDir =
|
|
22114
|
-
await
|
|
22115
|
-
const configPath =
|
|
22116
|
-
await
|
|
22971
|
+
const configDir = path33__default.default.join(workspacePath, ".cascade");
|
|
22972
|
+
await fs11__default.default.mkdir(configDir, { recursive: true });
|
|
22973
|
+
const configPath = path33__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22974
|
+
await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
22117
22975
|
savedConfigRef.current = config;
|
|
22118
22976
|
dispatchRef.current({ type: "GO_DONE" });
|
|
22119
22977
|
} catch (err) {
|
|
@@ -22454,14 +23312,14 @@ function printTelemetryBanner() {
|
|
|
22454
23312
|
async function initCommand(workspacePath = process.cwd()) {
|
|
22455
23313
|
const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
22456
23314
|
try {
|
|
22457
|
-
const configDir =
|
|
22458
|
-
await
|
|
22459
|
-
const mdPath =
|
|
23315
|
+
const configDir = path33__default.default.join(workspacePath, ".cascade");
|
|
23316
|
+
await fs11__default.default.mkdir(configDir, { recursive: true });
|
|
23317
|
+
const mdPath = path33__default.default.join(workspacePath, "CASCADE.md");
|
|
22460
23318
|
if (!await fileExists(mdPath)) {
|
|
22461
23319
|
await createDefaultCascadeMd(workspacePath);
|
|
22462
23320
|
spin.succeed(chalk7__default.default.green("Created CASCADE.md"));
|
|
22463
23321
|
}
|
|
22464
|
-
const ignorePath =
|
|
23322
|
+
const ignorePath = path33__default.default.join(workspacePath, ".cascadeignore");
|
|
22465
23323
|
if (!await fileExists(ignorePath)) {
|
|
22466
23324
|
await createDefaultIgnoreFile(workspacePath);
|
|
22467
23325
|
spin.succeed(chalk7__default.default.green("Created .cascadeignore"));
|
|
@@ -22470,7 +23328,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
22470
23328
|
console.log();
|
|
22471
23329
|
console.log(chalk7__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
22472
23330
|
console.log();
|
|
22473
|
-
const configPath =
|
|
23331
|
+
const configPath = path33__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22474
23332
|
if (await fileExists(configPath)) {
|
|
22475
23333
|
console.log(chalk7__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
22476
23334
|
console.log();
|
|
@@ -22489,7 +23347,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
22489
23347
|
}
|
|
22490
23348
|
async function fileExists(p) {
|
|
22491
23349
|
try {
|
|
22492
|
-
await
|
|
23350
|
+
await fs11__default.default.access(p);
|
|
22493
23351
|
return true;
|
|
22494
23352
|
} catch {
|
|
22495
23353
|
return false;
|
|
@@ -22501,7 +23359,7 @@ init_constants();
|
|
|
22501
23359
|
var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
|
|
22502
23360
|
async function readJson(file) {
|
|
22503
23361
|
try {
|
|
22504
|
-
const raw = await
|
|
23362
|
+
const raw = await fs11__default.default.readFile(file, "utf-8");
|
|
22505
23363
|
return JSON.parse(raw);
|
|
22506
23364
|
} catch {
|
|
22507
23365
|
return null;
|
|
@@ -22528,7 +23386,7 @@ function fromEnv(env) {
|
|
|
22528
23386
|
return out;
|
|
22529
23387
|
}
|
|
22530
23388
|
async function fromClaudeCode(home) {
|
|
22531
|
-
const file =
|
|
23389
|
+
const file = path33__default.default.join(home, ".claude", ".credentials.json");
|
|
22532
23390
|
const data = await readJson(file);
|
|
22533
23391
|
if (!data) return [];
|
|
22534
23392
|
const oauth = data["claudeAiOauth"];
|
|
@@ -22551,7 +23409,7 @@ async function fromClaudeCode(home) {
|
|
|
22551
23409
|
return [];
|
|
22552
23410
|
}
|
|
22553
23411
|
async function fromCodex(home) {
|
|
22554
|
-
const file =
|
|
23412
|
+
const file = path33__default.default.join(home, ".codex", "auth.json");
|
|
22555
23413
|
const data = await readJson(file);
|
|
22556
23414
|
if (!data) return [];
|
|
22557
23415
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -22574,7 +23432,7 @@ async function fromCodex(home) {
|
|
|
22574
23432
|
return [];
|
|
22575
23433
|
}
|
|
22576
23434
|
async function fromGemini(home) {
|
|
22577
|
-
const file =
|
|
23435
|
+
const file = path33__default.default.join(home, ".gemini", "oauth_creds.json");
|
|
22578
23436
|
const data = await readJson(file);
|
|
22579
23437
|
const accessToken = str(data?.["access_token"]);
|
|
22580
23438
|
if (!accessToken) return [];
|
|
@@ -22590,7 +23448,7 @@ async function fromGemini(home) {
|
|
|
22590
23448
|
}
|
|
22591
23449
|
async function fromCopilot(home) {
|
|
22592
23450
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
22593
|
-
const file =
|
|
23451
|
+
const file = path33__default.default.join(home, ".config", "github-copilot", name);
|
|
22594
23452
|
const data = await readJson(file);
|
|
22595
23453
|
if (!data) continue;
|
|
22596
23454
|
for (const value of Object.values(data)) {
|
|
@@ -22644,7 +23502,7 @@ async function doctorCommand() {
|
|
|
22644
23502
|
checks.push({
|
|
22645
23503
|
label: "Cascade config",
|
|
22646
23504
|
ok: true,
|
|
22647
|
-
detail: `Loaded ${
|
|
23505
|
+
detail: `Loaded ${path33__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
22648
23506
|
});
|
|
22649
23507
|
const providers = [
|
|
22650
23508
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -22755,7 +23613,7 @@ async function checkEndpoint(url) {
|
|
|
22755
23613
|
}
|
|
22756
23614
|
}
|
|
22757
23615
|
async function indexCommand(dirPath) {
|
|
22758
|
-
const workspace =
|
|
23616
|
+
const workspace = path33__default.default.resolve(dirPath || process.cwd());
|
|
22759
23617
|
const cm = new ConfigManager(workspace);
|
|
22760
23618
|
await cm.load();
|
|
22761
23619
|
const config = cm.getConfig();
|
|
@@ -22766,8 +23624,8 @@ async function indexCommand(dirPath) {
|
|
|
22766
23624
|
process.exitCode = 1;
|
|
22767
23625
|
return;
|
|
22768
23626
|
}
|
|
22769
|
-
const dbPath = config.codeIndex?.dbPath ||
|
|
22770
|
-
|
|
23627
|
+
const dbPath = config.codeIndex?.dbPath || path33__default.default.join(workspace, ".cascade", "code-index.db");
|
|
23628
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
|
|
22771
23629
|
const db = new Database2__default.default(dbPath);
|
|
22772
23630
|
db.pragma("journal_mode = WAL");
|
|
22773
23631
|
const complete = chatCompleterFromProviders(config.providers);
|
|
@@ -23256,7 +24114,7 @@ function openBrowser2(url) {
|
|
|
23256
24114
|
}
|
|
23257
24115
|
function storePathFor(name) {
|
|
23258
24116
|
const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
|
|
23259
|
-
return
|
|
24117
|
+
return path33__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
|
|
23260
24118
|
}
|
|
23261
24119
|
function defaultName(url) {
|
|
23262
24120
|
try {
|
|
@@ -23758,7 +24616,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
23758
24616
|
}
|
|
23759
24617
|
|
|
23760
24618
|
// src/dashboard/server.ts
|
|
23761
|
-
var __dirname$1 =
|
|
24619
|
+
var __dirname$1 = path33__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
23762
24620
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
23763
24621
|
var DashboardServer = class {
|
|
23764
24622
|
app;
|
|
@@ -24038,14 +24896,14 @@ var DashboardServer = class {
|
|
|
24038
24896
|
*/
|
|
24039
24897
|
persistConfig() {
|
|
24040
24898
|
try {
|
|
24041
|
-
const configPath =
|
|
24042
|
-
|
|
24043
|
-
|
|
24899
|
+
const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
24900
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(configPath), { recursive: true });
|
|
24901
|
+
fs30__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
24044
24902
|
} catch (err) {
|
|
24045
24903
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
24046
24904
|
}
|
|
24047
24905
|
try {
|
|
24048
|
-
saveGlobalCredentials(
|
|
24906
|
+
saveGlobalCredentials(path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
24049
24907
|
} catch (err) {
|
|
24050
24908
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
24051
24909
|
}
|
|
@@ -24060,15 +24918,15 @@ var DashboardServer = class {
|
|
|
24060
24918
|
resolveDashboardSecret() {
|
|
24061
24919
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
24062
24920
|
if (fromConfig) return fromConfig;
|
|
24063
|
-
const secretPath =
|
|
24921
|
+
const secretPath = path33__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
24064
24922
|
try {
|
|
24065
|
-
if (
|
|
24066
|
-
const existing =
|
|
24923
|
+
if (fs30__default.default.existsSync(secretPath)) {
|
|
24924
|
+
const existing = fs30__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
24067
24925
|
if (existing.length >= 16) return existing;
|
|
24068
24926
|
}
|
|
24069
24927
|
const generated = crypto.randomUUID();
|
|
24070
|
-
|
|
24071
|
-
|
|
24928
|
+
fs30__default.default.mkdirSync(path33__default.default.dirname(secretPath), { recursive: true });
|
|
24929
|
+
fs30__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
24072
24930
|
if (this.config.dashboard.auth) {
|
|
24073
24931
|
console.warn(
|
|
24074
24932
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -24095,7 +24953,7 @@ var DashboardServer = class {
|
|
|
24095
24953
|
// ── Setup ─────────────────────────────────────
|
|
24096
24954
|
getGlobalStore() {
|
|
24097
24955
|
if (!this.globalStore) {
|
|
24098
|
-
const globalDbPath =
|
|
24956
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24099
24957
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
24100
24958
|
}
|
|
24101
24959
|
return this.globalStore;
|
|
@@ -24475,12 +25333,12 @@ ${prompt}`;
|
|
|
24475
25333
|
}
|
|
24476
25334
|
}
|
|
24477
25335
|
watchRuntimeChanges() {
|
|
24478
|
-
const workspaceDbPath =
|
|
24479
|
-
const globalDbPath =
|
|
25336
|
+
const workspaceDbPath = path33__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
25337
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24480
25338
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
24481
25339
|
for (const watchPath of watchPaths) {
|
|
24482
|
-
if (!
|
|
24483
|
-
|
|
25340
|
+
if (!fs30__default.default.existsSync(watchPath)) continue;
|
|
25341
|
+
fs30__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
24484
25342
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
24485
25343
|
});
|
|
24486
25344
|
}
|
|
@@ -24602,7 +25460,7 @@ ${prompt}`;
|
|
|
24602
25460
|
const sessionId = req.params.id;
|
|
24603
25461
|
this.store.deleteSession(sessionId);
|
|
24604
25462
|
this.store.deleteRuntimeSession(sessionId);
|
|
24605
|
-
const globalDbPath =
|
|
25463
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24606
25464
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24607
25465
|
try {
|
|
24608
25466
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -24695,7 +25553,7 @@ ${prompt}`;
|
|
|
24695
25553
|
});
|
|
24696
25554
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
24697
25555
|
const body = req.body;
|
|
24698
|
-
const globalDbPath =
|
|
25556
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24699
25557
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
24700
25558
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24701
25559
|
try {
|
|
@@ -24718,7 +25576,7 @@ ${prompt}`;
|
|
|
24718
25576
|
});
|
|
24719
25577
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
24720
25578
|
this.store.deleteAllRuntimeNodes();
|
|
24721
|
-
const globalDbPath =
|
|
25579
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24722
25580
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24723
25581
|
try {
|
|
24724
25582
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -24804,12 +25662,12 @@ ${prompt}`;
|
|
|
24804
25662
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
24805
25663
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
24806
25664
|
try {
|
|
24807
|
-
const configPath =
|
|
24808
|
-
const existing =
|
|
25665
|
+
const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
25666
|
+
const existing = fs30__default.default.existsSync(configPath) ? JSON.parse(fs30__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
24809
25667
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
24810
25668
|
const tmp = configPath + ".tmp";
|
|
24811
|
-
|
|
24812
|
-
|
|
25669
|
+
fs30__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
25670
|
+
fs30__default.default.renameSync(tmp, configPath);
|
|
24813
25671
|
res.json({ ok: true });
|
|
24814
25672
|
} catch (err) {
|
|
24815
25673
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -24837,7 +25695,7 @@ ${prompt}`;
|
|
|
24837
25695
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
24838
25696
|
const scope = req.query["scope"] ?? "workspace";
|
|
24839
25697
|
if (scope === "global") {
|
|
24840
|
-
const globalDbPath =
|
|
25698
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24841
25699
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24842
25700
|
try {
|
|
24843
25701
|
res.json({
|
|
@@ -25178,13 +26036,13 @@ ${prompt}`;
|
|
|
25178
26036
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
25179
26037
|
}
|
|
25180
26038
|
});
|
|
25181
|
-
const prodPath =
|
|
25182
|
-
const devPath =
|
|
25183
|
-
const webDistPath =
|
|
25184
|
-
if (
|
|
26039
|
+
const prodPath = path33__default.default.resolve(__dirname$1, "../web/dist");
|
|
26040
|
+
const devPath = path33__default.default.resolve(__dirname$1, "../../web/dist");
|
|
26041
|
+
const webDistPath = fs30__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
26042
|
+
if (fs30__default.default.existsSync(webDistPath)) {
|
|
25185
26043
|
this.app.use(express__default.default.static(webDistPath));
|
|
25186
26044
|
this.app.get("*", (_req, res) => {
|
|
25187
|
-
res.sendFile(
|
|
26045
|
+
res.sendFile(path33__default.default.join(webDistPath, "index.html"));
|
|
25188
26046
|
});
|
|
25189
26047
|
} else {
|
|
25190
26048
|
this.app.get("/", (_req, res) => {
|
|
@@ -25205,7 +26063,7 @@ init_constants();
|
|
|
25205
26063
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
25206
26064
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
25207
26065
|
const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
25208
|
-
const store = new MemoryStore(
|
|
26066
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
25209
26067
|
const server = new DashboardServer(config, store, workspacePath);
|
|
25210
26068
|
server.watchRuntimeChanges();
|
|
25211
26069
|
const gThis = globalThis;
|
|
@@ -25237,7 +26095,7 @@ init_constants();
|
|
|
25237
26095
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
25238
26096
|
const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
|
|
25239
26097
|
identity.command("list").description("List all available identities").action(() => {
|
|
25240
|
-
const store = new MemoryStore(
|
|
26098
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
25241
26099
|
try {
|
|
25242
26100
|
const identities = store.listIdentities();
|
|
25243
26101
|
console.log(chalk7__default.default.bold("\n Identities:"));
|
|
@@ -25257,7 +26115,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
25257
26115
|
}
|
|
25258
26116
|
});
|
|
25259
26117
|
identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
|
|
25260
|
-
const store = new MemoryStore(
|
|
26118
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
25261
26119
|
try {
|
|
25262
26120
|
if (options.default) {
|
|
25263
26121
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -25283,7 +26141,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
25283
26141
|
}
|
|
25284
26142
|
});
|
|
25285
26143
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
25286
|
-
const store = new MemoryStore(
|
|
26144
|
+
const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
25287
26145
|
try {
|
|
25288
26146
|
const identities = store.listIdentities();
|
|
25289
26147
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -25432,11 +26290,11 @@ async function exportCommand(options = {}) {
|
|
|
25432
26290
|
let store;
|
|
25433
26291
|
try {
|
|
25434
26292
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
25435
|
-
const workspaceDbPath =
|
|
25436
|
-
const globalDbPath =
|
|
26293
|
+
const workspaceDbPath = path33__default.default.join(workspacePath, CASCADE_DB_FILE);
|
|
26294
|
+
const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
25437
26295
|
let dbPath = globalDbPath;
|
|
25438
26296
|
try {
|
|
25439
|
-
await
|
|
26297
|
+
await fs11__default.default.access(workspaceDbPath);
|
|
25440
26298
|
dbPath = workspaceDbPath;
|
|
25441
26299
|
} catch {
|
|
25442
26300
|
}
|
|
@@ -25475,8 +26333,8 @@ async function exportCommand(options = {}) {
|
|
|
25475
26333
|
const ext = format === "json" ? ".json" : ".md";
|
|
25476
26334
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
25477
26335
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
25478
|
-
const outPath = options.output ?
|
|
25479
|
-
await
|
|
26336
|
+
const outPath = options.output ? path33__default.default.resolve(options.output) : path33__default.default.join(process.cwd(), defaultFile);
|
|
26337
|
+
await fs11__default.default.writeFile(outPath, content, "utf-8");
|
|
25480
26338
|
spin.succeed(chalk7__default.default.green(`Exported to ${chalk7__default.default.white(outPath)}`));
|
|
25481
26339
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
25482
26340
|
console.log();
|
|
@@ -25703,11 +26561,11 @@ async function statsCommand() {
|
|
|
25703
26561
|
dotenv__default.default.config();
|
|
25704
26562
|
function warnIfBuildIsStale() {
|
|
25705
26563
|
try {
|
|
25706
|
-
const here =
|
|
26564
|
+
const here = path33__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
25707
26565
|
for (const rel of ["..", "../.."]) {
|
|
25708
|
-
const pkgPath =
|
|
25709
|
-
if (!
|
|
25710
|
-
const pkg = JSON.parse(
|
|
26566
|
+
const pkgPath = path33__default.default.join(here, rel, "package.json");
|
|
26567
|
+
if (!fs30__default.default.existsSync(pkgPath)) continue;
|
|
26568
|
+
const pkg = JSON.parse(fs30__default.default.readFileSync(pkgPath, "utf-8"));
|
|
25711
26569
|
if (pkg.name !== "cascade-ai") continue;
|
|
25712
26570
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
25713
26571
|
console.error(
|