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.js
CHANGED
|
@@ -7,8 +7,8 @@ import https from 'https';
|
|
|
7
7
|
import zlib from 'zlib';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
9
|
import Database2 from 'better-sqlite3';
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import path33, { dirname } from 'path';
|
|
11
|
+
import fs30, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
12
12
|
import os10 from 'os';
|
|
13
13
|
import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
|
|
14
14
|
import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
|
|
@@ -17,7 +17,7 @@ import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo }
|
|
|
17
17
|
import chalk7 from 'chalk';
|
|
18
18
|
import dotenv from 'dotenv';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
|
-
import
|
|
20
|
+
import fs11 from 'fs/promises';
|
|
21
21
|
import * as _ignoreModule from 'ignore';
|
|
22
22
|
import _ignoreModule__default from 'ignore';
|
|
23
23
|
import { z } from 'zod';
|
|
@@ -1757,7 +1757,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1757
1757
|
var init_constants = __esm({
|
|
1758
1758
|
"src/constants.ts"() {
|
|
1759
1759
|
init_pricing();
|
|
1760
|
-
CASCADE_VERSION = "0.
|
|
1760
|
+
CASCADE_VERSION = "0.67.0";
|
|
1761
1761
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1762
1762
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1763
1763
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -3879,12 +3879,12 @@ var init_audit_logger = __esm({
|
|
|
3879
3879
|
constructor(workspacePath, debugMode = false) {
|
|
3880
3880
|
this.workspacePath = workspacePath;
|
|
3881
3881
|
this.debugMode = debugMode;
|
|
3882
|
-
const cascadeDir =
|
|
3883
|
-
if (!
|
|
3884
|
-
|
|
3882
|
+
const cascadeDir = path33.join(workspacePath, ".cascade");
|
|
3883
|
+
if (!fs30.existsSync(cascadeDir)) {
|
|
3884
|
+
fs30.mkdirSync(cascadeDir, { recursive: true });
|
|
3885
3885
|
}
|
|
3886
|
-
this.keyPath =
|
|
3887
|
-
this.dbPath =
|
|
3886
|
+
this.keyPath = path33.join(cascadeDir, "audit_log.key");
|
|
3887
|
+
this.dbPath = path33.join(cascadeDir, "audit_log.db");
|
|
3888
3888
|
this.initEncryptionKey();
|
|
3889
3889
|
this.db = new Database2(this.dbPath);
|
|
3890
3890
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -3914,11 +3914,11 @@ var init_audit_logger = __esm({
|
|
|
3914
3914
|
return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
3915
3915
|
}
|
|
3916
3916
|
initEncryptionKey() {
|
|
3917
|
-
if (
|
|
3918
|
-
this.encryptionKey =
|
|
3917
|
+
if (fs30.existsSync(this.keyPath)) {
|
|
3918
|
+
this.encryptionKey = fs30.readFileSync(this.keyPath);
|
|
3919
3919
|
} else {
|
|
3920
3920
|
this.encryptionKey = crypto.randomBytes(32);
|
|
3921
|
-
|
|
3921
|
+
fs30.writeFileSync(this.keyPath, this.encryptionKey);
|
|
3922
3922
|
}
|
|
3923
3923
|
}
|
|
3924
3924
|
encrypt(text) {
|
|
@@ -3996,9 +3996,9 @@ var init_audit_logger = __esm({
|
|
|
3996
3996
|
dumpDebugIfNeeded() {
|
|
3997
3997
|
if (!this.debugMode) return;
|
|
3998
3998
|
try {
|
|
3999
|
-
const dumpPath =
|
|
3999
|
+
const dumpPath = path33.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
|
|
4000
4000
|
const entries = this.getAllLogs();
|
|
4001
|
-
|
|
4001
|
+
fs30.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
4002
4002
|
} catch (err) {
|
|
4003
4003
|
console.error("Failed to dump debug audit logs", err);
|
|
4004
4004
|
}
|
|
@@ -4053,7 +4053,7 @@ var Keystore = class {
|
|
|
4053
4053
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
4054
4054
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
4055
4055
|
this.backend = "keytar";
|
|
4056
|
-
if (password &&
|
|
4056
|
+
if (password && fs30.existsSync(this.storePath)) {
|
|
4057
4057
|
try {
|
|
4058
4058
|
const fileEntries = this.decryptFile(password);
|
|
4059
4059
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -4072,7 +4072,7 @@ var Keystore = class {
|
|
|
4072
4072
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
4073
4073
|
);
|
|
4074
4074
|
}
|
|
4075
|
-
if (!
|
|
4075
|
+
if (!fs30.existsSync(this.storePath)) {
|
|
4076
4076
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
4077
4077
|
this.masterKey = this.deriveKey(password, salt);
|
|
4078
4078
|
this.writeWithSalt({}, salt);
|
|
@@ -4086,7 +4086,7 @@ var Keystore = class {
|
|
|
4086
4086
|
}
|
|
4087
4087
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
4088
4088
|
unlockSync(password) {
|
|
4089
|
-
if (!
|
|
4089
|
+
if (!fs30.existsSync(this.storePath)) {
|
|
4090
4090
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
4091
4091
|
this.masterKey = this.deriveKey(password, salt);
|
|
4092
4092
|
this.writeWithSalt({}, salt);
|
|
@@ -4144,7 +4144,7 @@ var Keystore = class {
|
|
|
4144
4144
|
}
|
|
4145
4145
|
}
|
|
4146
4146
|
decryptFile(password, knownSalt) {
|
|
4147
|
-
if (!
|
|
4147
|
+
if (!fs30.existsSync(this.storePath)) return {};
|
|
4148
4148
|
try {
|
|
4149
4149
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
4150
4150
|
const useSalt = knownSalt ?? salt;
|
|
@@ -4166,8 +4166,8 @@ var Keystore = class {
|
|
|
4166
4166
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4167
4167
|
const tag = cipher.getAuthTag();
|
|
4168
4168
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
4169
|
-
|
|
4170
|
-
|
|
4169
|
+
fs30.mkdirSync(path33.dirname(this.storePath), { recursive: true });
|
|
4170
|
+
fs30.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4171
4171
|
}
|
|
4172
4172
|
writeWithSalt(data, salt) {
|
|
4173
4173
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -4177,11 +4177,11 @@ var Keystore = class {
|
|
|
4177
4177
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4178
4178
|
const tag = cipher.getAuthTag();
|
|
4179
4179
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
4180
|
-
|
|
4181
|
-
|
|
4180
|
+
fs30.mkdirSync(path33.dirname(this.storePath), { recursive: true });
|
|
4181
|
+
fs30.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4182
4182
|
}
|
|
4183
4183
|
readRaw() {
|
|
4184
|
-
const buf =
|
|
4184
|
+
const buf = fs30.readFileSync(this.storePath);
|
|
4185
4185
|
let offset = 0;
|
|
4186
4186
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
4187
4187
|
offset += SALT_LEN;
|
|
@@ -4214,9 +4214,9 @@ var CascadeIgnore = class {
|
|
|
4214
4214
|
]);
|
|
4215
4215
|
}
|
|
4216
4216
|
async load(workspacePath) {
|
|
4217
|
-
const filePath =
|
|
4217
|
+
const filePath = path33.join(workspacePath, ".cascadeignore");
|
|
4218
4218
|
try {
|
|
4219
|
-
const content = await
|
|
4219
|
+
const content = await fs11.readFile(filePath, "utf-8");
|
|
4220
4220
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
4221
4221
|
this.ig.add(lines);
|
|
4222
4222
|
this.loaded = true;
|
|
@@ -4225,7 +4225,7 @@ var CascadeIgnore = class {
|
|
|
4225
4225
|
}
|
|
4226
4226
|
isIgnored(filePath, workspacePath) {
|
|
4227
4227
|
try {
|
|
4228
|
-
const relative = workspacePath ?
|
|
4228
|
+
const relative = workspacePath ? path33.relative(workspacePath, filePath) : filePath;
|
|
4229
4229
|
return this.ig.ignores(relative);
|
|
4230
4230
|
} catch {
|
|
4231
4231
|
return false;
|
|
@@ -4236,7 +4236,7 @@ var CascadeIgnore = class {
|
|
|
4236
4236
|
}
|
|
4237
4237
|
};
|
|
4238
4238
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
4239
|
-
const filePath =
|
|
4239
|
+
const filePath = path33.join(workspacePath, ".cascadeignore");
|
|
4240
4240
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
4241
4241
|
# Syntax identical to .gitignore
|
|
4242
4242
|
|
|
@@ -4263,12 +4263,12 @@ build/
|
|
|
4263
4263
|
.DS_Store
|
|
4264
4264
|
Thumbs.db
|
|
4265
4265
|
`;
|
|
4266
|
-
await
|
|
4266
|
+
await fs11.writeFile(filePath, content, "utf-8");
|
|
4267
4267
|
}
|
|
4268
4268
|
async function loadCascadeMd(workspacePath) {
|
|
4269
|
-
const filePath =
|
|
4269
|
+
const filePath = path33.join(workspacePath, "CASCADE.md");
|
|
4270
4270
|
try {
|
|
4271
|
-
const raw = await
|
|
4271
|
+
const raw = await fs11.readFile(filePath, "utf-8");
|
|
4272
4272
|
return parseCascadeMd(raw);
|
|
4273
4273
|
} catch {
|
|
4274
4274
|
return null;
|
|
@@ -4295,7 +4295,7 @@ ${raw.trim()}`;
|
|
|
4295
4295
|
return { raw, sections, systemPrompt };
|
|
4296
4296
|
}
|
|
4297
4297
|
async function createDefaultCascadeMd(workspacePath) {
|
|
4298
|
-
const filePath =
|
|
4298
|
+
const filePath = path33.join(workspacePath, "CASCADE.md");
|
|
4299
4299
|
const content = `# Cascade Project Instructions
|
|
4300
4300
|
|
|
4301
4301
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -4325,12 +4325,12 @@ All tools are allowed unless specified otherwise.
|
|
|
4325
4325
|
|
|
4326
4326
|
List any areas the agent should not touch.
|
|
4327
4327
|
`;
|
|
4328
|
-
await
|
|
4328
|
+
await fs11.writeFile(filePath, content, "utf-8");
|
|
4329
4329
|
}
|
|
4330
4330
|
var MemoryStore = class _MemoryStore {
|
|
4331
4331
|
db;
|
|
4332
4332
|
constructor(dbPath) {
|
|
4333
|
-
|
|
4333
|
+
fs30.mkdirSync(path33.dirname(dbPath), { recursive: true });
|
|
4334
4334
|
try {
|
|
4335
4335
|
this.db = new Database2(dbPath, { timeout: 5e3 });
|
|
4336
4336
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -5582,7 +5582,7 @@ function validateConfig(raw) {
|
|
|
5582
5582
|
// src/config/global-credentials.ts
|
|
5583
5583
|
init_constants();
|
|
5584
5584
|
function credentialsPath(globalDir2) {
|
|
5585
|
-
return
|
|
5585
|
+
return path33.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
5586
5586
|
}
|
|
5587
5587
|
function providerKey(p) {
|
|
5588
5588
|
if (p.type === "azure") {
|
|
@@ -5595,7 +5595,7 @@ function isPersistable(p) {
|
|
|
5595
5595
|
}
|
|
5596
5596
|
function loadGlobalCredentials(globalDir2) {
|
|
5597
5597
|
try {
|
|
5598
|
-
const raw =
|
|
5598
|
+
const raw = fs30.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
5599
5599
|
const parsed = JSON.parse(raw);
|
|
5600
5600
|
if (!Array.isArray(parsed.providers)) return [];
|
|
5601
5601
|
return parsed.providers.filter(
|
|
@@ -5607,13 +5607,13 @@ function loadGlobalCredentials(globalDir2) {
|
|
|
5607
5607
|
}
|
|
5608
5608
|
function saveGlobalCredentials(globalDir2, providers) {
|
|
5609
5609
|
const filePath = credentialsPath(globalDir2);
|
|
5610
|
-
|
|
5610
|
+
fs30.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
5611
5611
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
5612
5612
|
const tmp = `${filePath}.tmp`;
|
|
5613
|
-
|
|
5614
|
-
|
|
5613
|
+
fs30.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
5614
|
+
fs30.renameSync(tmp, filePath);
|
|
5615
5615
|
try {
|
|
5616
|
-
|
|
5616
|
+
fs30.chmodSync(filePath, 384);
|
|
5617
5617
|
} catch {
|
|
5618
5618
|
}
|
|
5619
5619
|
}
|
|
@@ -5831,7 +5831,7 @@ var ConfigManager = class {
|
|
|
5831
5831
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
5832
5832
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
5833
5833
|
this.workspacePath = workspacePath;
|
|
5834
|
-
this.globalDir = globalDirOverride ??
|
|
5834
|
+
this.globalDir = globalDirOverride ?? path33.join(os10.homedir(), GLOBAL_CONFIG_DIR);
|
|
5835
5835
|
}
|
|
5836
5836
|
async load() {
|
|
5837
5837
|
this.config = await this.loadConfig();
|
|
@@ -5845,8 +5845,8 @@ var ConfigManager = class {
|
|
|
5845
5845
|
this.ignore = new CascadeIgnore();
|
|
5846
5846
|
await this.ignore.load(this.workspacePath);
|
|
5847
5847
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
5848
|
-
this.keystore = new Keystore(
|
|
5849
|
-
this.store = new MemoryStore(
|
|
5848
|
+
this.keystore = new Keystore(path33.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
5849
|
+
this.store = new MemoryStore(path33.join(this.workspacePath, CASCADE_DB_FILE));
|
|
5850
5850
|
await this.injectEnvKeys();
|
|
5851
5851
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
5852
5852
|
await this.ensureDefaultIdentity();
|
|
@@ -5871,9 +5871,9 @@ var ConfigManager = class {
|
|
|
5871
5871
|
return this.workspacePath;
|
|
5872
5872
|
}
|
|
5873
5873
|
async save() {
|
|
5874
|
-
const configPath =
|
|
5875
|
-
await
|
|
5876
|
-
await
|
|
5874
|
+
const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5875
|
+
await fs11.mkdir(path33.dirname(configPath), { recursive: true });
|
|
5876
|
+
await fs11.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
5877
5877
|
try {
|
|
5878
5878
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
5879
5879
|
} catch (err) {
|
|
@@ -5908,9 +5908,9 @@ var ConfigManager = class {
|
|
|
5908
5908
|
return configProvider?.apiKey;
|
|
5909
5909
|
}
|
|
5910
5910
|
async loadConfig() {
|
|
5911
|
-
const configPath =
|
|
5911
|
+
const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5912
5912
|
try {
|
|
5913
|
-
const raw = await
|
|
5913
|
+
const raw = await fs11.readFile(configPath, "utf-8");
|
|
5914
5914
|
return validateConfig(JSON.parse(raw));
|
|
5915
5915
|
} catch (err) {
|
|
5916
5916
|
if (err.code === "ENOENT") {
|
|
@@ -7075,7 +7075,7 @@ init_pricing();
|
|
|
7075
7075
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
7076
7076
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
7077
7077
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
7078
|
-
var DEFAULT_CACHE_FILE =
|
|
7078
|
+
var DEFAULT_CACHE_FILE = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
7079
7079
|
function normalizeModelId2(id) {
|
|
7080
7080
|
let s = id.toLowerCase();
|
|
7081
7081
|
const slash = s.lastIndexOf("/");
|
|
@@ -7110,7 +7110,7 @@ var LiveDataProvider = class {
|
|
|
7110
7110
|
if (this.loaded) return;
|
|
7111
7111
|
this.loaded = true;
|
|
7112
7112
|
try {
|
|
7113
|
-
const raw = await
|
|
7113
|
+
const raw = await fs11.readFile(this.opts.cacheFile, "utf-8");
|
|
7114
7114
|
const cache = JSON.parse(raw);
|
|
7115
7115
|
if (cache.snapshot?.families) {
|
|
7116
7116
|
this.snapshot = cache.snapshot;
|
|
@@ -7222,14 +7222,14 @@ var LiveDataProvider = class {
|
|
|
7222
7222
|
}
|
|
7223
7223
|
async saveCache() {
|
|
7224
7224
|
try {
|
|
7225
|
-
await
|
|
7225
|
+
await fs11.mkdir(path33.dirname(this.opts.cacheFile), { recursive: true });
|
|
7226
7226
|
const cache = {
|
|
7227
7227
|
fetchedAt: this.fetchedAt,
|
|
7228
7228
|
snapshot: this.snapshot ?? void 0,
|
|
7229
7229
|
prices: Object.fromEntries(this.prices),
|
|
7230
7230
|
capabilities: Object.fromEntries(this.capabilities)
|
|
7231
7231
|
};
|
|
7232
|
-
await
|
|
7232
|
+
await fs11.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
7233
7233
|
} catch {
|
|
7234
7234
|
}
|
|
7235
7235
|
}
|
|
@@ -9049,7 +9049,16 @@ var KNOWN_TOOLS = [
|
|
|
9049
9049
|
"glob",
|
|
9050
9050
|
"grep",
|
|
9051
9051
|
"web_fetch",
|
|
9052
|
-
|
|
9052
|
+
// Every media tool buildMediaTools can register, not just the image one.
|
|
9053
|
+
// Omitting three of the four meant a run whose ONLY tools were media ones
|
|
9054
|
+
// (a video-generation subtask on an account with no file/web tools) counted
|
|
9055
|
+
// as "no tools registered" and dropped all tool guidance — the worker was
|
|
9056
|
+
// then told nothing about using tools at all, and wrote the video as prose.
|
|
9057
|
+
"generate_image",
|
|
9058
|
+
"generate_video",
|
|
9059
|
+
"generate_speech",
|
|
9060
|
+
"transcribe_audio",
|
|
9061
|
+
"generate_document"
|
|
9053
9062
|
];
|
|
9054
9063
|
var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
|
|
9055
9064
|
"generate_image",
|
|
@@ -9066,19 +9075,55 @@ function buildWorkerRules(has) {
|
|
|
9066
9075
|
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.",
|
|
9067
9076
|
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
9068
9077
|
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
9078
|
+
// A .docx/.pptx/.xlsx is a ZIP of OOXML parts, not text with a suffix.
|
|
9079
|
+
// file_write does exactly what it promises — writes the model's characters
|
|
9080
|
+
// verbatim — so without this rule the worker "wrote" a Word document that
|
|
9081
|
+
// Word correctly reported as corrupted. Stated as an explicit prohibition
|
|
9082
|
+
// on the tool it would otherwise reach for, because "use generate_document"
|
|
9083
|
+
// alone competes with a file_write habit the model already has.
|
|
9084
|
+
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.',
|
|
9085
|
+
// The alternative to this rule is not "a worse chart" — it is a paragraph
|
|
9086
|
+
// describing a chart that does not exist, which is what real decks came
|
|
9087
|
+
// back with. A `chart:` block is checkable, carries the exact numbers, and
|
|
9088
|
+
// becomes a genuine editable PowerPoint chart object.
|
|
9089
|
+
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.',
|
|
9069
9090
|
// Without this the model "writes the image" as prose — a bracketed
|
|
9070
9091
|
// description sitting in the deck where the picture should be — even with a
|
|
9071
9092
|
// working image model registered. Naming the reference syntax matters as
|
|
9072
9093
|
// much as naming the tool: a generated image nobody embeds is still a
|
|
9073
|
-
// missing image.
|
|
9094
|
+
// missing image. It also has to say the reference must stand ALONE on its
|
|
9095
|
+
// line, because only a standalone reference is parsed as a picture (see
|
|
9096
|
+
// core/documents/blocks.ts matchImageLine) — one embedded mid-sentence
|
|
9097
|
+
// silently degrades to caption text.
|
|
9074
9098
|
//
|
|
9075
|
-
// Scoped to PowerPoint/Word specifically: those are the only
|
|
9076
|
-
//
|
|
9077
|
-
//
|
|
9078
|
-
//
|
|
9079
|
-
//
|
|
9080
|
-
has("generate_image") &&
|
|
9081
|
-
|
|
9099
|
+
// Scoped to PowerPoint/Word specifically: those are the only renderers that
|
|
9100
|
+
// actually embed a Markdown image reference. PDF and plain-text/Markdown
|
|
9101
|
+
// deliverables flatten a reference straight to caption text — telling the
|
|
9102
|
+
// model to call generate_image for THOSE just pays for an image nobody
|
|
9103
|
+
// ever sees.
|
|
9104
|
+
has("generate_image") && // Supersedes the earlier wording of this same rule: it now names the
|
|
9105
|
+
// standalone-line parser constraint, says to generate BEFORE writing the
|
|
9106
|
+
// document, and points data-driven visuals at a ```chart: block rather
|
|
9107
|
+
// than a Markdown table, which is only the right advice now that chart
|
|
9108
|
+
// blocks render as real chart objects.
|
|
9109
|
+
'- 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.',
|
|
9110
|
+
// The other half of image reliability, and the half no amount of
|
|
9111
|
+
// instruction-tightening could fix: generate_image only exists when an
|
|
9112
|
+
// OpenAI or Gemini key is configured (multimodal/registry.ts CAPABILITIES).
|
|
9113
|
+
// With neither, the model was never given a choice — and a model that
|
|
9114
|
+
// doesn't know that writes "[illustration of a cat]" and moves on. Said out
|
|
9115
|
+
// loud, up front, so the fallback is a real chart or honest prose.
|
|
9116
|
+
!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.",
|
|
9117
|
+
// The video counterpart of the image rule above, and the fix for the
|
|
9118
|
+
// reported "it writes scripts forever and never makes the video" run. The
|
|
9119
|
+
// failure mode is identical — the model narrates the deliverable instead of
|
|
9120
|
+
// producing it — but the stakes are not: a video subtask that ends in prose
|
|
9121
|
+
// has burned the whole pre-production plan for nothing. Unlike the image
|
|
9122
|
+
// rule this is NOT scoped to a document format, because the clip itself is
|
|
9123
|
+
// the deliverable rather than an illustration inside one, and the reference
|
|
9124
|
+
// syntax is a plain link: Markdown image syntax does not embed a video.
|
|
9125
|
+
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.',
|
|
9126
|
+
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.`,
|
|
9082
9127
|
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
9083
9128
|
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.',
|
|
9084
9129
|
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.",
|
|
@@ -9089,13 +9134,34 @@ function buildWorkerRules(has) {
|
|
|
9089
9134
|
Rules:
|
|
9090
9135
|
${rules.filter((r) => r !== false).join("\n")}`;
|
|
9091
9136
|
}
|
|
9092
|
-
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell"]);
|
|
9137
|
+
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell", "generate_document"]);
|
|
9138
|
+
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;
|
|
9093
9139
|
function shouldRequireArtifact(assignment, toolNames) {
|
|
9094
9140
|
if (!toolNames.some((n) => ARTIFACT_TOOLS.has(n))) return false;
|
|
9095
9141
|
if (assignment?.files?.length) return true;
|
|
9096
9142
|
const haystack = `${assignment?.description ?? ""}
|
|
9097
9143
|
${assignment?.expectedOutput ?? ""}`;
|
|
9098
|
-
return
|
|
9144
|
+
return ARTIFACT_FILE_RE.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
|
|
9145
|
+
}
|
|
9146
|
+
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;
|
|
9147
|
+
var IMAGE_REF_RE = /!\[[^\]]*\]\([^)]+\)/;
|
|
9148
|
+
var CHART_BLOCK_RE = /^\s*```+\s*chart\s*:/im;
|
|
9149
|
+
function missingVisualEvidence(assignment, output, calledTools, toolNames) {
|
|
9150
|
+
const canVisualize = toolNames.includes("generate_image") || toolNames.includes("generate_document");
|
|
9151
|
+
if (!canVisualize) return null;
|
|
9152
|
+
const haystack = `${assignment?.description ?? ""}
|
|
9153
|
+
${assignment?.expectedOutput ?? ""}`;
|
|
9154
|
+
if (!VISUAL_REQUEST_RE.test(haystack)) return null;
|
|
9155
|
+
if (calledTools.includes("generate_image") || calledTools.includes("generate_document")) return null;
|
|
9156
|
+
if (IMAGE_REF_RE.test(output) || CHART_BLOCK_RE.test(output)) return null;
|
|
9157
|
+
const options = [];
|
|
9158
|
+
if (toolNames.includes("generate_image")) {
|
|
9159
|
+
options.push('call "generate_image" and reference the result on its own line as ');
|
|
9160
|
+
}
|
|
9161
|
+
if (toolNames.includes("generate_document")) {
|
|
9162
|
+
options.push("emit a fenced ```chart:bar (or chart:line / chart:pie) block whose body is CSV, for anything data-driven");
|
|
9163
|
+
}
|
|
9164
|
+
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.`;
|
|
9099
9165
|
}
|
|
9100
9166
|
var T3Worker = class extends BaseTier {
|
|
9101
9167
|
router;
|
|
@@ -9276,6 +9342,18 @@ Now execute your subtask using this context where relevant.`
|
|
|
9276
9342
|
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
9277
9343
|
}
|
|
9278
9344
|
}
|
|
9345
|
+
const visualIssue = missingVisualEvidence(
|
|
9346
|
+
assignment,
|
|
9347
|
+
output,
|
|
9348
|
+
toolCalls.map((t) => t.name),
|
|
9349
|
+
this.tools.map((t) => t.name)
|
|
9350
|
+
);
|
|
9351
|
+
if (visualIssue) {
|
|
9352
|
+
correctionAttempts = 1;
|
|
9353
|
+
issues.push(visualIssue);
|
|
9354
|
+
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
9355
|
+
output = await this.correctOutput(output, [visualIssue]);
|
|
9356
|
+
}
|
|
9279
9357
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9280
9358
|
const testResult = await this.selfTest(assignment, output);
|
|
9281
9359
|
checksRun.push(...testResult.checksRun);
|
|
@@ -9590,8 +9668,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9590
9668
|
// Media generation can run for a minute; without this a cancelled run
|
|
9591
9669
|
// still pays for an image nobody will see.
|
|
9592
9670
|
...this.signal ? { signal: this.signal } : {},
|
|
9593
|
-
saveSnapshot: async (
|
|
9594
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
9671
|
+
saveSnapshot: async (path38, content) => {
|
|
9672
|
+
this.store?.addFileSnapshot(this.taskId, path38, content);
|
|
9595
9673
|
},
|
|
9596
9674
|
sendPeerSync: (to, syncType, content) => {
|
|
9597
9675
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -9620,6 +9698,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9620
9698
|
if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
|
|
9621
9699
|
throw new CriticalToolError(errMsg, tc.name);
|
|
9622
9700
|
}
|
|
9701
|
+
if (isProviderBacked) {
|
|
9702
|
+
throw new CriticalToolError(
|
|
9703
|
+
`${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}`,
|
|
9704
|
+
tc.name
|
|
9705
|
+
);
|
|
9706
|
+
}
|
|
9623
9707
|
return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
|
|
9624
9708
|
}
|
|
9625
9709
|
}
|
|
@@ -9629,6 +9713,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9629
9713
|
* 1. Find a semantically similar registered tool and retry with same input
|
|
9630
9714
|
* 2. Synthesize a new tool via ToolCreator (if available) and run it
|
|
9631
9715
|
* 3. Return the original error so the agent loop can decide what to do next
|
|
9716
|
+
*
|
|
9717
|
+
* Scoped to CHEAP, task-scoped failures — the class of problem it was built
|
|
9718
|
+
* for is a wrong or missing tool name, where a name-similar sibling plausibly
|
|
9719
|
+
* does the same job for the same (near-zero) cost. PROVIDER_BACKED_TOOLS never
|
|
9720
|
+
* reach it: their siblings are keyword-similar but semantically different
|
|
9721
|
+
* (generate_image is not a stand-in for generate_video), and every attempt —
|
|
9722
|
+
* theirs, or a synthesized replacement's — is separately billed. See the
|
|
9723
|
+
* fast-fail branch in executeTool().
|
|
9632
9724
|
*/
|
|
9633
9725
|
async adaptiveFallback(tc, originalError) {
|
|
9634
9726
|
const altTool = this.findAlternativeTool(tc.name);
|
|
@@ -9753,7 +9845,7 @@ ${assignment.expectedOutput}`;
|
|
|
9753
9845
|
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
9754
9846
|
const haystack = `${assignment.description}
|
|
9755
9847
|
${assignment.expectedOutput}`;
|
|
9756
|
-
const matches = haystack.match(
|
|
9848
|
+
const matches = haystack.match(new RegExp(ARTIFACT_FILE_RE.source, "gi")) ?? [];
|
|
9757
9849
|
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
9758
9850
|
}
|
|
9759
9851
|
async verifyArtifacts(assignment) {
|
|
@@ -9764,9 +9856,9 @@ ${assignment.expectedOutput}`;
|
|
|
9764
9856
|
const { promisify: promisify4 } = await import('util');
|
|
9765
9857
|
const execAsync3 = promisify4(exec3);
|
|
9766
9858
|
for (const artifactPath of artifactPaths) {
|
|
9767
|
-
const absolutePath =
|
|
9859
|
+
const absolutePath = path33.resolve(process.cwd(), artifactPath);
|
|
9768
9860
|
try {
|
|
9769
|
-
const stat = await
|
|
9861
|
+
const stat = await fs11.stat(absolutePath);
|
|
9770
9862
|
if (!stat.isFile()) {
|
|
9771
9863
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
9772
9864
|
continue;
|
|
@@ -9775,8 +9867,22 @@ ${assignment.expectedOutput}`;
|
|
|
9775
9867
|
issues.push(`Artifact is empty: ${artifactPath}`);
|
|
9776
9868
|
continue;
|
|
9777
9869
|
}
|
|
9778
|
-
if (
|
|
9779
|
-
const
|
|
9870
|
+
if (/\.(docx|pptx|xlsx)$/i.test(artifactPath)) {
|
|
9871
|
+
const head = Buffer.alloc(4);
|
|
9872
|
+
const fh = await fs11.open(absolutePath, "r");
|
|
9873
|
+
try {
|
|
9874
|
+
await fh.read(head, 0, 4, 0);
|
|
9875
|
+
} finally {
|
|
9876
|
+
await fh.close();
|
|
9877
|
+
}
|
|
9878
|
+
if (head.toString("latin1") !== "PK") {
|
|
9879
|
+
issues.push(
|
|
9880
|
+
`${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.`
|
|
9881
|
+
);
|
|
9882
|
+
continue;
|
|
9883
|
+
}
|
|
9884
|
+
} else if (!/\.pdf$/i.test(artifactPath)) {
|
|
9885
|
+
const content = await fs11.readFile(absolutePath, "utf-8");
|
|
9780
9886
|
if (!content.trim()) {
|
|
9781
9887
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
9782
9888
|
continue;
|
|
@@ -9785,7 +9891,7 @@ ${assignment.expectedOutput}`;
|
|
|
9785
9891
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
9786
9892
|
continue;
|
|
9787
9893
|
}
|
|
9788
|
-
const ext =
|
|
9894
|
+
const ext = path33.extname(absolutePath).toLowerCase();
|
|
9789
9895
|
try {
|
|
9790
9896
|
if (ext === ".ts" || ext === ".tsx") {
|
|
9791
9897
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -10381,15 +10487,245 @@ _(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
|
10381
10487
|
return output;
|
|
10382
10488
|
}
|
|
10383
10489
|
|
|
10490
|
+
// src/core/multimodal/registry.ts
|
|
10491
|
+
init_pricing();
|
|
10492
|
+
var CAPABILITIES = [
|
|
10493
|
+
// ── Image ──
|
|
10494
|
+
{
|
|
10495
|
+
modality: "image",
|
|
10496
|
+
provider: "openai",
|
|
10497
|
+
modelId: "dall-e-3",
|
|
10498
|
+
api: "openai-images"
|
|
10499
|
+
},
|
|
10500
|
+
{
|
|
10501
|
+
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
10502
|
+
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
10503
|
+
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
10504
|
+
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
10505
|
+
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
10506
|
+
// 'gemini-generate-content' rather than just the modelId moving.
|
|
10507
|
+
// https://ai.google.dev/gemini-api/docs/imagen
|
|
10508
|
+
modality: "image",
|
|
10509
|
+
provider: "gemini",
|
|
10510
|
+
modelId: "gemini-2.5-flash-image",
|
|
10511
|
+
api: "gemini-generate-content"
|
|
10512
|
+
},
|
|
10513
|
+
// ── Speech (text → audio) ──
|
|
10514
|
+
{
|
|
10515
|
+
modality: "speech",
|
|
10516
|
+
provider: "openai",
|
|
10517
|
+
modelId: "tts-1",
|
|
10518
|
+
api: "openai-speech"
|
|
10519
|
+
},
|
|
10520
|
+
{
|
|
10521
|
+
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
10522
|
+
// ships these as the same voice at two fidelities and charges double for
|
|
10523
|
+
// this one. That is the vendor's own ordering, not an inference.
|
|
10524
|
+
modality: "speech",
|
|
10525
|
+
provider: "openai",
|
|
10526
|
+
modelId: "tts-1-hd",
|
|
10527
|
+
api: "openai-speech",
|
|
10528
|
+
quality: 70,
|
|
10529
|
+
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
10530
|
+
},
|
|
10531
|
+
// ── Transcription (audio → text) ──
|
|
10532
|
+
{
|
|
10533
|
+
modality: "transcription",
|
|
10534
|
+
provider: "openai",
|
|
10535
|
+
modelId: "whisper-1",
|
|
10536
|
+
api: "openai-transcriptions"
|
|
10537
|
+
},
|
|
10538
|
+
// ── Video ──
|
|
10539
|
+
{
|
|
10540
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
10541
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
10542
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
10543
|
+
//
|
|
10544
|
+
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
10545
|
+
// not the Gemini Developer API's — this file calls
|
|
10546
|
+
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
10547
|
+
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
10548
|
+
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
10549
|
+
// there is `veo-3.1-generate-preview`.
|
|
10550
|
+
// https://ai.google.dev/gemini-api/docs/veo
|
|
10551
|
+
modality: "video",
|
|
10552
|
+
provider: "gemini",
|
|
10553
|
+
modelId: "veo-3.1-generate-preview",
|
|
10554
|
+
api: "gemini-predict-lro"
|
|
10555
|
+
}
|
|
10556
|
+
];
|
|
10557
|
+
function capabilityCost(cap) {
|
|
10558
|
+
const match = findPricing(cap.modelId, cap.provider, {
|
|
10559
|
+
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
10560
|
+
});
|
|
10561
|
+
const rate = match?.entry.rates?.[0];
|
|
10562
|
+
if (!rate) return null;
|
|
10563
|
+
const unitLabel = {
|
|
10564
|
+
per_image: "per image",
|
|
10565
|
+
per_second: "per second of video",
|
|
10566
|
+
per_character: "per character",
|
|
10567
|
+
per_audio_minute: "per audio minute"
|
|
10568
|
+
};
|
|
10569
|
+
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
10570
|
+
const amount = rate.amount;
|
|
10571
|
+
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
10572
|
+
return { label: `$${shown} ${unit}`, amount, unit };
|
|
10573
|
+
}
|
|
10574
|
+
function explainChoice(c, cost, modality, isBest) {
|
|
10575
|
+
const priced = cost ? ` ${cost.label}.` : "";
|
|
10576
|
+
if (c.quality !== void 0) {
|
|
10577
|
+
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}`;
|
|
10578
|
+
}
|
|
10579
|
+
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
10580
|
+
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;
|
|
10581
|
+
}
|
|
10582
|
+
var MultimodalRegistry = class {
|
|
10583
|
+
/** Provider types the user has actually configured a key for. */
|
|
10584
|
+
available;
|
|
10585
|
+
constructor(availableProviders) {
|
|
10586
|
+
this.available = new Set(availableProviders);
|
|
10587
|
+
}
|
|
10588
|
+
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
10589
|
+
usable(modality) {
|
|
10590
|
+
return CAPABILITIES.filter(
|
|
10591
|
+
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
10592
|
+
);
|
|
10593
|
+
}
|
|
10594
|
+
/** Modalities this account can actually generate right now. */
|
|
10595
|
+
availableModalities() {
|
|
10596
|
+
return [...new Set(this.usable().map((c) => c.modality))];
|
|
10597
|
+
}
|
|
10598
|
+
/**
|
|
10599
|
+
* Every usable capability for a modality, best first.
|
|
10600
|
+
*
|
|
10601
|
+
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
10602
|
+
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
10603
|
+
* the user naming a model is a stronger signal than any prior.
|
|
10604
|
+
*
|
|
10605
|
+
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
10606
|
+
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
10607
|
+
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
10608
|
+
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
10609
|
+
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
10610
|
+
*/
|
|
10611
|
+
rank(modality, opts = {}) {
|
|
10612
|
+
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
10613
|
+
const qa = a.c.quality ?? -1;
|
|
10614
|
+
const qb = b.c.quality ?? -1;
|
|
10615
|
+
if (qa !== qb) return qb - qa;
|
|
10616
|
+
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
10617
|
+
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
10618
|
+
return pa - pb;
|
|
10619
|
+
});
|
|
10620
|
+
const results = scored.map(({ c, cost }, i) => ({
|
|
10621
|
+
capability: c,
|
|
10622
|
+
cost,
|
|
10623
|
+
reason: explainChoice(c, cost, modality, i === 0)
|
|
10624
|
+
}));
|
|
10625
|
+
if (opts.preferModel) {
|
|
10626
|
+
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
10627
|
+
if (i > -1) {
|
|
10628
|
+
const [wanted] = results.splice(i, 1);
|
|
10629
|
+
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
10630
|
+
}
|
|
10631
|
+
}
|
|
10632
|
+
return results;
|
|
10633
|
+
}
|
|
10634
|
+
/**
|
|
10635
|
+
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
10636
|
+
*/
|
|
10637
|
+
select(modality, opts = {}) {
|
|
10638
|
+
return this.rank(modality, opts)[0] ?? null;
|
|
10639
|
+
}
|
|
10640
|
+
/**
|
|
10641
|
+
* A plain-language account of what this account can and cannot generate —
|
|
10642
|
+
* including the things it cannot, and why.
|
|
10643
|
+
*
|
|
10644
|
+
* This is the USER-facing inventory (model ids and unit prices), keyed on
|
|
10645
|
+
* which providers are configured. It is deliberately NOT what the planner
|
|
10646
|
+
* reads: see `describeGenerationForPlanner()` below for that, and the note
|
|
10647
|
+
* there for why the two cannot be the same string.
|
|
10648
|
+
*/
|
|
10649
|
+
describe() {
|
|
10650
|
+
const lines = [];
|
|
10651
|
+
const usable = this.usable();
|
|
10652
|
+
if (usable.length) {
|
|
10653
|
+
lines.push("Generation models available on your configured providers:");
|
|
10654
|
+
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
10655
|
+
const forModality = usable.filter((c) => c.modality === modality);
|
|
10656
|
+
if (!forModality.length) continue;
|
|
10657
|
+
const shown = forModality.map((c) => {
|
|
10658
|
+
const cost = capabilityCost(c);
|
|
10659
|
+
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
10660
|
+
});
|
|
10661
|
+
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
10662
|
+
}
|
|
10663
|
+
} else {
|
|
10664
|
+
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
10665
|
+
}
|
|
10666
|
+
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
10667
|
+
for (const c of blocked) {
|
|
10668
|
+
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
10669
|
+
}
|
|
10670
|
+
lines.push(
|
|
10671
|
+
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
10672
|
+
);
|
|
10673
|
+
return lines.join("\n");
|
|
10674
|
+
}
|
|
10675
|
+
};
|
|
10676
|
+
var PLANNER_FACTS = [
|
|
10677
|
+
{ tool: "generate_image", modality: "image", shape: "one call returns one finished image" },
|
|
10678
|
+
{ tool: "generate_speech", modality: "speech", shape: "one call returns one finished audio file" },
|
|
10679
|
+
{
|
|
10680
|
+
tool: "generate_video",
|
|
10681
|
+
modality: "video",
|
|
10682
|
+
shape: "one call returns one finished clip, after rendering for 1-3 minutes (Cascade stops waiting at 8)"
|
|
10683
|
+
},
|
|
10684
|
+
{
|
|
10685
|
+
tool: "transcribe_audio",
|
|
10686
|
+
modality: "transcription",
|
|
10687
|
+
shape: "one call returns the transcript of one audio file"
|
|
10688
|
+
}
|
|
10689
|
+
];
|
|
10690
|
+
function plannerPriceNote(modality) {
|
|
10691
|
+
const costs = CAPABILITIES.filter((c) => c.modality === modality && !c.unsupported).map((c) => capabilityCost(c)).filter((c) => c !== null);
|
|
10692
|
+
if (!costs.length) return "";
|
|
10693
|
+
const labels = new Set(costs.map((c) => c.label));
|
|
10694
|
+
if (labels.size === 1) return ` Billed ${costs[0].label}.`;
|
|
10695
|
+
const units = new Set(costs.map((c) => c.unit));
|
|
10696
|
+
return units.size === 1 ? ` Billed ${[...units][0]}.` : "";
|
|
10697
|
+
}
|
|
10698
|
+
function describeGenerationForPlanner(has) {
|
|
10699
|
+
const present = PLANNER_FACTS.filter((f) => has(f.tool));
|
|
10700
|
+
if (!present.length) return "";
|
|
10701
|
+
const lines = ["MEDIA GENERATION \u2014 the only generation tools this run can call:"];
|
|
10702
|
+
for (const f of present) {
|
|
10703
|
+
lines.push(`- "${f.tool}" (${f.modality}): ${f.shape}.${plannerPriceNote(f.modality)}`);
|
|
10704
|
+
}
|
|
10705
|
+
lines.push(
|
|
10706
|
+
"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.",
|
|
10707
|
+
"No other modality has a tool here \u2014 never plan a step that generates music, 3D or anything not listed above."
|
|
10708
|
+
);
|
|
10709
|
+
if (has("generate_video")) {
|
|
10710
|
+
lines.push(
|
|
10711
|
+
'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.'
|
|
10712
|
+
);
|
|
10713
|
+
}
|
|
10714
|
+
return lines.join("\n");
|
|
10715
|
+
}
|
|
10716
|
+
|
|
10384
10717
|
// src/core/tiers/t2-manager.ts
|
|
10385
|
-
function buildT2SystemPrompt(
|
|
10718
|
+
function buildT2SystemPrompt(has) {
|
|
10719
|
+
const generation = describeGenerationForPlanner(has);
|
|
10386
10720
|
return [
|
|
10387
10721
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
10388
10722
|
"Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
|
|
10389
10723
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
10390
|
-
|
|
10391
|
-
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text."
|
|
10392
|
-
|
|
10724
|
+
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
10725
|
+
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
10726
|
+
generation && `
|
|
10727
|
+
${generation}`
|
|
10728
|
+
].filter((l) => l !== false && l !== "").join("\n");
|
|
10393
10729
|
}
|
|
10394
10730
|
var T2Manager = class extends BaseTier {
|
|
10395
10731
|
router;
|
|
@@ -10686,7 +11022,7 @@ Return ONLY the JSON array.`;
|
|
|
10686
11022
|
const messages = [{ role: "user", content: prompt }];
|
|
10687
11023
|
const result = await this.router.generate("T2", {
|
|
10688
11024
|
messages,
|
|
10689
|
-
systemPrompt: this.systemPromptOverride + buildT2SystemPrompt(this.toolRegistry.hasTool(
|
|
11025
|
+
systemPrompt: this.systemPromptOverride + buildT2SystemPrompt((name) => this.toolRegistry.hasTool(name)) + (this.hierarchyContext ? `
|
|
10690
11026
|
|
|
10691
11027
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
10692
11028
|
maxTokens: 2e3,
|
|
@@ -11239,6 +11575,7 @@ function isStrongKeywordOverlap(a = [], b = []) {
|
|
|
11239
11575
|
}
|
|
11240
11576
|
function buildT1SystemPrompt(has) {
|
|
11241
11577
|
const canWriteArtifacts = has("file_write") || has("file_edit") || has("run_code") || has("pdf_create");
|
|
11578
|
+
const generation = describeGenerationForPlanner(has);
|
|
11242
11579
|
const rules = [
|
|
11243
11580
|
"- Simple \u2192 1 T3, Moderate \u2192 2-3 T2s, Complex \u2192 3-5 T2s, Highly Complex \u2192 5+ T2s",
|
|
11244
11581
|
"- Return ONLY valid JSON \u2014 no other text",
|
|
@@ -11273,7 +11610,9 @@ DEPENDENCY GUIDANCE:
|
|
|
11273
11610
|
QUALITY RULES:
|
|
11274
11611
|
- Each section must have a clear, testable "expectedOutput" so T2 knows when it is done.
|
|
11275
11612
|
- Do NOT create trivial sections that only move files or print summaries \u2014 fold those into adjacent sections.
|
|
11276
|
-
- If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2)
|
|
11613
|
+
- If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2).${generation ? `
|
|
11614
|
+
|
|
11615
|
+
${generation}` : ""}`;
|
|
11277
11616
|
}
|
|
11278
11617
|
var T1Administrator = class extends BaseTier {
|
|
11279
11618
|
router;
|
|
@@ -11935,225 +12274,42 @@ Instructions:
|
|
|
11935
12274
|
}
|
|
11936
12275
|
/**
|
|
11937
12276
|
* T1-level permission evaluator.
|
|
11938
|
-
* Uses T1's model with full task context.
|
|
11939
|
-
* Returns null only when the model explicitly says UNSURE (triggers user prompt).
|
|
11940
|
-
*/
|
|
11941
|
-
async evaluatePermissionAtT1(req) {
|
|
11942
|
-
const prompt = `You are T1 Administrator. Overall task goal:
|
|
11943
|
-
${this.taskGoal}
|
|
11944
|
-
|
|
11945
|
-
A T3 Worker (inside section "${req.sectionContext}") wants to:
|
|
11946
|
-
Tool: ${req.toolName}
|
|
11947
|
-
Target: ${JSON.stringify(req.input)}
|
|
11948
|
-
Reason: ${req.subtaskContext}
|
|
11949
|
-
|
|
11950
|
-
T2 Manager was uncertain about this. Given the overall task goal, should this be allowed?
|
|
11951
|
-
Reply with exactly one word: YES, NO, or UNSURE.
|
|
11952
|
-
(UNSURE = escalate to the human user for a final decision.)`;
|
|
11953
|
-
try {
|
|
11954
|
-
const result = await this.router.generate("T1", {
|
|
11955
|
-
messages: [{ role: "user", content: prompt }],
|
|
11956
|
-
systemPrompt: this.systemPromptOverride + "You are a T1 Administrator evaluating permissions.",
|
|
11957
|
-
maxTokens: 10,
|
|
11958
|
-
temperature: 0
|
|
11959
|
-
});
|
|
11960
|
-
const answer = result.content.trim().toUpperCase();
|
|
11961
|
-
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
11962
|
-
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
11963
|
-
return null;
|
|
11964
|
-
} catch {
|
|
11965
|
-
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
11966
|
-
return null;
|
|
11967
|
-
}
|
|
11968
|
-
}
|
|
11969
|
-
};
|
|
11970
|
-
|
|
11971
|
-
// src/core/cascade.ts
|
|
11972
|
-
init_cost();
|
|
11973
|
-
|
|
11974
|
-
// src/core/multimodal/registry.ts
|
|
11975
|
-
init_pricing();
|
|
11976
|
-
var CAPABILITIES = [
|
|
11977
|
-
// ── Image ──
|
|
11978
|
-
{
|
|
11979
|
-
modality: "image",
|
|
11980
|
-
provider: "openai",
|
|
11981
|
-
modelId: "dall-e-3",
|
|
11982
|
-
api: "openai-images"
|
|
11983
|
-
},
|
|
11984
|
-
{
|
|
11985
|
-
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
11986
|
-
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
11987
|
-
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
11988
|
-
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
11989
|
-
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
11990
|
-
// 'gemini-generate-content' rather than just the modelId moving.
|
|
11991
|
-
// https://ai.google.dev/gemini-api/docs/imagen
|
|
11992
|
-
modality: "image",
|
|
11993
|
-
provider: "gemini",
|
|
11994
|
-
modelId: "gemini-2.5-flash-image",
|
|
11995
|
-
api: "gemini-generate-content"
|
|
11996
|
-
},
|
|
11997
|
-
// ── Speech (text → audio) ──
|
|
11998
|
-
{
|
|
11999
|
-
modality: "speech",
|
|
12000
|
-
provider: "openai",
|
|
12001
|
-
modelId: "tts-1",
|
|
12002
|
-
api: "openai-speech"
|
|
12003
|
-
},
|
|
12004
|
-
{
|
|
12005
|
-
// The only quality claim in this table that needs no leaderboard: OpenAI
|
|
12006
|
-
// ships these as the same voice at two fidelities and charges double for
|
|
12007
|
-
// this one. That is the vendor's own ordering, not an inference.
|
|
12008
|
-
modality: "speech",
|
|
12009
|
-
provider: "openai",
|
|
12010
|
-
modelId: "tts-1-hd",
|
|
12011
|
-
api: "openai-speech",
|
|
12012
|
-
quality: 70,
|
|
12013
|
-
qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
|
|
12014
|
-
},
|
|
12015
|
-
// ── Transcription (audio → text) ──
|
|
12016
|
-
{
|
|
12017
|
-
modality: "transcription",
|
|
12018
|
-
provider: "openai",
|
|
12019
|
-
modelId: "whisper-1",
|
|
12020
|
-
api: "openai-transcriptions"
|
|
12021
|
-
},
|
|
12022
|
-
// ── Video ──
|
|
12023
|
-
{
|
|
12024
|
-
// Long-running: submit returns an operation name, the video lands minutes
|
|
12025
|
-
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
12026
|
-
// that polling loop; the registry only records the shape via `api`.
|
|
12027
|
-
//
|
|
12028
|
-
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
12029
|
-
// not the Gemini Developer API's — this file calls
|
|
12030
|
-
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
12031
|
-
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
12032
|
-
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
12033
|
-
// there is `veo-3.1-generate-preview`.
|
|
12034
|
-
// https://ai.google.dev/gemini-api/docs/veo
|
|
12035
|
-
modality: "video",
|
|
12036
|
-
provider: "gemini",
|
|
12037
|
-
modelId: "veo-3.1-generate-preview",
|
|
12038
|
-
api: "gemini-predict-lro"
|
|
12039
|
-
}
|
|
12040
|
-
];
|
|
12041
|
-
function capabilityCost(cap) {
|
|
12042
|
-
const match = findPricing(cap.modelId, cap.provider, {
|
|
12043
|
-
fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
|
|
12044
|
-
});
|
|
12045
|
-
const rate = match?.entry.rates?.[0];
|
|
12046
|
-
if (!rate) return null;
|
|
12047
|
-
const unitLabel = {
|
|
12048
|
-
per_image: "per image",
|
|
12049
|
-
per_second: "per second of video",
|
|
12050
|
-
per_character: "per character",
|
|
12051
|
-
per_audio_minute: "per audio minute"
|
|
12052
|
-
};
|
|
12053
|
-
const unit = unitLabel[rate.unit] ?? rate.unit;
|
|
12054
|
-
const amount = rate.amount;
|
|
12055
|
-
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
12056
|
-
return { label: `$${shown} ${unit}`, amount, unit };
|
|
12057
|
-
}
|
|
12058
|
-
function explainChoice(c, cost, modality, isBest) {
|
|
12059
|
-
const priced = cost ? ` ${cost.label}.` : "";
|
|
12060
|
-
if (c.quality !== void 0) {
|
|
12061
|
-
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}`;
|
|
12062
|
-
}
|
|
12063
|
-
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
12064
|
-
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;
|
|
12065
|
-
}
|
|
12066
|
-
var MultimodalRegistry = class {
|
|
12067
|
-
/** Provider types the user has actually configured a key for. */
|
|
12068
|
-
available;
|
|
12069
|
-
constructor(availableProviders) {
|
|
12070
|
-
this.available = new Set(availableProviders);
|
|
12071
|
-
}
|
|
12072
|
-
/** Every capability whose provider is configured AND which Cascade can call. */
|
|
12073
|
-
usable(modality) {
|
|
12074
|
-
return CAPABILITIES.filter(
|
|
12075
|
-
(c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
|
|
12076
|
-
);
|
|
12077
|
-
}
|
|
12078
|
-
/** Modalities this account can actually generate right now. */
|
|
12079
|
-
availableModalities() {
|
|
12080
|
-
return [...new Set(this.usable().map((c) => c.modality))];
|
|
12081
|
-
}
|
|
12082
|
-
/**
|
|
12083
|
-
* Every usable capability for a modality, best first.
|
|
12084
|
-
*
|
|
12085
|
-
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
12086
|
-
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
12087
|
-
* the user naming a model is a stronger signal than any prior.
|
|
12088
|
-
*
|
|
12089
|
-
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
12090
|
-
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
12091
|
-
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
12092
|
-
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
12093
|
-
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
12094
|
-
*/
|
|
12095
|
-
rank(modality, opts = {}) {
|
|
12096
|
-
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
12097
|
-
const qa = a.c.quality ?? -1;
|
|
12098
|
-
const qb = b.c.quality ?? -1;
|
|
12099
|
-
if (qa !== qb) return qb - qa;
|
|
12100
|
-
const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
12101
|
-
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
12102
|
-
return pa - pb;
|
|
12103
|
-
});
|
|
12104
|
-
const results = scored.map(({ c, cost }, i) => ({
|
|
12105
|
-
capability: c,
|
|
12106
|
-
cost,
|
|
12107
|
-
reason: explainChoice(c, cost, modality, i === 0)
|
|
12108
|
-
}));
|
|
12109
|
-
if (opts.preferModel) {
|
|
12110
|
-
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
12111
|
-
if (i > -1) {
|
|
12112
|
-
const [wanted] = results.splice(i, 1);
|
|
12113
|
-
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
12114
|
-
}
|
|
12115
|
-
}
|
|
12116
|
-
return results;
|
|
12117
|
-
}
|
|
12118
|
-
/**
|
|
12119
|
-
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
12120
|
-
*/
|
|
12121
|
-
select(modality, opts = {}) {
|
|
12122
|
-
return this.rank(modality, opts)[0] ?? null;
|
|
12123
|
-
}
|
|
12124
|
-
/**
|
|
12125
|
-
* A plain-language account of what this account can and cannot generate —
|
|
12126
|
-
* including the things it cannot, and why. The planner sees this, so being
|
|
12127
|
-
* candid here is what stops it inventing a `generate_music` call.
|
|
12128
|
-
*/
|
|
12129
|
-
describe() {
|
|
12130
|
-
const lines = [];
|
|
12131
|
-
const usable = this.usable();
|
|
12132
|
-
if (usable.length) {
|
|
12133
|
-
lines.push("Generation models available on your configured providers:");
|
|
12134
|
-
for (const modality of ["image", "speech", "transcription", "video"]) {
|
|
12135
|
-
const forModality = usable.filter((c) => c.modality === modality);
|
|
12136
|
-
if (!forModality.length) continue;
|
|
12137
|
-
const shown = forModality.map((c) => {
|
|
12138
|
-
const cost = capabilityCost(c);
|
|
12139
|
-
return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
|
|
12140
|
-
});
|
|
12141
|
-
lines.push(` ${modality}: ${shown.join(", ")}`);
|
|
12142
|
-
}
|
|
12143
|
-
} else {
|
|
12144
|
-
lines.push("No generation models are available \u2014 none of your configured providers offers one.");
|
|
12145
|
-
}
|
|
12146
|
-
const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
|
|
12147
|
-
for (const c of blocked) {
|
|
12148
|
-
lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
|
|
12277
|
+
* Uses T1's model with full task context.
|
|
12278
|
+
* Returns null only when the model explicitly says UNSURE (triggers user prompt).
|
|
12279
|
+
*/
|
|
12280
|
+
async evaluatePermissionAtT1(req) {
|
|
12281
|
+
const prompt = `You are T1 Administrator. Overall task goal:
|
|
12282
|
+
${this.taskGoal}
|
|
12283
|
+
|
|
12284
|
+
A T3 Worker (inside section "${req.sectionContext}") wants to:
|
|
12285
|
+
Tool: ${req.toolName}
|
|
12286
|
+
Target: ${JSON.stringify(req.input)}
|
|
12287
|
+
Reason: ${req.subtaskContext}
|
|
12288
|
+
|
|
12289
|
+
T2 Manager was uncertain about this. Given the overall task goal, should this be allowed?
|
|
12290
|
+
Reply with exactly one word: YES, NO, or UNSURE.
|
|
12291
|
+
(UNSURE = escalate to the human user for a final decision.)`;
|
|
12292
|
+
try {
|
|
12293
|
+
const result = await this.router.generate("T1", {
|
|
12294
|
+
messages: [{ role: "user", content: prompt }],
|
|
12295
|
+
systemPrompt: this.systemPromptOverride + "You are a T1 Administrator evaluating permissions.",
|
|
12296
|
+
maxTokens: 10,
|
|
12297
|
+
temperature: 0
|
|
12298
|
+
});
|
|
12299
|
+
const answer = result.content.trim().toUpperCase();
|
|
12300
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
12301
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
12302
|
+
return null;
|
|
12303
|
+
} catch {
|
|
12304
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
12305
|
+
return null;
|
|
12149
12306
|
}
|
|
12150
|
-
lines.push(
|
|
12151
|
-
" music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
|
|
12152
|
-
);
|
|
12153
|
-
return lines.join("\n");
|
|
12154
12307
|
}
|
|
12155
12308
|
};
|
|
12156
12309
|
|
|
12310
|
+
// src/core/cascade.ts
|
|
12311
|
+
init_cost();
|
|
12312
|
+
|
|
12157
12313
|
// src/tools/base.ts
|
|
12158
12314
|
var BaseTool = class {
|
|
12159
12315
|
workspaceRoot = process.cwd();
|
|
@@ -12174,10 +12330,17 @@ var BaseTool = class {
|
|
|
12174
12330
|
|
|
12175
12331
|
// src/core/multimodal/generate.ts
|
|
12176
12332
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
12333
|
+
var GenerationGaveUpError = class extends Error {
|
|
12334
|
+
constructor(message) {
|
|
12335
|
+
super(message);
|
|
12336
|
+
this.name = "GenerationGaveUpError";
|
|
12337
|
+
}
|
|
12338
|
+
};
|
|
12177
12339
|
async function callProvider(fn, modelId) {
|
|
12178
12340
|
try {
|
|
12179
12341
|
return await fn();
|
|
12180
12342
|
} catch (err) {
|
|
12343
|
+
if (err instanceof GenerationGaveUpError) throw err;
|
|
12181
12344
|
const classified = classifyProviderError(err);
|
|
12182
12345
|
throw new Error(describeProviderError(classified, modelId));
|
|
12183
12346
|
}
|
|
@@ -12375,8 +12538,8 @@ async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
|
12375
12538
|
onProgress?.("Still rendering\u2026");
|
|
12376
12539
|
}
|
|
12377
12540
|
if (!operation.done) {
|
|
12378
|
-
throw new
|
|
12379
|
-
|
|
12541
|
+
throw new GenerationGaveUpError(
|
|
12542
|
+
`${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.`
|
|
12380
12543
|
);
|
|
12381
12544
|
}
|
|
12382
12545
|
if (operation.error?.message) throw new Error(operation.error.message);
|
|
@@ -12682,6 +12845,700 @@ function buildMediaTools(deps, readFile) {
|
|
|
12682
12845
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
12683
12846
|
return tools;
|
|
12684
12847
|
}
|
|
12848
|
+
var WorkspaceSandboxError = class extends Error {
|
|
12849
|
+
constructor(attempted, workspaceRoot) {
|
|
12850
|
+
super(`Refusing access to "${attempted}" \u2014 outside workspace root "${workspaceRoot}".`);
|
|
12851
|
+
this.name = "WorkspaceSandboxError";
|
|
12852
|
+
}
|
|
12853
|
+
};
|
|
12854
|
+
function resolveInWorkspace(workspaceRoot, input) {
|
|
12855
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
12856
|
+
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12857
|
+
}
|
|
12858
|
+
const root = path33.resolve(workspaceRoot);
|
|
12859
|
+
const abs = path33.isAbsolute(input) ? path33.resolve(input) : path33.resolve(root, input);
|
|
12860
|
+
const rel = path33.relative(root, abs);
|
|
12861
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path33.isAbsolute(rel)) {
|
|
12862
|
+
throw new WorkspaceSandboxError(input, root);
|
|
12863
|
+
}
|
|
12864
|
+
try {
|
|
12865
|
+
const real = fs30.realpathSync(abs);
|
|
12866
|
+
const realRel = path33.relative(root, real);
|
|
12867
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path33.isAbsolute(realRel))) {
|
|
12868
|
+
throw new WorkspaceSandboxError(input, root);
|
|
12869
|
+
}
|
|
12870
|
+
} catch (e) {
|
|
12871
|
+
if (e instanceof WorkspaceSandboxError) throw e;
|
|
12872
|
+
}
|
|
12873
|
+
return abs;
|
|
12874
|
+
}
|
|
12875
|
+
|
|
12876
|
+
// src/core/documents/blocks.ts
|
|
12877
|
+
function fileExt(name) {
|
|
12878
|
+
const base = name.split(/[\\/]/).pop() ?? name;
|
|
12879
|
+
const dot = base.lastIndexOf(".");
|
|
12880
|
+
return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
12881
|
+
}
|
|
12882
|
+
function parseDelimited(text, name = "") {
|
|
12883
|
+
const delim = fileExt(name) === "tsv" || text.indexOf(" ") !== -1 && text.indexOf(",") === -1 ? " " : ",";
|
|
12884
|
+
const rows = [];
|
|
12885
|
+
let row = [];
|
|
12886
|
+
let cell = "";
|
|
12887
|
+
let quoted = false;
|
|
12888
|
+
for (let i = 0; i < text.length; i++) {
|
|
12889
|
+
const ch = text[i];
|
|
12890
|
+
if (quoted) {
|
|
12891
|
+
if (ch === '"') {
|
|
12892
|
+
if (text[i + 1] === '"') {
|
|
12893
|
+
cell += '"';
|
|
12894
|
+
i++;
|
|
12895
|
+
} else quoted = false;
|
|
12896
|
+
} else cell += ch;
|
|
12897
|
+
} else if (ch === '"') {
|
|
12898
|
+
quoted = true;
|
|
12899
|
+
} else if (ch === delim) {
|
|
12900
|
+
row.push(cell);
|
|
12901
|
+
cell = "";
|
|
12902
|
+
} else if (ch === "\n") {
|
|
12903
|
+
row.push(cell);
|
|
12904
|
+
rows.push(row);
|
|
12905
|
+
row = [];
|
|
12906
|
+
cell = "";
|
|
12907
|
+
} else if (ch === "\r") ; else {
|
|
12908
|
+
cell += ch;
|
|
12909
|
+
}
|
|
12910
|
+
if (rows.length > 5e3) break;
|
|
12911
|
+
}
|
|
12912
|
+
if (cell !== "" || row.length) {
|
|
12913
|
+
row.push(cell);
|
|
12914
|
+
rows.push(row);
|
|
12915
|
+
}
|
|
12916
|
+
return rows.filter((r) => r.length > 1 || (r[0] ?? "") !== "");
|
|
12917
|
+
}
|
|
12918
|
+
var CHART_ALIASES = {
|
|
12919
|
+
bar: "bar",
|
|
12920
|
+
column: "bar",
|
|
12921
|
+
bars: "bar",
|
|
12922
|
+
barchart: "bar",
|
|
12923
|
+
horizontalbar: "bar",
|
|
12924
|
+
line: "line",
|
|
12925
|
+
lines: "line",
|
|
12926
|
+
linechart: "line",
|
|
12927
|
+
pie: "pie",
|
|
12928
|
+
piechart: "pie",
|
|
12929
|
+
doughnut: "doughnut",
|
|
12930
|
+
donut: "doughnut",
|
|
12931
|
+
area: "area",
|
|
12932
|
+
areachart: "area",
|
|
12933
|
+
scatter: "scatter",
|
|
12934
|
+
xy: "scatter"
|
|
12935
|
+
};
|
|
12936
|
+
function chartKind(raw) {
|
|
12937
|
+
return CHART_ALIASES[raw.trim().toLowerCase().replace(/[\s_-]/g, "")] ?? null;
|
|
12938
|
+
}
|
|
12939
|
+
function toNumber(cell) {
|
|
12940
|
+
const cleaned = cell.replace(/[$£€¥,\s%]/g, "").replace(/^\((.*)\)$/, "-$1");
|
|
12941
|
+
const n = Number.parseFloat(cleaned);
|
|
12942
|
+
return Number.isFinite(n) ? n : 0;
|
|
12943
|
+
}
|
|
12944
|
+
function parseChartSpec(kind, body) {
|
|
12945
|
+
const lines = body.replace(/\r\n/g, "\n").split("\n");
|
|
12946
|
+
let title;
|
|
12947
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
12948
|
+
const titleMatch = lines[0]?.match(/^\s*title\s*:\s*(.+)$/i);
|
|
12949
|
+
if (titleMatch) {
|
|
12950
|
+
title = titleMatch[1].trim();
|
|
12951
|
+
lines.shift();
|
|
12952
|
+
}
|
|
12953
|
+
const rows = parseDelimited(lines.join("\n").trim());
|
|
12954
|
+
if (rows.length < 2) return null;
|
|
12955
|
+
const header = rows[0].map((c) => c.trim());
|
|
12956
|
+
if (header.length < 2) return null;
|
|
12957
|
+
const dataRows = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ""));
|
|
12958
|
+
if (!dataRows.length) return null;
|
|
12959
|
+
const categories = dataRows.map((r, i) => (r[0] ?? "").trim() || `Item ${i + 1}`);
|
|
12960
|
+
const series = header.slice(1).map((name, col2) => ({
|
|
12961
|
+
name: name || `Series ${col2 + 1}`,
|
|
12962
|
+
values: dataRows.map((r) => toNumber((r[col2 + 1] ?? "").trim()))
|
|
12963
|
+
}));
|
|
12964
|
+
if (!series.length) return null;
|
|
12965
|
+
return {
|
|
12966
|
+
kind,
|
|
12967
|
+
...title ? { title } : {},
|
|
12968
|
+
categoryName: header[0] || "Category",
|
|
12969
|
+
categories,
|
|
12970
|
+
series
|
|
12971
|
+
};
|
|
12972
|
+
}
|
|
12973
|
+
function chartToTableRows(spec) {
|
|
12974
|
+
return [
|
|
12975
|
+
[spec.categoryName, ...spec.series.map((s) => s.name)],
|
|
12976
|
+
...spec.categories.map((cat, i) => [cat, ...spec.series.map((s) => String(s.values[i] ?? 0))])
|
|
12977
|
+
];
|
|
12978
|
+
}
|
|
12979
|
+
function parseBlocks(md) {
|
|
12980
|
+
const out = [];
|
|
12981
|
+
const lines = md.replace(/\r\n/g, "\n").split("\n");
|
|
12982
|
+
let i = 0;
|
|
12983
|
+
let ordinal = 0;
|
|
12984
|
+
const reset = () => {
|
|
12985
|
+
ordinal = 0;
|
|
12986
|
+
};
|
|
12987
|
+
while (i < lines.length) {
|
|
12988
|
+
const line = lines[i] ?? "";
|
|
12989
|
+
const fence = line.match(/^```+\s*(.*)$/);
|
|
12990
|
+
if (fence) {
|
|
12991
|
+
const info = (fence[1] ?? "").trim();
|
|
12992
|
+
i++;
|
|
12993
|
+
const code = [];
|
|
12994
|
+
while (i < lines.length && !/^```/.test(lines[i] ?? "")) {
|
|
12995
|
+
code.push(lines[i] ?? "");
|
|
12996
|
+
i++;
|
|
12997
|
+
}
|
|
12998
|
+
i++;
|
|
12999
|
+
const chartInfo = info.match(/^chart\s*:\s*(.+)$/i);
|
|
13000
|
+
const kind = chartInfo ? chartKind(chartInfo[1]) : null;
|
|
13001
|
+
const spec = kind ? parseChartSpec(kind, code.join("\n")) : null;
|
|
13002
|
+
out.push(spec ? { t: "chart", spec } : { t: "code", lines: code });
|
|
13003
|
+
reset();
|
|
13004
|
+
continue;
|
|
13005
|
+
}
|
|
13006
|
+
if (!line.trim()) {
|
|
13007
|
+
i++;
|
|
13008
|
+
reset();
|
|
13009
|
+
continue;
|
|
13010
|
+
}
|
|
13011
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
13012
|
+
if (h) {
|
|
13013
|
+
out.push({ t: "heading", level: h[1].length, text: h[2] });
|
|
13014
|
+
i++;
|
|
13015
|
+
reset();
|
|
13016
|
+
continue;
|
|
13017
|
+
}
|
|
13018
|
+
if (/^\s*([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
13019
|
+
out.push({ t: "hr" });
|
|
13020
|
+
i++;
|
|
13021
|
+
reset();
|
|
13022
|
+
continue;
|
|
13023
|
+
}
|
|
13024
|
+
const li = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
13025
|
+
if (li) {
|
|
13026
|
+
const ordered = /\d/.test(li[2]);
|
|
13027
|
+
ordinal = ordered ? ordinal + 1 : 0;
|
|
13028
|
+
out.push({ t: "bullet", ordered, index: ordered ? ordinal : 0, text: li[3] });
|
|
13029
|
+
i++;
|
|
13030
|
+
continue;
|
|
13031
|
+
}
|
|
13032
|
+
if (/^>\s?/.test(line)) {
|
|
13033
|
+
out.push({ t: "quote", text: line.replace(/^>\s?/, "") });
|
|
13034
|
+
i++;
|
|
13035
|
+
reset();
|
|
13036
|
+
continue;
|
|
13037
|
+
}
|
|
13038
|
+
if (/^\|.*\|/.test(line)) {
|
|
13039
|
+
const rows = [];
|
|
13040
|
+
while (i < lines.length && /^\|.*\|/.test(lines[i] ?? "")) {
|
|
13041
|
+
const r = lines[i];
|
|
13042
|
+
if (!/^\|[\s:|-]+\|?\s*$/.test(r)) rows.push(r.replace(/^\||\|\s*$/g, "").split("|").map((c) => c.trim()));
|
|
13043
|
+
i++;
|
|
13044
|
+
}
|
|
13045
|
+
out.push({ t: "table", rows });
|
|
13046
|
+
reset();
|
|
13047
|
+
continue;
|
|
13048
|
+
}
|
|
13049
|
+
out.push({ t: "para", text: line });
|
|
13050
|
+
i++;
|
|
13051
|
+
reset();
|
|
13052
|
+
}
|
|
13053
|
+
return out;
|
|
13054
|
+
}
|
|
13055
|
+
var IMAGE_LINE = /^!\[([^\]]*)\]\(([^)]+)\)$/;
|
|
13056
|
+
function matchImageLine(s) {
|
|
13057
|
+
const m = s.trim().match(IMAGE_LINE);
|
|
13058
|
+
return m ? { alt: m[1].trim(), url: m[2].trim() } : null;
|
|
13059
|
+
}
|
|
13060
|
+
function sniffImage(b) {
|
|
13061
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
13062
|
+
if (b.length > 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
|
|
13063
|
+
return { type: "png", mime: "image/png", width: dv.getUint32(16), height: dv.getUint32(20) };
|
|
13064
|
+
}
|
|
13065
|
+
if (b.length > 10 && b[0] === 71 && b[1] === 73 && b[2] === 70) {
|
|
13066
|
+
return { type: "gif", mime: "image/gif", width: dv.getUint16(6, true), height: dv.getUint16(8, true) };
|
|
13067
|
+
}
|
|
13068
|
+
if (b.length > 26 && b[0] === 66 && b[1] === 77) {
|
|
13069
|
+
return { type: "bmp", mime: "image/bmp", width: Math.abs(dv.getInt32(18, true)), height: Math.abs(dv.getInt32(22, true)) };
|
|
13070
|
+
}
|
|
13071
|
+
if (b.length > 4 && b[0] === 255 && b[1] === 216) {
|
|
13072
|
+
let i = 2;
|
|
13073
|
+
while (i + 9 < b.length) {
|
|
13074
|
+
if (b[i] !== 255) {
|
|
13075
|
+
i++;
|
|
13076
|
+
continue;
|
|
13077
|
+
}
|
|
13078
|
+
const marker = b[i + 1];
|
|
13079
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
13080
|
+
return { type: "jpg", mime: "image/jpeg", height: dv.getUint16(i + 5), width: dv.getUint16(i + 7) };
|
|
13081
|
+
}
|
|
13082
|
+
i += 2 + dv.getUint16(i + 2);
|
|
13083
|
+
}
|
|
13084
|
+
}
|
|
13085
|
+
return null;
|
|
13086
|
+
}
|
|
13087
|
+
function bytesToBase64(bytes) {
|
|
13088
|
+
const g = globalThis;
|
|
13089
|
+
if (g.Buffer) return g.Buffer.from(bytes).toString("base64");
|
|
13090
|
+
const CHUNK = 32768;
|
|
13091
|
+
let binary = "";
|
|
13092
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
13093
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
13094
|
+
}
|
|
13095
|
+
return g.btoa(binary);
|
|
13096
|
+
}
|
|
13097
|
+
function stripInline(s) {
|
|
13098
|
+
return s.replace(/\*\*(.+?)\*\*/g, "$1").replace(/(^|[^*])\*(?!\s)(.+?)\*/g, "$1$2").replace(/`([^`]+)`/g, "$1").replace(/\[(.+?)\]\((.+?)\)/g, "$1 ($2)");
|
|
13099
|
+
}
|
|
13100
|
+
function inlineRuns(text) {
|
|
13101
|
+
const acc = [];
|
|
13102
|
+
const re = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]{1,500}\]\([^)]{1,2000}\)|\*[^*\s][^*]*\*)/g;
|
|
13103
|
+
let last = 0;
|
|
13104
|
+
let m;
|
|
13105
|
+
while ((m = re.exec(text)) !== null) {
|
|
13106
|
+
if (m.index > last) acc.push({ text: text.slice(last, m.index) });
|
|
13107
|
+
const tok = m[0];
|
|
13108
|
+
if (tok.startsWith("**")) acc.push({ text: tok.slice(2, -2), bold: true });
|
|
13109
|
+
else if (tok.startsWith("`")) acc.push({ text: tok.slice(1, -1), code: true });
|
|
13110
|
+
else if (tok.startsWith("[")) {
|
|
13111
|
+
const l = tok.match(/\[([^\]]{1,500})\]\(([^)]{1,2000})\)/);
|
|
13112
|
+
acc.push({ text: l ? `${l[1]} (${l[2]})` : tok });
|
|
13113
|
+
} else acc.push({ text: tok.slice(1, -1), italic: true });
|
|
13114
|
+
last = re.lastIndex;
|
|
13115
|
+
}
|
|
13116
|
+
if (last < text.length) acc.push({ text: text.slice(last) });
|
|
13117
|
+
return acc.length ? acc : [{ text }];
|
|
13118
|
+
}
|
|
13119
|
+
function splitSlides(md) {
|
|
13120
|
+
const norm = md.replace(/\r\n/g, "\n").trim();
|
|
13121
|
+
let chunks = norm.split(/\n\s*-{3,}\s*\n/);
|
|
13122
|
+
if (chunks.length === 1) {
|
|
13123
|
+
const lines = norm.split("\n");
|
|
13124
|
+
const sections = [];
|
|
13125
|
+
let cur = [];
|
|
13126
|
+
for (const ln of lines) {
|
|
13127
|
+
if (/^#{1,2}\s+/.test(ln) && cur.some((l) => l.trim())) {
|
|
13128
|
+
sections.push(cur.join("\n"));
|
|
13129
|
+
cur = [];
|
|
13130
|
+
}
|
|
13131
|
+
cur.push(ln);
|
|
13132
|
+
}
|
|
13133
|
+
if (cur.length) sections.push(cur.join("\n"));
|
|
13134
|
+
if (sections.length > 1) chunks = sections;
|
|
13135
|
+
}
|
|
13136
|
+
return chunks.map(parseSlide).filter((s) => s.title || s.body.length || s.images.length || s.charts.length);
|
|
13137
|
+
}
|
|
13138
|
+
function parseSlide(chunk) {
|
|
13139
|
+
let title = "";
|
|
13140
|
+
const body = [];
|
|
13141
|
+
const images = [];
|
|
13142
|
+
const charts = [];
|
|
13143
|
+
const lines = chunk.split("\n");
|
|
13144
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13145
|
+
const raw = lines[i] ?? "";
|
|
13146
|
+
const fence = raw.match(/^[ \t]*```+[ \t]*chart[ \t]*:(.*)/i);
|
|
13147
|
+
if (fence) {
|
|
13148
|
+
const kind = chartKind(fence[1]);
|
|
13149
|
+
const bodyLines = [];
|
|
13150
|
+
i++;
|
|
13151
|
+
while (i < lines.length && !/^\s*```/.test(lines[i] ?? "")) {
|
|
13152
|
+
bodyLines.push(lines[i] ?? "");
|
|
13153
|
+
i++;
|
|
13154
|
+
}
|
|
13155
|
+
const spec = kind ? parseChartSpec(kind, bodyLines.join("\n")) : null;
|
|
13156
|
+
if (spec) charts.push(spec);
|
|
13157
|
+
else for (const bl of bodyLines) if (bl.trim()) body.push(stripInline(bl.trim()));
|
|
13158
|
+
continue;
|
|
13159
|
+
}
|
|
13160
|
+
const line = raw.trim();
|
|
13161
|
+
if (!line) continue;
|
|
13162
|
+
const h = line.match(/^#{1,6}[ \t]+(.*)/);
|
|
13163
|
+
if (h) {
|
|
13164
|
+
if (!title) title = stripInline(h[1]);
|
|
13165
|
+
else body.push(stripInline(h[1]));
|
|
13166
|
+
continue;
|
|
13167
|
+
}
|
|
13168
|
+
const img = matchImageLine(line);
|
|
13169
|
+
if (img) {
|
|
13170
|
+
images.push(img);
|
|
13171
|
+
continue;
|
|
13172
|
+
}
|
|
13173
|
+
const li = line.match(/^([-*+]|\d+[.)])[ \t]+(.*)/);
|
|
13174
|
+
if (li) {
|
|
13175
|
+
const bulletImg = matchImageLine(li[2]);
|
|
13176
|
+
if (bulletImg) {
|
|
13177
|
+
images.push(bulletImg);
|
|
13178
|
+
continue;
|
|
13179
|
+
}
|
|
13180
|
+
body.push(stripInline(li[2]));
|
|
13181
|
+
continue;
|
|
13182
|
+
}
|
|
13183
|
+
if (/^>\s?/.test(line)) {
|
|
13184
|
+
body.push(stripInline(line.replace(/^>\s?/, "")));
|
|
13185
|
+
continue;
|
|
13186
|
+
}
|
|
13187
|
+
body.push(stripInline(line));
|
|
13188
|
+
}
|
|
13189
|
+
return { title, body, images, charts };
|
|
13190
|
+
}
|
|
13191
|
+
function extractCharts(source) {
|
|
13192
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
13193
|
+
const charts = [];
|
|
13194
|
+
const rest = [];
|
|
13195
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13196
|
+
const fence = lines[i].match(/^[ \t]*```+[ \t]*chart[ \t]*:(.*)/i);
|
|
13197
|
+
if (!fence) {
|
|
13198
|
+
rest.push(lines[i]);
|
|
13199
|
+
continue;
|
|
13200
|
+
}
|
|
13201
|
+
const kind = chartKind(fence[1]);
|
|
13202
|
+
const body = [];
|
|
13203
|
+
i++;
|
|
13204
|
+
while (i < lines.length && !/^\s*```/.test(lines[i] ?? "")) {
|
|
13205
|
+
body.push(lines[i] ?? "");
|
|
13206
|
+
i++;
|
|
13207
|
+
}
|
|
13208
|
+
const spec = kind ? parseChartSpec(kind, body.join("\n")) : null;
|
|
13209
|
+
if (spec) charts.push(spec);
|
|
13210
|
+
else rest.push(...body);
|
|
13211
|
+
}
|
|
13212
|
+
return { charts, rest: rest.join("\n") };
|
|
13213
|
+
}
|
|
13214
|
+
|
|
13215
|
+
// src/core/documents/render.ts
|
|
13216
|
+
function isDocumentFormat(ext) {
|
|
13217
|
+
return ext === "docx" || ext === "pptx" || ext === "xlsx";
|
|
13218
|
+
}
|
|
13219
|
+
async function loadImage(ref, load) {
|
|
13220
|
+
if (!load) return null;
|
|
13221
|
+
try {
|
|
13222
|
+
const bytes = await load(ref.url);
|
|
13223
|
+
if (!bytes || !bytes.length) return null;
|
|
13224
|
+
const info = sniffImage(bytes);
|
|
13225
|
+
if (!info || info.width <= 0 || info.height <= 0) return null;
|
|
13226
|
+
return { ...info, bytes, alt: ref.alt };
|
|
13227
|
+
} catch {
|
|
13228
|
+
return null;
|
|
13229
|
+
}
|
|
13230
|
+
}
|
|
13231
|
+
async function renderXlsx(source, opts = {}) {
|
|
13232
|
+
const XLSX = await import('xlsx');
|
|
13233
|
+
const { charts, rest } = extractCharts(source);
|
|
13234
|
+
const name = opts.name ?? "";
|
|
13235
|
+
const rows = parseDelimited(rest, name.toLowerCase().endsWith(".tsv") ? "x.tsv" : "x.csv");
|
|
13236
|
+
const wb = XLSX.utils.book_new();
|
|
13237
|
+
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(rows.length ? rows : [[""]]), "Sheet1");
|
|
13238
|
+
const used = /* @__PURE__ */ new Set(["Sheet1"]);
|
|
13239
|
+
charts.forEach((spec, i) => {
|
|
13240
|
+
const base = (spec.title || `Chart ${i + 1}`).replace(/[:\\/?*[\]]/g, " ").trim().slice(0, 28) || `Chart ${i + 1}`;
|
|
13241
|
+
let sheetName = base;
|
|
13242
|
+
let n = 2;
|
|
13243
|
+
while (used.has(sheetName)) sheetName = `${base} ${n++}`.slice(0, 31);
|
|
13244
|
+
used.add(sheetName);
|
|
13245
|
+
const aoa = [
|
|
13246
|
+
[spec.categoryName, ...spec.series.map((s) => s.name)],
|
|
13247
|
+
...spec.categories.map((cat, r) => [cat, ...spec.series.map((s) => s.values[r] ?? 0)])
|
|
13248
|
+
];
|
|
13249
|
+
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sheetName);
|
|
13250
|
+
});
|
|
13251
|
+
return new Uint8Array(XLSX.write(wb, { type: "array", bookType: "xlsx" }));
|
|
13252
|
+
}
|
|
13253
|
+
var DOCX_MAX_W = 600;
|
|
13254
|
+
var DOCX_MAX_H = 864;
|
|
13255
|
+
async function renderDocx(md, opts = {}) {
|
|
13256
|
+
const d = await import('docx');
|
|
13257
|
+
const {
|
|
13258
|
+
Document,
|
|
13259
|
+
Packer,
|
|
13260
|
+
Paragraph,
|
|
13261
|
+
TextRun,
|
|
13262
|
+
ImageRun,
|
|
13263
|
+
HeadingLevel,
|
|
13264
|
+
Table,
|
|
13265
|
+
TableRow,
|
|
13266
|
+
TableCell,
|
|
13267
|
+
WidthType,
|
|
13268
|
+
BorderStyle,
|
|
13269
|
+
AlignmentType
|
|
13270
|
+
} = d;
|
|
13271
|
+
const HEADINGS = [
|
|
13272
|
+
HeadingLevel.HEADING_1,
|
|
13273
|
+
HeadingLevel.HEADING_2,
|
|
13274
|
+
HeadingLevel.HEADING_3,
|
|
13275
|
+
HeadingLevel.HEADING_4,
|
|
13276
|
+
HeadingLevel.HEADING_5,
|
|
13277
|
+
HeadingLevel.HEADING_6
|
|
13278
|
+
];
|
|
13279
|
+
const runs = (text) => inlineRuns(text).map((r) => new TextRun({
|
|
13280
|
+
text: r.text,
|
|
13281
|
+
...r.bold ? { bold: true } : {},
|
|
13282
|
+
...r.italic ? { italics: true } : {},
|
|
13283
|
+
...r.code ? { font: "Courier New" } : {}
|
|
13284
|
+
}));
|
|
13285
|
+
const tableOf = (rows) => new Table({
|
|
13286
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
13287
|
+
rows: rows.map((r) => new TableRow({
|
|
13288
|
+
children: r.map((c) => new TableCell({ children: [new Paragraph({ children: runs(c) })] }))
|
|
13289
|
+
}))
|
|
13290
|
+
});
|
|
13291
|
+
const imageParagraph = (img) => {
|
|
13292
|
+
const scale = Math.min(1, DOCX_MAX_W / img.width, DOCX_MAX_H / img.height);
|
|
13293
|
+
return new Paragraph({
|
|
13294
|
+
children: [new ImageRun({
|
|
13295
|
+
type: img.type,
|
|
13296
|
+
data: img.bytes,
|
|
13297
|
+
transformation: { width: Math.round(img.width * scale), height: Math.round(img.height * scale) },
|
|
13298
|
+
...img.alt ? { altText: { name: img.alt, description: img.alt, title: img.alt } } : {}
|
|
13299
|
+
})]
|
|
13300
|
+
});
|
|
13301
|
+
};
|
|
13302
|
+
const asImage = async (text) => {
|
|
13303
|
+
const ref = matchImageLine(text);
|
|
13304
|
+
return ref ? await loadImage(ref, opts.loadImageBytes) : null;
|
|
13305
|
+
};
|
|
13306
|
+
const children = [];
|
|
13307
|
+
for (const b of parseBlocks(md)) {
|
|
13308
|
+
switch (b.t) {
|
|
13309
|
+
case "heading":
|
|
13310
|
+
children.push(new Paragraph({ heading: HEADINGS[Math.min(b.level, 6) - 1], children: runs(b.text) }));
|
|
13311
|
+
break;
|
|
13312
|
+
case "para": {
|
|
13313
|
+
const img = await asImage(b.text);
|
|
13314
|
+
children.push(img ? imageParagraph(img) : new Paragraph({ children: runs(b.text) }));
|
|
13315
|
+
break;
|
|
13316
|
+
}
|
|
13317
|
+
case "bullet": {
|
|
13318
|
+
const img = await asImage(b.text);
|
|
13319
|
+
children.push(img ? imageParagraph(img) : new Paragraph({
|
|
13320
|
+
children: runs(b.text),
|
|
13321
|
+
...b.ordered ? { numbering: { reference: "ol", level: 0 } } : { bullet: { level: 0 } }
|
|
13322
|
+
}));
|
|
13323
|
+
break;
|
|
13324
|
+
}
|
|
13325
|
+
case "code":
|
|
13326
|
+
for (const cl of b.lines) {
|
|
13327
|
+
children.push(new Paragraph({ children: [new TextRun({ text: cl || " ", font: "Courier New", size: 18 })] }));
|
|
13328
|
+
}
|
|
13329
|
+
break;
|
|
13330
|
+
case "quote":
|
|
13331
|
+
children.push(new Paragraph({ indent: { left: 360 }, children: [new TextRun({ text: stripInline(b.text), italics: true })] }));
|
|
13332
|
+
break;
|
|
13333
|
+
case "table":
|
|
13334
|
+
children.push(tableOf(b.rows));
|
|
13335
|
+
break;
|
|
13336
|
+
case "chart":
|
|
13337
|
+
if (b.spec.title) {
|
|
13338
|
+
children.push(new Paragraph({ children: [new TextRun({ text: b.spec.title, bold: true })] }));
|
|
13339
|
+
}
|
|
13340
|
+
children.push(tableOf(chartToTableRows(b.spec)));
|
|
13341
|
+
break;
|
|
13342
|
+
case "hr":
|
|
13343
|
+
children.push(new Paragraph({
|
|
13344
|
+
children: [],
|
|
13345
|
+
border: { bottom: { color: "CCCCCC", space: 1, style: BorderStyle.SINGLE, size: 6 } }
|
|
13346
|
+
}));
|
|
13347
|
+
break;
|
|
13348
|
+
}
|
|
13349
|
+
}
|
|
13350
|
+
const doc = new Document({
|
|
13351
|
+
numbering: {
|
|
13352
|
+
config: [{ reference: "ol", levels: [{ level: 0, format: "decimal", text: "%1.", alignment: AlignmentType.START }] }]
|
|
13353
|
+
},
|
|
13354
|
+
sections: [{ children: children.length ? children : [new Paragraph({ children: [new TextRun("")] })] }]
|
|
13355
|
+
});
|
|
13356
|
+
return new Uint8Array(await Packer.toArrayBuffer(doc));
|
|
13357
|
+
}
|
|
13358
|
+
function pptxConstructor(mod) {
|
|
13359
|
+
let c = mod;
|
|
13360
|
+
for (let i = 0; i < 3 && c && typeof c !== "function"; i++) c = c.default;
|
|
13361
|
+
if (typeof c !== "function") throw new Error("pptxgenjs did not export a constructor.");
|
|
13362
|
+
return c;
|
|
13363
|
+
}
|
|
13364
|
+
var SLIDE_H = 7.5;
|
|
13365
|
+
var CONTENT_X = 0.6;
|
|
13366
|
+
var CONTENT_W = 12.1;
|
|
13367
|
+
var CONTENT_BOTTOM = 0.4;
|
|
13368
|
+
async function renderPptx(md, opts = {}) {
|
|
13369
|
+
const PptxGen = pptxConstructor(await import('pptxgenjs'));
|
|
13370
|
+
const pptx = new PptxGen();
|
|
13371
|
+
pptx.layout = "LAYOUT_WIDE";
|
|
13372
|
+
const CHART_TYPES = {
|
|
13373
|
+
bar: pptx.ChartType["bar"],
|
|
13374
|
+
line: pptx.ChartType["line"],
|
|
13375
|
+
pie: pptx.ChartType["pie"],
|
|
13376
|
+
doughnut: pptx.ChartType["doughnut"],
|
|
13377
|
+
area: pptx.ChartType["area"],
|
|
13378
|
+
scatter: pptx.ChartType["scatter"]
|
|
13379
|
+
};
|
|
13380
|
+
const slides = splitSlides(md);
|
|
13381
|
+
const blank = { title: "", body: [""], images: [], charts: [] };
|
|
13382
|
+
for (const s of slides.length ? slides : [blank]) {
|
|
13383
|
+
const slide = pptx.addSlide();
|
|
13384
|
+
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" });
|
|
13385
|
+
const top = s.title ? 1.5 : 0.5;
|
|
13386
|
+
const pics = (await Promise.all(s.images.map((r) => loadImage(r, opts.loadImageBytes)))).filter((p) => p !== null);
|
|
13387
|
+
const visuals = [
|
|
13388
|
+
...s.charts.map((spec) => ({ kind: "chart", spec })),
|
|
13389
|
+
...pics.map((img) => ({ kind: "image", img }))
|
|
13390
|
+
];
|
|
13391
|
+
const bodyH = visuals.length ? 2.2 : 5.4;
|
|
13392
|
+
if (s.body.length) {
|
|
13393
|
+
slide.addText(
|
|
13394
|
+
s.body.map((t) => ({ text: t, options: { bullet: true, breakLine: true } })),
|
|
13395
|
+
{ x: CONTENT_X, y: top, w: CONTENT_W, h: bodyH, fontSize: 16, color: "374151", valign: "top" }
|
|
13396
|
+
);
|
|
13397
|
+
}
|
|
13398
|
+
if (visuals.length) {
|
|
13399
|
+
const y = top + (s.body.length ? bodyH + 0.2 : 0);
|
|
13400
|
+
const h = Math.max(1, SLIDE_H - CONTENT_BOTTOM - y);
|
|
13401
|
+
const gap = 0.3;
|
|
13402
|
+
const w = (CONTENT_W - gap * (visuals.length - 1)) / visuals.length;
|
|
13403
|
+
visuals.forEach((v, i) => {
|
|
13404
|
+
const x = CONTENT_X + i * (w + gap);
|
|
13405
|
+
if (v.kind === "image") {
|
|
13406
|
+
slide.addImage({
|
|
13407
|
+
data: `data:${v.img.mime};base64,${bytesToBase64(v.img.bytes)}`,
|
|
13408
|
+
x,
|
|
13409
|
+
y,
|
|
13410
|
+
w,
|
|
13411
|
+
h,
|
|
13412
|
+
// 'contain' keeps the aspect ratio inside the box — a stretched
|
|
13413
|
+
// render looks worse than a smaller correct one.
|
|
13414
|
+
sizing: { type: "contain", w, h },
|
|
13415
|
+
...v.img.alt ? { altText: v.img.alt } : {}
|
|
13416
|
+
});
|
|
13417
|
+
} else {
|
|
13418
|
+
slide.addChart(CHART_TYPES[v.spec.kind], chartData(v.spec), {
|
|
13419
|
+
x,
|
|
13420
|
+
y,
|
|
13421
|
+
w,
|
|
13422
|
+
h,
|
|
13423
|
+
showLegend: v.spec.series.length > 1 || v.spec.kind === "pie" || v.spec.kind === "doughnut",
|
|
13424
|
+
legendPos: "b",
|
|
13425
|
+
showTitle: !!v.spec.title,
|
|
13426
|
+
...v.spec.title ? { title: v.spec.title } : {},
|
|
13427
|
+
showValue: v.spec.kind === "pie" || v.spec.kind === "doughnut"
|
|
13428
|
+
});
|
|
13429
|
+
}
|
|
13430
|
+
});
|
|
13431
|
+
}
|
|
13432
|
+
}
|
|
13433
|
+
return new Uint8Array(await pptx.write({ outputType: "arraybuffer" }));
|
|
13434
|
+
}
|
|
13435
|
+
function chartData(spec) {
|
|
13436
|
+
const series = spec.kind === "pie" || spec.kind === "doughnut" ? spec.series.slice(0, 1) : spec.series;
|
|
13437
|
+
return series.map((s) => ({ name: s.name, labels: spec.categories, values: s.values }));
|
|
13438
|
+
}
|
|
13439
|
+
async function renderDocument(format, source, opts = {}) {
|
|
13440
|
+
if (format === "docx") return renderDocx(source, opts);
|
|
13441
|
+
if (format === "pptx") return renderPptx(source, opts);
|
|
13442
|
+
return renderXlsx(source, opts);
|
|
13443
|
+
}
|
|
13444
|
+
|
|
13445
|
+
// src/tools/generate-document.ts
|
|
13446
|
+
var SOURCE_HINT = {
|
|
13447
|
+
docx: "Markdown",
|
|
13448
|
+
pptx: "Markdown slides (separate slides with a --- rule; each slide starts with a heading)",
|
|
13449
|
+
xlsx: "CSV (one header row, then data rows)"
|
|
13450
|
+
};
|
|
13451
|
+
var GenerateDocumentTool = class extends BaseTool {
|
|
13452
|
+
/**
|
|
13453
|
+
* `readFile` is injected so the image loader is testable and so a host with a
|
|
13454
|
+
* different workspace (not plain disk) can supply its own reader.
|
|
13455
|
+
*/
|
|
13456
|
+
constructor(readFile = async (p) => new Uint8Array(await fs11.readFile(p))) {
|
|
13457
|
+
super();
|
|
13458
|
+
this.readFile = readFile;
|
|
13459
|
+
}
|
|
13460
|
+
readFile;
|
|
13461
|
+
name = "generate_document";
|
|
13462
|
+
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.";
|
|
13463
|
+
inputSchema = {
|
|
13464
|
+
type: "object",
|
|
13465
|
+
properties: {
|
|
13466
|
+
path: {
|
|
13467
|
+
type: "string",
|
|
13468
|
+
description: "Where to write the file, relative to the workspace root. The extension decides the format: .docx, .pptx or .xlsx."
|
|
13469
|
+
},
|
|
13470
|
+
content: {
|
|
13471
|
+
type: "string",
|
|
13472
|
+
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.'
|
|
13473
|
+
},
|
|
13474
|
+
format: {
|
|
13475
|
+
type: "string",
|
|
13476
|
+
description: 'Optional override: "docx", "pptx" or "xlsx". Normally inferred from the path extension.'
|
|
13477
|
+
}
|
|
13478
|
+
},
|
|
13479
|
+
required: ["path", "content"]
|
|
13480
|
+
};
|
|
13481
|
+
isDangerous() {
|
|
13482
|
+
return true;
|
|
13483
|
+
}
|
|
13484
|
+
async execute(input, options) {
|
|
13485
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
13486
|
+
if (!filePath) return 'Provide a "path" for the document, e.g. "report.docx".';
|
|
13487
|
+
const content = typeof input["content"] === "string" ? input["content"] : "";
|
|
13488
|
+
if (!content.trim()) return 'Provide "content" \u2014 the Markdown (docx/pptx) or CSV (xlsx) source for the document.';
|
|
13489
|
+
const requested = String(input["format"] ?? "").trim().toLowerCase().replace(/^\./, "");
|
|
13490
|
+
const ext = requested || fileExt(filePath);
|
|
13491
|
+
if (!isDocumentFormat(ext)) {
|
|
13492
|
+
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.`;
|
|
13493
|
+
}
|
|
13494
|
+
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
13495
|
+
const embedded = [];
|
|
13496
|
+
const skipped = [];
|
|
13497
|
+
const loadImageBytes = async (url) => {
|
|
13498
|
+
if (/^(https?|ftp):\/\//i.test(url)) {
|
|
13499
|
+
skipped.push(`${url} (remote URL \u2014 download it into the workspace first)`);
|
|
13500
|
+
return null;
|
|
13501
|
+
}
|
|
13502
|
+
const local = url.replace(/^file:\/\//i, "").split("?")[0].split("#")[0];
|
|
13503
|
+
try {
|
|
13504
|
+
const bytes2 = await this.readFile(resolveInWorkspace(this.workspaceRoot, decodeURIComponent(local)));
|
|
13505
|
+
embedded.push(local);
|
|
13506
|
+
return bytes2;
|
|
13507
|
+
} catch {
|
|
13508
|
+
skipped.push(`${local} (not found in the workspace)`);
|
|
13509
|
+
return null;
|
|
13510
|
+
}
|
|
13511
|
+
};
|
|
13512
|
+
if (options.saveSnapshot) {
|
|
13513
|
+
try {
|
|
13514
|
+
const old = await fs11.readFile(absPath, "utf-8");
|
|
13515
|
+
await options.saveSnapshot(absPath, old);
|
|
13516
|
+
} catch {
|
|
13517
|
+
}
|
|
13518
|
+
}
|
|
13519
|
+
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
13520
|
+
await fs11.mkdir(path33.dirname(absPath), { recursive: true });
|
|
13521
|
+
await fs11.writeFile(absPath, bytes);
|
|
13522
|
+
const charts = countCharts(content);
|
|
13523
|
+
return [
|
|
13524
|
+
`Wrote a real ${LABEL[ext]} file to ${filePath} (${bytes.length} bytes) from ${SOURCE_HINT[ext]}.`,
|
|
13525
|
+
embedded.length ? `Embedded ${embedded.length} image(s): ${embedded.join(", ")}.` : "",
|
|
13526
|
+
// Named, not counted: a dropped image is the failure the user actually
|
|
13527
|
+
// notices, and the model can only fix it if it is told which reference died.
|
|
13528
|
+
skipped.length ? `Could NOT embed ${skipped.length} image reference(s): ${skipped.join("; ")}.` : "",
|
|
13529
|
+
charts && ext === "pptx" ? `Rendered ${charts} chart block(s) as real, editable PowerPoint charts.` : "",
|
|
13530
|
+
charts && ext === "docx" ? `Rendered ${charts} chart block(s) as data tables \u2014 Word documents cannot hold a native chart object.` : "",
|
|
13531
|
+
charts && ext === "xlsx" ? `Wrote ${charts} chart block(s) onto their own worksheets as real rows the user can chart in Excel.` : ""
|
|
13532
|
+
].filter(Boolean).join("\n");
|
|
13533
|
+
}
|
|
13534
|
+
};
|
|
13535
|
+
var LABEL = { docx: "Word", pptx: "PowerPoint", xlsx: "Excel" };
|
|
13536
|
+
function countCharts(source) {
|
|
13537
|
+
return (source.match(/^\s*```+\s*chart\s*:/gim) ?? []).length;
|
|
13538
|
+
}
|
|
13539
|
+
function buildDocumentTools(readFile) {
|
|
13540
|
+
return readFile ? [new GenerateDocumentTool(readFile)] : [];
|
|
13541
|
+
}
|
|
12685
13542
|
|
|
12686
13543
|
// src/core/run-breaker.ts
|
|
12687
13544
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
@@ -12830,35 +13687,6 @@ ${[e.stdout, e.stderr].filter(Boolean).join("\n").trim()}`;
|
|
|
12830
13687
|
}
|
|
12831
13688
|
}
|
|
12832
13689
|
};
|
|
12833
|
-
var WorkspaceSandboxError = class extends Error {
|
|
12834
|
-
constructor(attempted, workspaceRoot) {
|
|
12835
|
-
super(`Refusing access to "${attempted}" \u2014 outside workspace root "${workspaceRoot}".`);
|
|
12836
|
-
this.name = "WorkspaceSandboxError";
|
|
12837
|
-
}
|
|
12838
|
-
};
|
|
12839
|
-
function resolveInWorkspace(workspaceRoot, input) {
|
|
12840
|
-
if (typeof input !== "string" || input.length === 0) {
|
|
12841
|
-
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12842
|
-
}
|
|
12843
|
-
const root = path32.resolve(workspaceRoot);
|
|
12844
|
-
const abs = path32.isAbsolute(input) ? path32.resolve(input) : path32.resolve(root, input);
|
|
12845
|
-
const rel = path32.relative(root, abs);
|
|
12846
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path32.isAbsolute(rel)) {
|
|
12847
|
-
throw new WorkspaceSandboxError(input, root);
|
|
12848
|
-
}
|
|
12849
|
-
try {
|
|
12850
|
-
const real = fs29.realpathSync(abs);
|
|
12851
|
-
const realRel = path32.relative(root, real);
|
|
12852
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path32.isAbsolute(realRel))) {
|
|
12853
|
-
throw new WorkspaceSandboxError(input, root);
|
|
12854
|
-
}
|
|
12855
|
-
} catch (e) {
|
|
12856
|
-
if (e instanceof WorkspaceSandboxError) throw e;
|
|
12857
|
-
}
|
|
12858
|
-
return abs;
|
|
12859
|
-
}
|
|
12860
|
-
|
|
12861
|
-
// src/tools/file.ts
|
|
12862
13690
|
var FileReadTool = class extends BaseTool {
|
|
12863
13691
|
name = "file_read";
|
|
12864
13692
|
description = "Read the contents of a file. Returns the file content as a string.";
|
|
@@ -12876,7 +13704,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
12876
13704
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
12877
13705
|
const offset = input["offset"] ?? 1;
|
|
12878
13706
|
const limit = input["limit"];
|
|
12879
|
-
const content = await
|
|
13707
|
+
const content = await fs11.readFile(absPath, "utf-8");
|
|
12880
13708
|
const lines = content.split("\n");
|
|
12881
13709
|
const start = Math.max(0, offset - 1);
|
|
12882
13710
|
const end = limit ? start + limit : lines.length;
|
|
@@ -12905,13 +13733,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
12905
13733
|
const content = input["content"];
|
|
12906
13734
|
if (options.saveSnapshot) {
|
|
12907
13735
|
try {
|
|
12908
|
-
const oldContent = await
|
|
13736
|
+
const oldContent = await fs11.readFile(absPath, "utf-8");
|
|
12909
13737
|
await options.saveSnapshot(absPath, oldContent);
|
|
12910
13738
|
} catch {
|
|
12911
13739
|
}
|
|
12912
13740
|
}
|
|
12913
|
-
await
|
|
12914
|
-
await
|
|
13741
|
+
await fs11.mkdir(path33.dirname(absPath), { recursive: true });
|
|
13742
|
+
await fs11.writeFile(absPath, content, "utf-8");
|
|
12915
13743
|
return `Written ${content.length} characters to ${filePath}`;
|
|
12916
13744
|
}
|
|
12917
13745
|
};
|
|
@@ -12937,7 +13765,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
12937
13765
|
const oldString = input["old_string"];
|
|
12938
13766
|
const newString = input["new_string"];
|
|
12939
13767
|
const replaceAll = input["replace_all"] ?? false;
|
|
12940
|
-
const rawContent = await
|
|
13768
|
+
const rawContent = await fs11.readFile(absPath, "utf-8");
|
|
12941
13769
|
if (options.saveSnapshot) {
|
|
12942
13770
|
await options.saveSnapshot(absPath, rawContent);
|
|
12943
13771
|
}
|
|
@@ -12949,7 +13777,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
12949
13777
|
);
|
|
12950
13778
|
}
|
|
12951
13779
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
12952
|
-
await
|
|
13780
|
+
await fs11.writeFile(absPath, updated, "utf-8");
|
|
12953
13781
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
12954
13782
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
12955
13783
|
}
|
|
@@ -12972,12 +13800,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
12972
13800
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
12973
13801
|
if (options.saveSnapshot) {
|
|
12974
13802
|
try {
|
|
12975
|
-
const oldContent = await
|
|
13803
|
+
const oldContent = await fs11.readFile(absPath, "utf-8");
|
|
12976
13804
|
await options.saveSnapshot(absPath, oldContent);
|
|
12977
13805
|
} catch {
|
|
12978
13806
|
}
|
|
12979
13807
|
}
|
|
12980
|
-
await
|
|
13808
|
+
await fs11.rm(absPath, { recursive: false });
|
|
12981
13809
|
return `Deleted ${filePath}`;
|
|
12982
13810
|
}
|
|
12983
13811
|
};
|
|
@@ -12994,7 +13822,7 @@ var FileListTool = class extends BaseTool {
|
|
|
12994
13822
|
async execute(input, _options) {
|
|
12995
13823
|
const inputPath = input["path"] || ".";
|
|
12996
13824
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
12997
|
-
const entries = await
|
|
13825
|
+
const entries = await fs11.readdir(absPath, { withFileTypes: true });
|
|
12998
13826
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
12999
13827
|
}
|
|
13000
13828
|
};
|
|
@@ -13407,8 +14235,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
13407
14235
|
}
|
|
13408
14236
|
};
|
|
13409
14237
|
async function fileToImageAttachment(filePath) {
|
|
13410
|
-
const data = await
|
|
13411
|
-
const ext =
|
|
14238
|
+
const data = await fs11.readFile(filePath);
|
|
14239
|
+
const ext = path33.extname(filePath).toLowerCase();
|
|
13412
14240
|
const mimeMap = {
|
|
13413
14241
|
".jpg": "image/jpeg",
|
|
13414
14242
|
".jpeg": "image/jpeg",
|
|
@@ -13442,14 +14270,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
13442
14270
|
const filePath = input["path"];
|
|
13443
14271
|
const content = input["content"];
|
|
13444
14272
|
const title = input["title"];
|
|
13445
|
-
const dir =
|
|
13446
|
-
if (!
|
|
13447
|
-
|
|
14273
|
+
const dir = path33.dirname(filePath);
|
|
14274
|
+
if (!fs30.existsSync(dir)) {
|
|
14275
|
+
fs30.mkdirSync(dir, { recursive: true });
|
|
13448
14276
|
}
|
|
13449
14277
|
return new Promise((resolve2, reject) => {
|
|
13450
14278
|
try {
|
|
13451
14279
|
const doc = new PDFDocument({ margin: 50 });
|
|
13452
|
-
const stream =
|
|
14280
|
+
const stream = fs30.createWriteStream(filePath);
|
|
13453
14281
|
doc.pipe(stream);
|
|
13454
14282
|
if (title) {
|
|
13455
14283
|
doc.info["Title"] = title;
|
|
@@ -13527,22 +14355,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
13527
14355
|
}
|
|
13528
14356
|
cmdPrefix = NODE_CMD;
|
|
13529
14357
|
}
|
|
13530
|
-
const tmpDir =
|
|
13531
|
-
if (!
|
|
13532
|
-
|
|
14358
|
+
const tmpDir = path33.join(this.workspaceRoot, ".cascade", "tmp");
|
|
14359
|
+
if (!fs30.existsSync(tmpDir)) {
|
|
14360
|
+
fs30.mkdirSync(tmpDir, { recursive: true });
|
|
13533
14361
|
}
|
|
13534
14362
|
const extension = language === "python" ? "py" : "js";
|
|
13535
14363
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
13536
|
-
const filePath =
|
|
13537
|
-
|
|
14364
|
+
const filePath = path33.join(tmpDir, fileName);
|
|
14365
|
+
fs30.writeFileSync(filePath, code, "utf-8");
|
|
13538
14366
|
const execArgs = [filePath, ...args];
|
|
13539
14367
|
return new Promise((resolve2) => {
|
|
13540
14368
|
const startMs = Date.now();
|
|
13541
14369
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
13542
14370
|
const duration = Date.now() - startMs;
|
|
13543
14371
|
try {
|
|
13544
|
-
if (
|
|
13545
|
-
|
|
14372
|
+
if (fs30.existsSync(filePath)) {
|
|
14373
|
+
fs30.unlinkSync(filePath);
|
|
13546
14374
|
}
|
|
13547
14375
|
} catch (cleanupErr) {
|
|
13548
14376
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -13854,7 +14682,7 @@ var GlobTool = class extends BaseTool {
|
|
|
13854
14682
|
};
|
|
13855
14683
|
async execute(input, _options) {
|
|
13856
14684
|
const pattern = input["pattern"];
|
|
13857
|
-
const searchPath = input["path"] ?
|
|
14685
|
+
const searchPath = input["path"] ? path33.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
13858
14686
|
const matches = await glob(pattern, {
|
|
13859
14687
|
cwd: searchPath,
|
|
13860
14688
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -13867,7 +14695,7 @@ var GlobTool = class extends BaseTool {
|
|
|
13867
14695
|
const withMtime = await Promise.all(
|
|
13868
14696
|
matches.map(async (rel) => {
|
|
13869
14697
|
try {
|
|
13870
|
-
const stat = await
|
|
14698
|
+
const stat = await fs11.stat(path33.join(searchPath, rel));
|
|
13871
14699
|
return { rel, mtime: stat.mtimeMs };
|
|
13872
14700
|
} catch {
|
|
13873
14701
|
return { rel, mtime: 0 };
|
|
@@ -13916,7 +14744,7 @@ var GrepTool = class extends BaseTool {
|
|
|
13916
14744
|
};
|
|
13917
14745
|
async execute(input, _options) {
|
|
13918
14746
|
const pattern = input["pattern"];
|
|
13919
|
-
const searchPath = input["path"] ?
|
|
14747
|
+
const searchPath = input["path"] ? path33.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
13920
14748
|
const globPattern = input["glob"];
|
|
13921
14749
|
const outputMode = input["output_mode"] ?? "content";
|
|
13922
14750
|
const context = input["context"] ?? 0;
|
|
@@ -13970,15 +14798,15 @@ var GrepTool = class extends BaseTool {
|
|
|
13970
14798
|
nodir: true
|
|
13971
14799
|
});
|
|
13972
14800
|
} catch {
|
|
13973
|
-
files = [
|
|
14801
|
+
files = [path33.relative(searchPath, searchPath) || "."];
|
|
13974
14802
|
}
|
|
13975
14803
|
const results = [];
|
|
13976
14804
|
let totalCount = 0;
|
|
13977
14805
|
for (const rel of files) {
|
|
13978
|
-
const abs =
|
|
14806
|
+
const abs = path33.join(searchPath, rel);
|
|
13979
14807
|
let content;
|
|
13980
14808
|
try {
|
|
13981
|
-
content = await
|
|
14809
|
+
content = await fs11.readFile(abs, "utf-8");
|
|
13982
14810
|
} catch {
|
|
13983
14811
|
continue;
|
|
13984
14812
|
}
|
|
@@ -14362,10 +15190,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
14362
15190
|
}
|
|
14363
15191
|
isIgnored(filePath) {
|
|
14364
15192
|
if (!filePath) return false;
|
|
14365
|
-
const abs =
|
|
14366
|
-
const rel =
|
|
14367
|
-
if (!rel || rel.startsWith("..") ||
|
|
14368
|
-
const posixRel = rel.split(
|
|
15193
|
+
const abs = path33.resolve(this.workspaceRoot, filePath);
|
|
15194
|
+
const rel = path33.relative(this.workspaceRoot, abs);
|
|
15195
|
+
if (!rel || rel.startsWith("..") || path33.isAbsolute(rel)) return true;
|
|
15196
|
+
const posixRel = rel.split(path33.sep).join("/");
|
|
14369
15197
|
return this.ignoreMatcher.ignores(posixRel);
|
|
14370
15198
|
}
|
|
14371
15199
|
};
|
|
@@ -14599,22 +15427,22 @@ var FileMcpOAuthStore = class {
|
|
|
14599
15427
|
filePath;
|
|
14600
15428
|
load() {
|
|
14601
15429
|
try {
|
|
14602
|
-
return JSON.parse(
|
|
15430
|
+
return JSON.parse(fs30.readFileSync(this.filePath, "utf-8"));
|
|
14603
15431
|
} catch {
|
|
14604
15432
|
return void 0;
|
|
14605
15433
|
}
|
|
14606
15434
|
}
|
|
14607
15435
|
save(state) {
|
|
14608
|
-
|
|
14609
|
-
|
|
15436
|
+
fs30.mkdirSync(path33.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
15437
|
+
fs30.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
14610
15438
|
try {
|
|
14611
|
-
|
|
15439
|
+
fs30.chmodSync(this.filePath, 384);
|
|
14612
15440
|
} catch {
|
|
14613
15441
|
}
|
|
14614
15442
|
}
|
|
14615
15443
|
clear() {
|
|
14616
15444
|
try {
|
|
14617
|
-
|
|
15445
|
+
fs30.rmSync(this.filePath);
|
|
14618
15446
|
} catch {
|
|
14619
15447
|
}
|
|
14620
15448
|
}
|
|
@@ -15222,7 +16050,7 @@ var TaskAnalyzer = class {
|
|
|
15222
16050
|
|
|
15223
16051
|
// src/core/router/model-performance-tracker.ts
|
|
15224
16052
|
init_pricing();
|
|
15225
|
-
var DEFAULT_STATS_FILE =
|
|
16053
|
+
var DEFAULT_STATS_FILE = path33.join(os10.homedir(), ".cascade", "model-perf.json");
|
|
15226
16054
|
var ModelPerformanceTracker = class {
|
|
15227
16055
|
stats = /* @__PURE__ */ new Map();
|
|
15228
16056
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -15242,7 +16070,7 @@ var ModelPerformanceTracker = class {
|
|
|
15242
16070
|
if (this.loaded) return;
|
|
15243
16071
|
this.loaded = true;
|
|
15244
16072
|
try {
|
|
15245
|
-
const raw = await
|
|
16073
|
+
const raw = await fs11.readFile(this.statsFile, "utf-8");
|
|
15246
16074
|
const parsed = JSON.parse(raw);
|
|
15247
16075
|
if (parsed.models) {
|
|
15248
16076
|
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
@@ -15262,15 +16090,15 @@ var ModelPerformanceTracker = class {
|
|
|
15262
16090
|
async save() {
|
|
15263
16091
|
if (this.readOnly) return;
|
|
15264
16092
|
try {
|
|
15265
|
-
await
|
|
16093
|
+
await fs11.mkdir(path33.dirname(this.statsFile), { recursive: true });
|
|
15266
16094
|
const modelsObj = {};
|
|
15267
16095
|
const featuresObj = {};
|
|
15268
16096
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
15269
16097
|
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
15270
16098
|
const json = JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2);
|
|
15271
16099
|
const tmp = `${this.statsFile}.${process.pid}.${Date.now()}.tmp`;
|
|
15272
|
-
await
|
|
15273
|
-
await
|
|
16100
|
+
await fs11.writeFile(tmp, json, "utf-8");
|
|
16101
|
+
await fs11.rename(tmp, this.statsFile);
|
|
15274
16102
|
} catch {
|
|
15275
16103
|
}
|
|
15276
16104
|
}
|
|
@@ -15810,9 +16638,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
15810
16638
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
15811
16639
|
async loadPersistedTools() {
|
|
15812
16640
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
15813
|
-
const file =
|
|
16641
|
+
const file = path33.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
15814
16642
|
try {
|
|
15815
|
-
const raw = await
|
|
16643
|
+
const raw = await fs11.readFile(file, "utf-8");
|
|
15816
16644
|
const specs = JSON.parse(raw);
|
|
15817
16645
|
if (!Array.isArray(specs)) return;
|
|
15818
16646
|
let loaded = 0;
|
|
@@ -15834,11 +16662,11 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
15834
16662
|
}
|
|
15835
16663
|
async persist() {
|
|
15836
16664
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
15837
|
-
const dir =
|
|
15838
|
-
const file =
|
|
16665
|
+
const dir = path33.join(this.workspacePath, ".cascade");
|
|
16666
|
+
const file = path33.join(dir, DYNAMIC_TOOLS_FILE);
|
|
15839
16667
|
try {
|
|
15840
|
-
await
|
|
15841
|
-
await
|
|
16668
|
+
await fs11.mkdir(dir, { recursive: true });
|
|
16669
|
+
await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
15842
16670
|
} catch (err) {
|
|
15843
16671
|
this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
15844
16672
|
}
|
|
@@ -15855,12 +16683,12 @@ var WorldStateDB = class {
|
|
|
15855
16683
|
constructor(workspacePath, debugMode = false) {
|
|
15856
16684
|
this.workspacePath = workspacePath;
|
|
15857
16685
|
this.debugMode = debugMode;
|
|
15858
|
-
const cascadeDir =
|
|
15859
|
-
if (!
|
|
15860
|
-
|
|
16686
|
+
const cascadeDir = path33.join(workspacePath, ".cascade");
|
|
16687
|
+
if (!fs30.existsSync(cascadeDir)) {
|
|
16688
|
+
fs30.mkdirSync(cascadeDir, { recursive: true });
|
|
15861
16689
|
}
|
|
15862
|
-
this.keyPath =
|
|
15863
|
-
this.dbPath =
|
|
16690
|
+
this.keyPath = path33.join(cascadeDir, "world_state.key");
|
|
16691
|
+
this.dbPath = path33.join(cascadeDir, "world_state.db");
|
|
15864
16692
|
this.initEncryptionKey();
|
|
15865
16693
|
this.db = new Database2(this.dbPath);
|
|
15866
16694
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -15917,11 +16745,11 @@ var WorldStateDB = class {
|
|
|
15917
16745
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
15918
16746
|
}
|
|
15919
16747
|
initEncryptionKey() {
|
|
15920
|
-
if (
|
|
15921
|
-
this.encryptionKey =
|
|
16748
|
+
if (fs30.existsSync(this.keyPath)) {
|
|
16749
|
+
this.encryptionKey = fs30.readFileSync(this.keyPath);
|
|
15922
16750
|
} else {
|
|
15923
16751
|
this.encryptionKey = crypto.randomBytes(32);
|
|
15924
|
-
|
|
16752
|
+
fs30.writeFileSync(this.keyPath, this.encryptionKey);
|
|
15925
16753
|
}
|
|
15926
16754
|
}
|
|
15927
16755
|
encrypt(text) {
|
|
@@ -16207,9 +17035,9 @@ var WorldStateDB = class {
|
|
|
16207
17035
|
dumpDebugIfNeeded() {
|
|
16208
17036
|
if (!this.debugMode) return;
|
|
16209
17037
|
try {
|
|
16210
|
-
const dumpPath =
|
|
17038
|
+
const dumpPath = path33.join(os10.tmpdir(), "cascade_world_state_debug.json");
|
|
16211
17039
|
const entries = this.getAllEntries();
|
|
16212
|
-
|
|
17040
|
+
fs30.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
16213
17041
|
} catch (err) {
|
|
16214
17042
|
console.error("Failed to dump debug world state", err);
|
|
16215
17043
|
}
|
|
@@ -16579,7 +17407,7 @@ var WorkspaceIndex = class {
|
|
|
16579
17407
|
isIgnored;
|
|
16580
17408
|
chunker;
|
|
16581
17409
|
constructor(opts) {
|
|
16582
|
-
this.root =
|
|
17410
|
+
this.root = path33.resolve(opts.root);
|
|
16583
17411
|
this.db = opts.db;
|
|
16584
17412
|
this.namespace = opts.namespace ?? "code";
|
|
16585
17413
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -16606,19 +17434,19 @@ var WorkspaceIndex = class {
|
|
|
16606
17434
|
const walk = async (dir) => {
|
|
16607
17435
|
let entries;
|
|
16608
17436
|
try {
|
|
16609
|
-
entries = await
|
|
17437
|
+
entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
16610
17438
|
} catch {
|
|
16611
17439
|
return;
|
|
16612
17440
|
}
|
|
16613
17441
|
for (const entry of entries) {
|
|
16614
|
-
const abs =
|
|
17442
|
+
const abs = path33.join(dir, entry.name);
|
|
16615
17443
|
if (entry.isDirectory()) {
|
|
16616
17444
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
16617
17445
|
await walk(abs);
|
|
16618
17446
|
} else if (entry.isFile()) {
|
|
16619
|
-
const ext =
|
|
17447
|
+
const ext = path33.extname(entry.name).slice(1).toLowerCase();
|
|
16620
17448
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
16621
|
-
out.push(
|
|
17449
|
+
out.push(path33.relative(this.root, abs));
|
|
16622
17450
|
}
|
|
16623
17451
|
}
|
|
16624
17452
|
};
|
|
@@ -16648,10 +17476,10 @@ var WorkspaceIndex = class {
|
|
|
16648
17476
|
const entries = [];
|
|
16649
17477
|
for (const rel of rels) {
|
|
16650
17478
|
try {
|
|
16651
|
-
const abs =
|
|
16652
|
-
const stat = await
|
|
17479
|
+
const abs = path33.join(this.root, rel);
|
|
17480
|
+
const stat = await fs11.stat(abs);
|
|
16653
17481
|
if (stat.size > this.maxFileBytes) continue;
|
|
16654
|
-
const buf = await
|
|
17482
|
+
const buf = await fs11.readFile(abs);
|
|
16655
17483
|
if (buf.includes(0)) continue;
|
|
16656
17484
|
entries.push({ path: rel, hash: hashContent(buf), text: buf.toString("utf8") });
|
|
16657
17485
|
} catch {
|
|
@@ -17220,7 +18048,7 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
17220
18048
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
17221
18049
|
try {
|
|
17222
18050
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
17223
|
-
fileDeadModelPersistence(
|
|
18051
|
+
fileDeadModelPersistence(path33.join(workspacePath, ".cascade", "dead-models.json"))
|
|
17224
18052
|
));
|
|
17225
18053
|
} catch {
|
|
17226
18054
|
}
|
|
@@ -17228,6 +18056,7 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
17228
18056
|
(this.config.providers ?? []).map((p) => p.type)
|
|
17229
18057
|
);
|
|
17230
18058
|
this.registerMediaTools(workspacePath);
|
|
18059
|
+
this.registerDocumentTools(workspacePath);
|
|
17231
18060
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
17232
18061
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
17233
18062
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -17307,8 +18136,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
17307
18136
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
17308
18137
|
return;
|
|
17309
18138
|
}
|
|
17310
|
-
const dbPath = ci.dbPath ||
|
|
17311
|
-
|
|
18139
|
+
const dbPath = ci.dbPath || path33.join(this.workspacePath, ".cascade", "code-index.db");
|
|
18140
|
+
fs30.mkdirSync(path33.dirname(dbPath), { recursive: true });
|
|
17312
18141
|
const db = new Database2(dbPath);
|
|
17313
18142
|
db.pragma("journal_mode = WAL");
|
|
17314
18143
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -17648,6 +18477,35 @@ ${input}` }],
|
|
|
17648
18477
|
this.toolRegistry.register(tool);
|
|
17649
18478
|
}
|
|
17650
18479
|
}
|
|
18480
|
+
/**
|
|
18481
|
+
* Register `generate_document` where there is a real workspace to write into.
|
|
18482
|
+
*
|
|
18483
|
+
* A `.docx`/`.pptx`/`.xlsx` is a ZIP of OOXML parts, so `file_write` — which
|
|
18484
|
+
* does exactly what it says and writes the model's text verbatim — produced
|
|
18485
|
+
* files Word/Excel/PowerPoint correctly reported as corrupted. This tool owns
|
|
18486
|
+
* the conversion (see tools/generate-document.ts for why it isn't folded into
|
|
18487
|
+
* file_write).
|
|
18488
|
+
*
|
|
18489
|
+
* Registered OUTSIDE `enabledTools` for the same reason the media tools are:
|
|
18490
|
+
* that allowlist is a blast-radius control for tools that reach the machine
|
|
18491
|
+
* or the network, and this one only writes the artifact the user asked for,
|
|
18492
|
+
* into the workspace the run already owns. `disabledTools` still removes it.
|
|
18493
|
+
* A host with no filesystem (the cloud) has its own browser-side exporter and
|
|
18494
|
+
* simply never reaches this path — `buildDocumentTools` returns nothing
|
|
18495
|
+
* without a file reader, mirroring `transcribe_audio`.
|
|
18496
|
+
*/
|
|
18497
|
+
registerDocumentTools(workspacePath) {
|
|
18498
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
18499
|
+
const tools = buildDocumentTools(async (p) => {
|
|
18500
|
+
const fsp = await import('fs/promises');
|
|
18501
|
+
return new Uint8Array(await fsp.readFile(p));
|
|
18502
|
+
});
|
|
18503
|
+
for (const tool of tools) {
|
|
18504
|
+
if (denied.has(tool.name)) continue;
|
|
18505
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
18506
|
+
this.toolRegistry.register(tool);
|
|
18507
|
+
}
|
|
18508
|
+
}
|
|
17651
18509
|
/**
|
|
17652
18510
|
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
17653
18511
|
* have actually performed for this user, on top of public benchmarks.
|
|
@@ -18560,14 +19418,14 @@ ${finalOutput}` : ""
|
|
|
18560
19418
|
init_constants();
|
|
18561
19419
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
18562
19420
|
function globalDir(homeDir = os10.homedir()) {
|
|
18563
|
-
return
|
|
19421
|
+
return path33.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
18564
19422
|
}
|
|
18565
19423
|
function cloudSessionPath(dir = globalDir()) {
|
|
18566
|
-
return
|
|
19424
|
+
return path33.join(dir, CLOUD_SESSION_FILE);
|
|
18567
19425
|
}
|
|
18568
19426
|
function loadCloudSession(dir = globalDir()) {
|
|
18569
19427
|
try {
|
|
18570
|
-
const raw =
|
|
19428
|
+
const raw = fs30.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
18571
19429
|
const parsed = JSON.parse(raw);
|
|
18572
19430
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
18573
19431
|
return parsed;
|
|
@@ -18576,17 +19434,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
18576
19434
|
}
|
|
18577
19435
|
}
|
|
18578
19436
|
function saveCloudSession(session, dir = globalDir()) {
|
|
18579
|
-
|
|
19437
|
+
fs30.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
18580
19438
|
const file = cloudSessionPath(dir);
|
|
18581
|
-
|
|
19439
|
+
fs30.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
18582
19440
|
try {
|
|
18583
|
-
|
|
19441
|
+
fs30.chmodSync(file, 384);
|
|
18584
19442
|
} catch {
|
|
18585
19443
|
}
|
|
18586
19444
|
}
|
|
18587
19445
|
function clearCloudSession(dir = globalDir()) {
|
|
18588
19446
|
try {
|
|
18589
|
-
|
|
19447
|
+
fs30.rmSync(cloudSessionPath(dir));
|
|
18590
19448
|
} catch {
|
|
18591
19449
|
}
|
|
18592
19450
|
}
|
|
@@ -18758,16 +19616,16 @@ var CloudClient = class _CloudClient {
|
|
|
18758
19616
|
return (await this.refresh()).accessToken;
|
|
18759
19617
|
}
|
|
18760
19618
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
18761
|
-
async authed(
|
|
18762
|
-
const call = (token) => fetch(this.url(
|
|
19619
|
+
async authed(path38, init = {}) {
|
|
19620
|
+
const call = (token) => fetch(this.url(path38), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
18763
19621
|
let res = await call(await this.accessToken());
|
|
18764
19622
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
18765
19623
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
18766
19624
|
const text = await res.text();
|
|
18767
19625
|
return text ? JSON.parse(text) : {};
|
|
18768
19626
|
}
|
|
18769
|
-
authedGet(
|
|
18770
|
-
return this.authed(
|
|
19627
|
+
authedGet(path38) {
|
|
19628
|
+
return this.authed(path38);
|
|
18771
19629
|
}
|
|
18772
19630
|
async fetchMe(token) {
|
|
18773
19631
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -20542,10 +21400,10 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20542
21400
|
}
|
|
20543
21401
|
} catch {
|
|
20544
21402
|
}
|
|
20545
|
-
const configPath =
|
|
21403
|
+
const configPath = path33.join(workspacePath, ".cascade", "config.json");
|
|
20546
21404
|
try {
|
|
20547
|
-
await
|
|
20548
|
-
await
|
|
21405
|
+
await fs11.mkdir(path33.dirname(configPath), { recursive: true });
|
|
21406
|
+
await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
20549
21407
|
} catch (err) {
|
|
20550
21408
|
const msg = err instanceof Error ? err.message : String(err);
|
|
20551
21409
|
dispatch({
|
|
@@ -20596,9 +21454,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20596
21454
|
if (msg.includes("non-text parts")) return;
|
|
20597
21455
|
originalLog(...args);
|
|
20598
21456
|
};
|
|
20599
|
-
const store = new MemoryStore(
|
|
21457
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
20600
21458
|
storeRef.current = store;
|
|
20601
|
-
globalStoreRef.current = new MemoryStore(
|
|
21459
|
+
globalStoreRef.current = new MemoryStore(path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
20602
21460
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
20603
21461
|
setIdentities(identityRows);
|
|
20604
21462
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -20666,14 +21524,14 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20666
21524
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
20667
21525
|
onExport: async (fmt) => {
|
|
20668
21526
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20669
|
-
const exportPath =
|
|
21527
|
+
const exportPath = path33.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
20670
21528
|
if (fmt === "json") {
|
|
20671
|
-
await
|
|
21529
|
+
await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
20672
21530
|
} else {
|
|
20673
21531
|
const markdown = state.messages.map((msg) => `## ${msg.role.toUpperCase()} \u2014 ${msg.timestamp}
|
|
20674
21532
|
|
|
20675
21533
|
${msg.content}`).join("\n\n");
|
|
20676
|
-
await
|
|
21534
|
+
await fs11.writeFile(exportPath, markdown, "utf-8");
|
|
20677
21535
|
}
|
|
20678
21536
|
},
|
|
20679
21537
|
onRollback: async () => {
|
|
@@ -20683,7 +21541,7 @@ ${msg.content}`).join("\n\n");
|
|
|
20683
21541
|
if (!snapshots.length) return "No file snapshots found for this session.";
|
|
20684
21542
|
for (const { filePath, content } of snapshots) {
|
|
20685
21543
|
try {
|
|
20686
|
-
await
|
|
21544
|
+
await fs11.writeFile(filePath, content, "utf-8");
|
|
20687
21545
|
} catch (err) {
|
|
20688
21546
|
console.error(`Restore failed: ${filePath}`, err);
|
|
20689
21547
|
}
|
|
@@ -21564,9 +22422,9 @@ function stringifySlashOutput(val) {
|
|
|
21564
22422
|
}
|
|
21565
22423
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
21566
22424
|
if (!query) return "Usage: /search <query>";
|
|
21567
|
-
const dbPath =
|
|
22425
|
+
const dbPath = path33.join(workspacePath, CASCADE_DB_FILE);
|
|
21568
22426
|
try {
|
|
21569
|
-
await
|
|
22427
|
+
await fs11.access(dbPath);
|
|
21570
22428
|
} catch {
|
|
21571
22429
|
return "No database found. Start a conversation first.";
|
|
21572
22430
|
}
|
|
@@ -21598,7 +22456,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
21598
22456
|
async function diagnoseRuntime(config, workspacePath) {
|
|
21599
22457
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
21600
22458
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
21601
|
-
const store = new MemoryStore(
|
|
22459
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
21602
22460
|
try {
|
|
21603
22461
|
const sessions2 = store.listSessions(void 0, 3);
|
|
21604
22462
|
return [
|
|
@@ -21617,7 +22475,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
21617
22475
|
}
|
|
21618
22476
|
async function showRecentLogs(args, workspacePath) {
|
|
21619
22477
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
21620
|
-
const store = new MemoryStore(
|
|
22478
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
21621
22479
|
try {
|
|
21622
22480
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
21623
22481
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -21629,7 +22487,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
21629
22487
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
21630
22488
|
const sessionId = args[0];
|
|
21631
22489
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
21632
|
-
const store = new MemoryStore(
|
|
22490
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
21633
22491
|
try {
|
|
21634
22492
|
const session = store.getSession(sessionId);
|
|
21635
22493
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -22057,10 +22915,10 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
22057
22915
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
22058
22916
|
};
|
|
22059
22917
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
22060
|
-
const configDir =
|
|
22061
|
-
await
|
|
22062
|
-
const configPath =
|
|
22063
|
-
await
|
|
22918
|
+
const configDir = path33.join(workspacePath, ".cascade");
|
|
22919
|
+
await fs11.mkdir(configDir, { recursive: true });
|
|
22920
|
+
const configPath = path33.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22921
|
+
await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
22064
22922
|
savedConfigRef.current = config;
|
|
22065
22923
|
dispatchRef.current({ type: "GO_DONE" });
|
|
22066
22924
|
} catch (err) {
|
|
@@ -22401,14 +23259,14 @@ function printTelemetryBanner() {
|
|
|
22401
23259
|
async function initCommand(workspacePath = process.cwd()) {
|
|
22402
23260
|
const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
22403
23261
|
try {
|
|
22404
|
-
const configDir =
|
|
22405
|
-
await
|
|
22406
|
-
const mdPath =
|
|
23262
|
+
const configDir = path33.join(workspacePath, ".cascade");
|
|
23263
|
+
await fs11.mkdir(configDir, { recursive: true });
|
|
23264
|
+
const mdPath = path33.join(workspacePath, "CASCADE.md");
|
|
22407
23265
|
if (!await fileExists(mdPath)) {
|
|
22408
23266
|
await createDefaultCascadeMd(workspacePath);
|
|
22409
23267
|
spin.succeed(chalk7.green("Created CASCADE.md"));
|
|
22410
23268
|
}
|
|
22411
|
-
const ignorePath =
|
|
23269
|
+
const ignorePath = path33.join(workspacePath, ".cascadeignore");
|
|
22412
23270
|
if (!await fileExists(ignorePath)) {
|
|
22413
23271
|
await createDefaultIgnoreFile(workspacePath);
|
|
22414
23272
|
spin.succeed(chalk7.green("Created .cascadeignore"));
|
|
@@ -22417,7 +23275,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
22417
23275
|
console.log();
|
|
22418
23276
|
console.log(chalk7.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
22419
23277
|
console.log();
|
|
22420
|
-
const configPath =
|
|
23278
|
+
const configPath = path33.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22421
23279
|
if (await fileExists(configPath)) {
|
|
22422
23280
|
console.log(chalk7.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
22423
23281
|
console.log();
|
|
@@ -22436,7 +23294,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
22436
23294
|
}
|
|
22437
23295
|
async function fileExists(p) {
|
|
22438
23296
|
try {
|
|
22439
|
-
await
|
|
23297
|
+
await fs11.access(p);
|
|
22440
23298
|
return true;
|
|
22441
23299
|
} catch {
|
|
22442
23300
|
return false;
|
|
@@ -22448,7 +23306,7 @@ init_constants();
|
|
|
22448
23306
|
var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
|
|
22449
23307
|
async function readJson(file) {
|
|
22450
23308
|
try {
|
|
22451
|
-
const raw = await
|
|
23309
|
+
const raw = await fs11.readFile(file, "utf-8");
|
|
22452
23310
|
return JSON.parse(raw);
|
|
22453
23311
|
} catch {
|
|
22454
23312
|
return null;
|
|
@@ -22475,7 +23333,7 @@ function fromEnv(env) {
|
|
|
22475
23333
|
return out;
|
|
22476
23334
|
}
|
|
22477
23335
|
async function fromClaudeCode(home) {
|
|
22478
|
-
const file =
|
|
23336
|
+
const file = path33.join(home, ".claude", ".credentials.json");
|
|
22479
23337
|
const data = await readJson(file);
|
|
22480
23338
|
if (!data) return [];
|
|
22481
23339
|
const oauth = data["claudeAiOauth"];
|
|
@@ -22498,7 +23356,7 @@ async function fromClaudeCode(home) {
|
|
|
22498
23356
|
return [];
|
|
22499
23357
|
}
|
|
22500
23358
|
async function fromCodex(home) {
|
|
22501
|
-
const file =
|
|
23359
|
+
const file = path33.join(home, ".codex", "auth.json");
|
|
22502
23360
|
const data = await readJson(file);
|
|
22503
23361
|
if (!data) return [];
|
|
22504
23362
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -22521,7 +23379,7 @@ async function fromCodex(home) {
|
|
|
22521
23379
|
return [];
|
|
22522
23380
|
}
|
|
22523
23381
|
async function fromGemini(home) {
|
|
22524
|
-
const file =
|
|
23382
|
+
const file = path33.join(home, ".gemini", "oauth_creds.json");
|
|
22525
23383
|
const data = await readJson(file);
|
|
22526
23384
|
const accessToken = str(data?.["access_token"]);
|
|
22527
23385
|
if (!accessToken) return [];
|
|
@@ -22537,7 +23395,7 @@ async function fromGemini(home) {
|
|
|
22537
23395
|
}
|
|
22538
23396
|
async function fromCopilot(home) {
|
|
22539
23397
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
22540
|
-
const file =
|
|
23398
|
+
const file = path33.join(home, ".config", "github-copilot", name);
|
|
22541
23399
|
const data = await readJson(file);
|
|
22542
23400
|
if (!data) continue;
|
|
22543
23401
|
for (const value of Object.values(data)) {
|
|
@@ -22591,7 +23449,7 @@ async function doctorCommand() {
|
|
|
22591
23449
|
checks.push({
|
|
22592
23450
|
label: "Cascade config",
|
|
22593
23451
|
ok: true,
|
|
22594
|
-
detail: `Loaded ${
|
|
23452
|
+
detail: `Loaded ${path33.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
22595
23453
|
});
|
|
22596
23454
|
const providers = [
|
|
22597
23455
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -22702,7 +23560,7 @@ async function checkEndpoint(url) {
|
|
|
22702
23560
|
}
|
|
22703
23561
|
}
|
|
22704
23562
|
async function indexCommand(dirPath) {
|
|
22705
|
-
const workspace =
|
|
23563
|
+
const workspace = path33.resolve(dirPath || process.cwd());
|
|
22706
23564
|
const cm = new ConfigManager(workspace);
|
|
22707
23565
|
await cm.load();
|
|
22708
23566
|
const config = cm.getConfig();
|
|
@@ -22713,8 +23571,8 @@ async function indexCommand(dirPath) {
|
|
|
22713
23571
|
process.exitCode = 1;
|
|
22714
23572
|
return;
|
|
22715
23573
|
}
|
|
22716
|
-
const dbPath = config.codeIndex?.dbPath ||
|
|
22717
|
-
|
|
23574
|
+
const dbPath = config.codeIndex?.dbPath || path33.join(workspace, ".cascade", "code-index.db");
|
|
23575
|
+
fs30.mkdirSync(path33.dirname(dbPath), { recursive: true });
|
|
22718
23576
|
const db = new Database2(dbPath);
|
|
22719
23577
|
db.pragma("journal_mode = WAL");
|
|
22720
23578
|
const complete = chatCompleterFromProviders(config.providers);
|
|
@@ -23203,7 +24061,7 @@ function openBrowser2(url) {
|
|
|
23203
24061
|
}
|
|
23204
24062
|
function storePathFor(name) {
|
|
23205
24063
|
const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
|
|
23206
|
-
return
|
|
24064
|
+
return path33.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
|
|
23207
24065
|
}
|
|
23208
24066
|
function defaultName(url) {
|
|
23209
24067
|
try {
|
|
@@ -23705,7 +24563,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
23705
24563
|
}
|
|
23706
24564
|
|
|
23707
24565
|
// src/dashboard/server.ts
|
|
23708
|
-
var __dirname$1 =
|
|
24566
|
+
var __dirname$1 = path33.dirname(fileURLToPath(import.meta.url));
|
|
23709
24567
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
23710
24568
|
var DashboardServer = class {
|
|
23711
24569
|
app;
|
|
@@ -23985,14 +24843,14 @@ var DashboardServer = class {
|
|
|
23985
24843
|
*/
|
|
23986
24844
|
persistConfig() {
|
|
23987
24845
|
try {
|
|
23988
|
-
const configPath =
|
|
23989
|
-
|
|
23990
|
-
|
|
24846
|
+
const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
24847
|
+
fs30.mkdirSync(path33.dirname(configPath), { recursive: true });
|
|
24848
|
+
fs30.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
23991
24849
|
} catch (err) {
|
|
23992
24850
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
23993
24851
|
}
|
|
23994
24852
|
try {
|
|
23995
|
-
saveGlobalCredentials(
|
|
24853
|
+
saveGlobalCredentials(path33.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
23996
24854
|
} catch (err) {
|
|
23997
24855
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
23998
24856
|
}
|
|
@@ -24007,15 +24865,15 @@ var DashboardServer = class {
|
|
|
24007
24865
|
resolveDashboardSecret() {
|
|
24008
24866
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
24009
24867
|
if (fromConfig) return fromConfig;
|
|
24010
|
-
const secretPath =
|
|
24868
|
+
const secretPath = path33.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
24011
24869
|
try {
|
|
24012
|
-
if (
|
|
24013
|
-
const existing =
|
|
24870
|
+
if (fs30.existsSync(secretPath)) {
|
|
24871
|
+
const existing = fs30.readFileSync(secretPath, "utf-8").trim();
|
|
24014
24872
|
if (existing.length >= 16) return existing;
|
|
24015
24873
|
}
|
|
24016
24874
|
const generated = randomUUID();
|
|
24017
|
-
|
|
24018
|
-
|
|
24875
|
+
fs30.mkdirSync(path33.dirname(secretPath), { recursive: true });
|
|
24876
|
+
fs30.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
24019
24877
|
if (this.config.dashboard.auth) {
|
|
24020
24878
|
console.warn(
|
|
24021
24879
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -24042,7 +24900,7 @@ var DashboardServer = class {
|
|
|
24042
24900
|
// ── Setup ─────────────────────────────────────
|
|
24043
24901
|
getGlobalStore() {
|
|
24044
24902
|
if (!this.globalStore) {
|
|
24045
|
-
const globalDbPath =
|
|
24903
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24046
24904
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
24047
24905
|
}
|
|
24048
24906
|
return this.globalStore;
|
|
@@ -24422,12 +25280,12 @@ ${prompt}`;
|
|
|
24422
25280
|
}
|
|
24423
25281
|
}
|
|
24424
25282
|
watchRuntimeChanges() {
|
|
24425
|
-
const workspaceDbPath =
|
|
24426
|
-
const globalDbPath =
|
|
25283
|
+
const workspaceDbPath = path33.join(this.workspacePath, CASCADE_DB_FILE);
|
|
25284
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24427
25285
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
24428
25286
|
for (const watchPath of watchPaths) {
|
|
24429
|
-
if (!
|
|
24430
|
-
|
|
25287
|
+
if (!fs30.existsSync(watchPath)) continue;
|
|
25288
|
+
fs30.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
24431
25289
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
24432
25290
|
});
|
|
24433
25291
|
}
|
|
@@ -24549,7 +25407,7 @@ ${prompt}`;
|
|
|
24549
25407
|
const sessionId = req.params.id;
|
|
24550
25408
|
this.store.deleteSession(sessionId);
|
|
24551
25409
|
this.store.deleteRuntimeSession(sessionId);
|
|
24552
|
-
const globalDbPath =
|
|
25410
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24553
25411
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24554
25412
|
try {
|
|
24555
25413
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -24642,7 +25500,7 @@ ${prompt}`;
|
|
|
24642
25500
|
});
|
|
24643
25501
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
24644
25502
|
const body = req.body;
|
|
24645
|
-
const globalDbPath =
|
|
25503
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24646
25504
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
24647
25505
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24648
25506
|
try {
|
|
@@ -24665,7 +25523,7 @@ ${prompt}`;
|
|
|
24665
25523
|
});
|
|
24666
25524
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
24667
25525
|
this.store.deleteAllRuntimeNodes();
|
|
24668
|
-
const globalDbPath =
|
|
25526
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24669
25527
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24670
25528
|
try {
|
|
24671
25529
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -24751,12 +25609,12 @@ ${prompt}`;
|
|
|
24751
25609
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
24752
25610
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
24753
25611
|
try {
|
|
24754
|
-
const configPath =
|
|
24755
|
-
const existing =
|
|
25612
|
+
const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
25613
|
+
const existing = fs30.existsSync(configPath) ? JSON.parse(fs30.readFileSync(configPath, "utf-8")) : {};
|
|
24756
25614
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
24757
25615
|
const tmp = configPath + ".tmp";
|
|
24758
|
-
|
|
24759
|
-
|
|
25616
|
+
fs30.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
25617
|
+
fs30.renameSync(tmp, configPath);
|
|
24760
25618
|
res.json({ ok: true });
|
|
24761
25619
|
} catch (err) {
|
|
24762
25620
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -24784,7 +25642,7 @@ ${prompt}`;
|
|
|
24784
25642
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
24785
25643
|
const scope = req.query["scope"] ?? "workspace";
|
|
24786
25644
|
if (scope === "global") {
|
|
24787
|
-
const globalDbPath =
|
|
25645
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24788
25646
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24789
25647
|
try {
|
|
24790
25648
|
res.json({
|
|
@@ -25125,13 +25983,13 @@ ${prompt}`;
|
|
|
25125
25983
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
25126
25984
|
}
|
|
25127
25985
|
});
|
|
25128
|
-
const prodPath =
|
|
25129
|
-
const devPath =
|
|
25130
|
-
const webDistPath =
|
|
25131
|
-
if (
|
|
25986
|
+
const prodPath = path33.resolve(__dirname$1, "../web/dist");
|
|
25987
|
+
const devPath = path33.resolve(__dirname$1, "../../web/dist");
|
|
25988
|
+
const webDistPath = fs30.existsSync(prodPath) ? prodPath : devPath;
|
|
25989
|
+
if (fs30.existsSync(webDistPath)) {
|
|
25132
25990
|
this.app.use(express.static(webDistPath));
|
|
25133
25991
|
this.app.get("*", (_req, res) => {
|
|
25134
|
-
res.sendFile(
|
|
25992
|
+
res.sendFile(path33.join(webDistPath, "index.html"));
|
|
25135
25993
|
});
|
|
25136
25994
|
} else {
|
|
25137
25995
|
this.app.get("/", (_req, res) => {
|
|
@@ -25152,7 +26010,7 @@ init_constants();
|
|
|
25152
26010
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
25153
26011
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
25154
26012
|
const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
25155
|
-
const store = new MemoryStore(
|
|
26013
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
25156
26014
|
const server = new DashboardServer(config, store, workspacePath);
|
|
25157
26015
|
server.watchRuntimeChanges();
|
|
25158
26016
|
const gThis = globalThis;
|
|
@@ -25184,7 +26042,7 @@ init_constants();
|
|
|
25184
26042
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
25185
26043
|
const identity = new Command("identity").alias("id").description("Manage Cascade identities");
|
|
25186
26044
|
identity.command("list").description("List all available identities").action(() => {
|
|
25187
|
-
const store = new MemoryStore(
|
|
26045
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
25188
26046
|
try {
|
|
25189
26047
|
const identities = store.listIdentities();
|
|
25190
26048
|
console.log(chalk7.bold("\n Identities:"));
|
|
@@ -25204,7 +26062,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
25204
26062
|
}
|
|
25205
26063
|
});
|
|
25206
26064
|
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) => {
|
|
25207
|
-
const store = new MemoryStore(
|
|
26065
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
25208
26066
|
try {
|
|
25209
26067
|
if (options.default) {
|
|
25210
26068
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -25230,7 +26088,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
25230
26088
|
}
|
|
25231
26089
|
});
|
|
25232
26090
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
25233
|
-
const store = new MemoryStore(
|
|
26091
|
+
const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
|
|
25234
26092
|
try {
|
|
25235
26093
|
const identities = store.listIdentities();
|
|
25236
26094
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -25379,11 +26237,11 @@ async function exportCommand(options = {}) {
|
|
|
25379
26237
|
let store;
|
|
25380
26238
|
try {
|
|
25381
26239
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
25382
|
-
const workspaceDbPath =
|
|
25383
|
-
const globalDbPath =
|
|
26240
|
+
const workspaceDbPath = path33.join(workspacePath, CASCADE_DB_FILE);
|
|
26241
|
+
const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
25384
26242
|
let dbPath = globalDbPath;
|
|
25385
26243
|
try {
|
|
25386
|
-
await
|
|
26244
|
+
await fs11.access(workspaceDbPath);
|
|
25387
26245
|
dbPath = workspaceDbPath;
|
|
25388
26246
|
} catch {
|
|
25389
26247
|
}
|
|
@@ -25422,8 +26280,8 @@ async function exportCommand(options = {}) {
|
|
|
25422
26280
|
const ext = format === "json" ? ".json" : ".md";
|
|
25423
26281
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
25424
26282
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
25425
|
-
const outPath = options.output ?
|
|
25426
|
-
await
|
|
26283
|
+
const outPath = options.output ? path33.resolve(options.output) : path33.join(process.cwd(), defaultFile);
|
|
26284
|
+
await fs11.writeFile(outPath, content, "utf-8");
|
|
25427
26285
|
spin.succeed(chalk7.green(`Exported to ${chalk7.white(outPath)}`));
|
|
25428
26286
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
25429
26287
|
console.log();
|
|
@@ -25650,11 +26508,11 @@ async function statsCommand() {
|
|
|
25650
26508
|
dotenv.config();
|
|
25651
26509
|
function warnIfBuildIsStale() {
|
|
25652
26510
|
try {
|
|
25653
|
-
const here =
|
|
26511
|
+
const here = path33.dirname(fileURLToPath(import.meta.url));
|
|
25654
26512
|
for (const rel of ["..", "../.."]) {
|
|
25655
|
-
const pkgPath =
|
|
25656
|
-
if (!
|
|
25657
|
-
const pkg = JSON.parse(
|
|
26513
|
+
const pkgPath = path33.join(here, rel, "package.json");
|
|
26514
|
+
if (!fs30.existsSync(pkgPath)) continue;
|
|
26515
|
+
const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
|
|
25658
26516
|
if (pkg.name !== "cascade-ai") continue;
|
|
25659
26517
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
25660
26518
|
console.error(
|