cascade-ai 0.66.2 → 0.68.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 +1769 -437
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1762 -430
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +70858 -1948
- package/dist/index.cjs +1746 -378
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +564 -12
- package/dist/index.d.ts +564 -12
- package/dist/index.js +1707 -372
- 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 path34, { dirname } from 'path';
|
|
11
|
+
import fs31, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } 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.68.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 = path34.join(workspacePath, ".cascade");
|
|
3883
|
+
if (!fs31.existsSync(cascadeDir)) {
|
|
3884
|
+
fs31.mkdirSync(cascadeDir, { recursive: true });
|
|
3885
3885
|
}
|
|
3886
|
-
this.keyPath =
|
|
3887
|
-
this.dbPath =
|
|
3886
|
+
this.keyPath = path34.join(cascadeDir, "audit_log.key");
|
|
3887
|
+
this.dbPath = path34.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 (fs31.existsSync(this.keyPath)) {
|
|
3918
|
+
this.encryptionKey = fs31.readFileSync(this.keyPath);
|
|
3919
3919
|
} else {
|
|
3920
3920
|
this.encryptionKey = crypto.randomBytes(32);
|
|
3921
|
-
|
|
3921
|
+
fs31.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 = path34.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
|
|
4000
4000
|
const entries = this.getAllLogs();
|
|
4001
|
-
|
|
4001
|
+
fs31.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 && fs31.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 (!fs31.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 (!fs31.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 (!fs31.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
|
+
fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
|
|
4170
|
+
fs31.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
|
+
fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
|
|
4181
|
+
fs31.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4182
4182
|
}
|
|
4183
4183
|
readRaw() {
|
|
4184
|
-
const buf =
|
|
4184
|
+
const buf = fs31.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 = path34.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 ? path34.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 = path34.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 = path34.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 = path34.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
|
+
fs31.mkdirSync(path34.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 path34.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 = fs31.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
|
+
fs31.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
|
+
fs31.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
5614
|
+
fs31.renameSync(tmp, filePath);
|
|
5615
5615
|
try {
|
|
5616
|
-
|
|
5616
|
+
fs31.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 ?? path34.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(path34.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
5849
|
+
this.store = new MemoryStore(path34.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 = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5875
|
+
await fs11.mkdir(path34.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 = path34.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 = path34.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(path34.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
|
}
|
|
@@ -9015,6 +9015,79 @@ function describeProviderError(c, modelId) {
|
|
|
9015
9015
|
}
|
|
9016
9016
|
}
|
|
9017
9017
|
|
|
9018
|
+
// src/core/verification/acceptance.ts
|
|
9019
|
+
var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
|
|
9020
|
+
var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
|
|
9021
|
+
var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
|
|
9022
|
+
var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
|
|
9023
|
+
var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
|
|
9024
|
+
function targetFile(criterion, ownedFiles) {
|
|
9025
|
+
const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
|
|
9026
|
+
if (mentioned.length > 1) return null;
|
|
9027
|
+
if (mentioned.length === 1) {
|
|
9028
|
+
const candidate = mentioned[0];
|
|
9029
|
+
return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
|
|
9030
|
+
}
|
|
9031
|
+
return ownedFiles.length === 1 ? ownedFiles[0] : null;
|
|
9032
|
+
}
|
|
9033
|
+
async function evaluateAcceptance(criteria, ownedFiles, probe) {
|
|
9034
|
+
const results = [];
|
|
9035
|
+
for (const criterion of criteria) {
|
|
9036
|
+
const text = criterion.trim();
|
|
9037
|
+
if (!text) {
|
|
9038
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9039
|
+
continue;
|
|
9040
|
+
}
|
|
9041
|
+
if (NEGATION.test(text)) {
|
|
9042
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9043
|
+
continue;
|
|
9044
|
+
}
|
|
9045
|
+
const needle = text.match(QUOTED_NEEDLE)?.[1];
|
|
9046
|
+
const wantsExists = EXISTS.test(text);
|
|
9047
|
+
const wantsNonEmpty = NON_EMPTY.test(text);
|
|
9048
|
+
if (!needle && !wantsExists && !wantsNonEmpty) {
|
|
9049
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9050
|
+
continue;
|
|
9051
|
+
}
|
|
9052
|
+
const file = targetFile(text, ownedFiles);
|
|
9053
|
+
if (!file) {
|
|
9054
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9055
|
+
continue;
|
|
9056
|
+
}
|
|
9057
|
+
const stat = await probe.stat(file);
|
|
9058
|
+
if (!stat) {
|
|
9059
|
+
results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
|
|
9060
|
+
continue;
|
|
9061
|
+
}
|
|
9062
|
+
if (wantsNonEmpty && stat.size === 0) {
|
|
9063
|
+
results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
|
|
9064
|
+
continue;
|
|
9065
|
+
}
|
|
9066
|
+
if (needle) {
|
|
9067
|
+
const content = await probe.read(file);
|
|
9068
|
+
if (content === null) {
|
|
9069
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9070
|
+
continue;
|
|
9071
|
+
}
|
|
9072
|
+
const found = content.includes(needle);
|
|
9073
|
+
results.push({
|
|
9074
|
+
criterion,
|
|
9075
|
+
verdict: found ? "passed" : "failed",
|
|
9076
|
+
detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
|
|
9077
|
+
});
|
|
9078
|
+
continue;
|
|
9079
|
+
}
|
|
9080
|
+
results.push({ criterion, verdict: "passed", detail: `${file} exists` });
|
|
9081
|
+
}
|
|
9082
|
+
return results;
|
|
9083
|
+
}
|
|
9084
|
+
function undecided(results) {
|
|
9085
|
+
return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
|
|
9086
|
+
}
|
|
9087
|
+
function failures(results) {
|
|
9088
|
+
return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
|
|
9089
|
+
}
|
|
9090
|
+
|
|
9018
9091
|
// src/core/tiers/t3-worker.ts
|
|
9019
9092
|
var CriticalToolError = class extends Error {
|
|
9020
9093
|
constructor(message, toolName) {
|
|
@@ -9057,7 +9130,8 @@ var KNOWN_TOOLS = [
|
|
|
9057
9130
|
"generate_image",
|
|
9058
9131
|
"generate_video",
|
|
9059
9132
|
"generate_speech",
|
|
9060
|
-
"transcribe_audio"
|
|
9133
|
+
"transcribe_audio",
|
|
9134
|
+
"generate_document"
|
|
9061
9135
|
];
|
|
9062
9136
|
var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
|
|
9063
9137
|
"generate_image",
|
|
@@ -9074,18 +9148,45 @@ function buildWorkerRules(has) {
|
|
|
9074
9148
|
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.",
|
|
9075
9149
|
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
9076
9150
|
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
9151
|
+
// A .docx/.pptx/.xlsx is a ZIP of OOXML parts, not text with a suffix.
|
|
9152
|
+
// file_write does exactly what it promises — writes the model's characters
|
|
9153
|
+
// verbatim — so without this rule the worker "wrote" a Word document that
|
|
9154
|
+
// Word correctly reported as corrupted. Stated as an explicit prohibition
|
|
9155
|
+
// on the tool it would otherwise reach for, because "use generate_document"
|
|
9156
|
+
// alone competes with a file_write habit the model already has.
|
|
9157
|
+
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.',
|
|
9158
|
+
// The alternative to this rule is not "a worse chart" — it is a paragraph
|
|
9159
|
+
// describing a chart that does not exist, which is what real decks came
|
|
9160
|
+
// back with. A `chart:` block is checkable, carries the exact numbers, and
|
|
9161
|
+
// becomes a genuine editable PowerPoint chart object.
|
|
9162
|
+
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.',
|
|
9077
9163
|
// Without this the model "writes the image" as prose — a bracketed
|
|
9078
9164
|
// description sitting in the deck where the picture should be — even with a
|
|
9079
9165
|
// working image model registered. Naming the reference syntax matters as
|
|
9080
9166
|
// much as naming the tool: a generated image nobody embeds is still a
|
|
9081
|
-
// missing image.
|
|
9167
|
+
// missing image. It also has to say the reference must stand ALONE on its
|
|
9168
|
+
// line, because only a standalone reference is parsed as a picture (see
|
|
9169
|
+
// core/documents/blocks.ts matchImageLine) — one embedded mid-sentence
|
|
9170
|
+
// silently degrades to caption text.
|
|
9082
9171
|
//
|
|
9083
|
-
// Scoped to PowerPoint/Word specifically: those are the only
|
|
9084
|
-
//
|
|
9085
|
-
//
|
|
9086
|
-
//
|
|
9087
|
-
//
|
|
9088
|
-
has("generate_image") &&
|
|
9172
|
+
// Scoped to PowerPoint/Word specifically: those are the only renderers that
|
|
9173
|
+
// actually embed a Markdown image reference. PDF and plain-text/Markdown
|
|
9174
|
+
// deliverables flatten a reference straight to caption text — telling the
|
|
9175
|
+
// model to call generate_image for THOSE just pays for an image nobody
|
|
9176
|
+
// ever sees.
|
|
9177
|
+
has("generate_image") && // Supersedes the earlier wording of this same rule: it now names the
|
|
9178
|
+
// standalone-line parser constraint, says to generate BEFORE writing the
|
|
9179
|
+
// document, and points data-driven visuals at a ```chart: block rather
|
|
9180
|
+
// than a Markdown table, which is only the right advice now that chart
|
|
9181
|
+
// blocks render as real chart objects.
|
|
9182
|
+
'- 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.',
|
|
9183
|
+
// The other half of image reliability, and the half no amount of
|
|
9184
|
+
// instruction-tightening could fix: generate_image only exists when an
|
|
9185
|
+
// OpenAI or Gemini key is configured (multimodal/registry.ts CAPABILITIES).
|
|
9186
|
+
// With neither, the model was never given a choice — and a model that
|
|
9187
|
+
// doesn't know that writes "[illustration of a cat]" and moves on. Said out
|
|
9188
|
+
// loud, up front, so the fallback is a real chart or honest prose.
|
|
9189
|
+
!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.",
|
|
9089
9190
|
// The video counterpart of the image rule above, and the fix for the
|
|
9090
9191
|
// reported "it writes scripts forever and never makes the video" run. The
|
|
9091
9192
|
// failure mode is identical — the model narrates the deliverable instead of
|
|
@@ -9095,7 +9196,7 @@ function buildWorkerRules(has) {
|
|
|
9095
9196
|
// the deliverable rather than an illustration inside one, and the reference
|
|
9096
9197
|
// syntax is a plain link: Markdown image syntax does not embed a video.
|
|
9097
9198
|
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.',
|
|
9098
|
-
has("run_code") &&
|
|
9199
|
+
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.`,
|
|
9099
9200
|
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
9100
9201
|
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.',
|
|
9101
9202
|
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.",
|
|
@@ -9106,13 +9207,34 @@ function buildWorkerRules(has) {
|
|
|
9106
9207
|
Rules:
|
|
9107
9208
|
${rules.filter((r) => r !== false).join("\n")}`;
|
|
9108
9209
|
}
|
|
9109
|
-
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell"]);
|
|
9210
|
+
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell", "generate_document"]);
|
|
9211
|
+
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;
|
|
9110
9212
|
function shouldRequireArtifact(assignment, toolNames) {
|
|
9111
9213
|
if (!toolNames.some((n) => ARTIFACT_TOOLS.has(n))) return false;
|
|
9112
9214
|
if (assignment?.files?.length) return true;
|
|
9113
9215
|
const haystack = `${assignment?.description ?? ""}
|
|
9114
9216
|
${assignment?.expectedOutput ?? ""}`;
|
|
9115
|
-
return
|
|
9217
|
+
return ARTIFACT_FILE_RE.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
|
|
9218
|
+
}
|
|
9219
|
+
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;
|
|
9220
|
+
var IMAGE_REF_RE = /!\[[^\]]*\]\([^)]+\)/;
|
|
9221
|
+
var CHART_BLOCK_RE = /^\s*```+\s*chart\s*:/im;
|
|
9222
|
+
function missingVisualEvidence(assignment, output, calledTools, toolNames) {
|
|
9223
|
+
const canVisualize = toolNames.includes("generate_image") || toolNames.includes("generate_document");
|
|
9224
|
+
if (!canVisualize) return null;
|
|
9225
|
+
const haystack = `${assignment?.description ?? ""}
|
|
9226
|
+
${assignment?.expectedOutput ?? ""}`;
|
|
9227
|
+
if (!VISUAL_REQUEST_RE.test(haystack)) return null;
|
|
9228
|
+
if (calledTools.includes("generate_image") || calledTools.includes("generate_document")) return null;
|
|
9229
|
+
if (IMAGE_REF_RE.test(output) || CHART_BLOCK_RE.test(output)) return null;
|
|
9230
|
+
const options = [];
|
|
9231
|
+
if (toolNames.includes("generate_image")) {
|
|
9232
|
+
options.push('call "generate_image" and reference the result on its own line as ');
|
|
9233
|
+
}
|
|
9234
|
+
if (toolNames.includes("generate_document")) {
|
|
9235
|
+
options.push("emit a fenced ```chart:bar (or chart:line / chart:pie) block whose body is CSV, for anything data-driven");
|
|
9236
|
+
}
|
|
9237
|
+
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.`;
|
|
9116
9238
|
}
|
|
9117
9239
|
var T3Worker = class extends BaseTier {
|
|
9118
9240
|
router;
|
|
@@ -9282,7 +9404,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
9282
9404
|
}
|
|
9283
9405
|
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
9284
9406
|
if (!artifactCheck.ok) {
|
|
9285
|
-
correctionAttempts
|
|
9407
|
+
correctionAttempts++;
|
|
9286
9408
|
issues.push(...artifactCheck.issues);
|
|
9287
9409
|
output = await this.correctOutput(output, artifactCheck.issues);
|
|
9288
9410
|
const retryArtifactCheck = await this.verifyArtifacts(assignment);
|
|
@@ -9293,13 +9415,47 @@ Now execute your subtask using this context where relevant.`
|
|
|
9293
9415
|
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
9294
9416
|
}
|
|
9295
9417
|
}
|
|
9418
|
+
const visualIssue = missingVisualEvidence(
|
|
9419
|
+
assignment,
|
|
9420
|
+
output,
|
|
9421
|
+
toolCalls.map((t) => t.name),
|
|
9422
|
+
this.tools.map((t) => t.name)
|
|
9423
|
+
);
|
|
9424
|
+
if (visualIssue) {
|
|
9425
|
+
correctionAttempts++;
|
|
9426
|
+
issues.push(visualIssue);
|
|
9427
|
+
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
9428
|
+
output = await this.correctOutput(output, [visualIssue]);
|
|
9429
|
+
}
|
|
9430
|
+
const acceptanceResults = await this.checkAcceptance(assignment);
|
|
9431
|
+
const acceptanceFailures = failures(acceptanceResults);
|
|
9432
|
+
if (acceptanceFailures.length > 0) {
|
|
9433
|
+
correctionAttempts++;
|
|
9434
|
+
issues.push(...acceptanceFailures);
|
|
9435
|
+
this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
|
|
9436
|
+
output = await this.correctOutput(output, acceptanceFailures);
|
|
9437
|
+
const recheck = failures(await this.checkAcceptance(assignment));
|
|
9438
|
+
if (recheck.length > 0) {
|
|
9439
|
+
issues.push(...recheck);
|
|
9440
|
+
checksRun.push(...acceptanceResults.map((r) => r.criterion));
|
|
9441
|
+
failed.push(...recheck);
|
|
9442
|
+
this.setStatus("FAILED");
|
|
9443
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
|
|
9444
|
+
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
9445
|
+
}
|
|
9446
|
+
}
|
|
9447
|
+
for (const result2 of acceptanceResults) {
|
|
9448
|
+
if (result2.verdict === "undecidable") continue;
|
|
9449
|
+
checksRun.push(result2.criterion);
|
|
9450
|
+
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
9451
|
+
}
|
|
9296
9452
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9297
|
-
const testResult = await this.selfTest(assignment, output);
|
|
9453
|
+
const testResult = await this.selfTest(assignment, output, undecided(acceptanceResults));
|
|
9298
9454
|
checksRun.push(...testResult.checksRun);
|
|
9299
9455
|
passed.push(...testResult.passed);
|
|
9300
9456
|
failed.push(...testResult.failed);
|
|
9301
9457
|
if (testResult.failed.length > 0) {
|
|
9302
|
-
correctionAttempts
|
|
9458
|
+
correctionAttempts++;
|
|
9303
9459
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
9304
9460
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
9305
9461
|
output = corrected;
|
|
@@ -9607,8 +9763,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9607
9763
|
// Media generation can run for a minute; without this a cancelled run
|
|
9608
9764
|
// still pays for an image nobody will see.
|
|
9609
9765
|
...this.signal ? { signal: this.signal } : {},
|
|
9610
|
-
saveSnapshot: async (
|
|
9611
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
9766
|
+
saveSnapshot: async (path39, content) => {
|
|
9767
|
+
this.store?.addFileSnapshot(this.taskId, path39, content);
|
|
9612
9768
|
},
|
|
9613
9769
|
sendPeerSync: (to, syncType, content) => {
|
|
9614
9770
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -9784,7 +9940,7 @@ ${assignment.expectedOutput}`;
|
|
|
9784
9940
|
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
9785
9941
|
const haystack = `${assignment.description}
|
|
9786
9942
|
${assignment.expectedOutput}`;
|
|
9787
|
-
const matches = haystack.match(
|
|
9943
|
+
const matches = haystack.match(new RegExp(ARTIFACT_FILE_RE.source, "gi")) ?? [];
|
|
9788
9944
|
return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
|
|
9789
9945
|
}
|
|
9790
9946
|
async verifyArtifacts(assignment) {
|
|
@@ -9795,9 +9951,9 @@ ${assignment.expectedOutput}`;
|
|
|
9795
9951
|
const { promisify: promisify4 } = await import('util');
|
|
9796
9952
|
const execAsync3 = promisify4(exec3);
|
|
9797
9953
|
for (const artifactPath of artifactPaths) {
|
|
9798
|
-
const absolutePath =
|
|
9954
|
+
const absolutePath = path34.resolve(process.cwd(), artifactPath);
|
|
9799
9955
|
try {
|
|
9800
|
-
const stat = await
|
|
9956
|
+
const stat = await fs11.stat(absolutePath);
|
|
9801
9957
|
if (!stat.isFile()) {
|
|
9802
9958
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
9803
9959
|
continue;
|
|
@@ -9806,8 +9962,22 @@ ${assignment.expectedOutput}`;
|
|
|
9806
9962
|
issues.push(`Artifact is empty: ${artifactPath}`);
|
|
9807
9963
|
continue;
|
|
9808
9964
|
}
|
|
9809
|
-
if (
|
|
9810
|
-
const
|
|
9965
|
+
if (/\.(docx|pptx|xlsx)$/i.test(artifactPath)) {
|
|
9966
|
+
const head = Buffer.alloc(4);
|
|
9967
|
+
const fh = await fs11.open(absolutePath, "r");
|
|
9968
|
+
try {
|
|
9969
|
+
await fh.read(head, 0, 4, 0);
|
|
9970
|
+
} finally {
|
|
9971
|
+
await fh.close();
|
|
9972
|
+
}
|
|
9973
|
+
if (head.toString("latin1") !== "PK") {
|
|
9974
|
+
issues.push(
|
|
9975
|
+
`${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.`
|
|
9976
|
+
);
|
|
9977
|
+
continue;
|
|
9978
|
+
}
|
|
9979
|
+
} else if (!/\.pdf$/i.test(artifactPath)) {
|
|
9980
|
+
const content = await fs11.readFile(absolutePath, "utf-8");
|
|
9811
9981
|
if (!content.trim()) {
|
|
9812
9982
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
9813
9983
|
continue;
|
|
@@ -9816,7 +9986,7 @@ ${assignment.expectedOutput}`;
|
|
|
9816
9986
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
9817
9987
|
continue;
|
|
9818
9988
|
}
|
|
9819
|
-
const ext =
|
|
9989
|
+
const ext = path34.extname(absolutePath).toLowerCase();
|
|
9820
9990
|
try {
|
|
9821
9991
|
if (ext === ".ts" || ext === ".tsx") {
|
|
9822
9992
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -9902,14 +10072,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9902
10072
|
}
|
|
9903
10073
|
return current;
|
|
9904
10074
|
}
|
|
9905
|
-
|
|
10075
|
+
/**
|
|
10076
|
+
* Run the acceptance criteria that can be settled by looking at the workspace.
|
|
10077
|
+
* Reads go through the same workspace-relative resolution verifyArtifacts uses.
|
|
10078
|
+
*/
|
|
10079
|
+
async checkAcceptance(assignment) {
|
|
10080
|
+
const criteria = assignment.acceptance ?? [];
|
|
10081
|
+
if (!criteria.length) return [];
|
|
10082
|
+
const resolve2 = (target) => path34.resolve(process.cwd(), target);
|
|
10083
|
+
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
10084
|
+
stat: async (target) => {
|
|
10085
|
+
try {
|
|
10086
|
+
const stat = await fs11.stat(resolve2(target));
|
|
10087
|
+
return stat.isFile() ? { size: stat.size } : null;
|
|
10088
|
+
} catch {
|
|
10089
|
+
return null;
|
|
10090
|
+
}
|
|
10091
|
+
},
|
|
10092
|
+
read: async (target) => {
|
|
10093
|
+
try {
|
|
10094
|
+
const content = await fs11.readFile(resolve2(target), "utf-8");
|
|
10095
|
+
return content.includes("\uFFFD") ? null : content;
|
|
10096
|
+
} catch {
|
|
10097
|
+
return null;
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
});
|
|
10101
|
+
}
|
|
10102
|
+
async selfTest(assignment, output, pendingAcceptance) {
|
|
10103
|
+
const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
|
|
9906
10104
|
const prompt = `Self-test this output against the assignment requirements.
|
|
9907
10105
|
|
|
9908
10106
|
Assignment: ${assignment.description}
|
|
9909
10107
|
Expected output: ${assignment.expectedOutput}
|
|
9910
10108
|
Constraints: ${assignment.constraints.join("; ")}
|
|
9911
|
-
${
|
|
9912
|
-
${
|
|
10109
|
+
${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
10110
|
+
${acceptance.map((a) => `- ${a}`).join("\n")}
|
|
9913
10111
|
` : ""}
|
|
9914
10112
|
Output to test:
|
|
9915
10113
|
${output}
|
|
@@ -9976,8 +10174,8 @@ ${output.slice(0, 4e3)}`;
|
|
|
9976
10174
|
} catch {
|
|
9977
10175
|
}
|
|
9978
10176
|
}
|
|
9979
|
-
async correctOutput(originalOutput,
|
|
9980
|
-
const correctionPrompt = `The following output failed these checks: ${
|
|
10177
|
+
async correctOutput(originalOutput, failures2) {
|
|
10178
|
+
const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
|
|
9981
10179
|
|
|
9982
10180
|
Original output:
|
|
9983
10181
|
${originalOutput}
|
|
@@ -10639,12 +10837,220 @@ function describeGenerationForPlanner(has) {
|
|
|
10639
10837
|
return lines.join("\n");
|
|
10640
10838
|
}
|
|
10641
10839
|
|
|
10840
|
+
// src/core/orchestration/task-graph.ts
|
|
10841
|
+
var TaskGraphValidationError = class extends Error {
|
|
10842
|
+
constructor(issues) {
|
|
10843
|
+
super(issues.map((issue) => issue.message).join("; "));
|
|
10844
|
+
this.issues = issues;
|
|
10845
|
+
this.name = "TaskGraphValidationError";
|
|
10846
|
+
}
|
|
10847
|
+
issues;
|
|
10848
|
+
};
|
|
10849
|
+
function normalizeDependencies(input) {
|
|
10850
|
+
if (!input?.length) return [];
|
|
10851
|
+
return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
|
|
10852
|
+
}
|
|
10853
|
+
function cycleNodes(nodes) {
|
|
10854
|
+
const adjacency = new Map(
|
|
10855
|
+
nodes.map((node) => [node.id, node.dependsOn])
|
|
10856
|
+
);
|
|
10857
|
+
const index = /* @__PURE__ */ new Map();
|
|
10858
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
10859
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
10860
|
+
const stack = [];
|
|
10861
|
+
const inCycle = /* @__PURE__ */ new Set();
|
|
10862
|
+
let counter = 0;
|
|
10863
|
+
const open = (id) => {
|
|
10864
|
+
index.set(id, counter);
|
|
10865
|
+
lowlink.set(id, counter);
|
|
10866
|
+
counter++;
|
|
10867
|
+
stack.push(id);
|
|
10868
|
+
onStack.add(id);
|
|
10869
|
+
};
|
|
10870
|
+
for (const root of nodes) {
|
|
10871
|
+
if (index.has(root.id)) continue;
|
|
10872
|
+
open(root.id);
|
|
10873
|
+
const frames = [{ id: root.id, edge: 0 }];
|
|
10874
|
+
while (frames.length > 0) {
|
|
10875
|
+
const frame = frames[frames.length - 1];
|
|
10876
|
+
const neighbours = adjacency.get(frame.id) ?? [];
|
|
10877
|
+
if (frame.edge < neighbours.length) {
|
|
10878
|
+
const next = neighbours[frame.edge];
|
|
10879
|
+
frame.edge++;
|
|
10880
|
+
if (!adjacency.has(next)) continue;
|
|
10881
|
+
if (!index.has(next)) {
|
|
10882
|
+
open(next);
|
|
10883
|
+
frames.push({ id: next, edge: 0 });
|
|
10884
|
+
} else if (onStack.has(next)) {
|
|
10885
|
+
lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
|
|
10886
|
+
}
|
|
10887
|
+
continue;
|
|
10888
|
+
}
|
|
10889
|
+
frames.pop();
|
|
10890
|
+
const parent = frames[frames.length - 1];
|
|
10891
|
+
if (parent) {
|
|
10892
|
+
lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
|
|
10893
|
+
}
|
|
10894
|
+
if (lowlink.get(frame.id) === index.get(frame.id)) {
|
|
10895
|
+
const component = [];
|
|
10896
|
+
for (; ; ) {
|
|
10897
|
+
const member = stack.pop();
|
|
10898
|
+
onStack.delete(member);
|
|
10899
|
+
component.push(member);
|
|
10900
|
+
if (member === frame.id) break;
|
|
10901
|
+
}
|
|
10902
|
+
const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
|
|
10903
|
+
if (cyclic) for (const member of component) inCycle.add(member);
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
return inCycle;
|
|
10908
|
+
}
|
|
10909
|
+
function throwIfStrict(mode, issues) {
|
|
10910
|
+
if (mode === "strict" && issues.length > 0) {
|
|
10911
|
+
throw new TaskGraphValidationError(issues);
|
|
10912
|
+
}
|
|
10913
|
+
}
|
|
10914
|
+
function compileTaskGraph(inputNodes, options = {}) {
|
|
10915
|
+
const mode = options.mode ?? "strict";
|
|
10916
|
+
const issues = [];
|
|
10917
|
+
if (inputNodes.length === 0) {
|
|
10918
|
+
issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
|
|
10919
|
+
throwIfStrict(mode, issues);
|
|
10920
|
+
}
|
|
10921
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10922
|
+
const nodes = [];
|
|
10923
|
+
for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
|
|
10924
|
+
const input = inputNodes[ordinal];
|
|
10925
|
+
const id = input.id?.trim();
|
|
10926
|
+
const title = input.title?.trim();
|
|
10927
|
+
if (!id || !title) {
|
|
10928
|
+
issues.push({
|
|
10929
|
+
code: "INVALID_NODE",
|
|
10930
|
+
nodeId: id || void 0,
|
|
10931
|
+
message: `Task graph node at index ${ordinal} requires a non-empty id and title`
|
|
10932
|
+
});
|
|
10933
|
+
continue;
|
|
10934
|
+
}
|
|
10935
|
+
if (seen.has(id)) {
|
|
10936
|
+
issues.push({
|
|
10937
|
+
code: "DUPLICATE_NODE_ID",
|
|
10938
|
+
nodeId: id,
|
|
10939
|
+
message: `Duplicate task graph node id: ${id}`
|
|
10940
|
+
});
|
|
10941
|
+
continue;
|
|
10942
|
+
}
|
|
10943
|
+
seen.add(id);
|
|
10944
|
+
nodes.push({
|
|
10945
|
+
id,
|
|
10946
|
+
title,
|
|
10947
|
+
kind: input.kind,
|
|
10948
|
+
dependsOn: normalizeDependencies(input.dependsOn),
|
|
10949
|
+
payload: input.payload,
|
|
10950
|
+
ordinal
|
|
10951
|
+
});
|
|
10952
|
+
}
|
|
10953
|
+
throwIfStrict(mode, issues);
|
|
10954
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
10955
|
+
const sanitized = nodes.map((node) => {
|
|
10956
|
+
const dependsOn = [];
|
|
10957
|
+
for (const dependencyId of node.dependsOn) {
|
|
10958
|
+
if (dependencyId === node.id) {
|
|
10959
|
+
issues.push({
|
|
10960
|
+
code: "SELF_DEPENDENCY",
|
|
10961
|
+
nodeId: node.id,
|
|
10962
|
+
dependencyId,
|
|
10963
|
+
message: `Task graph node ${node.id} cannot depend on itself`
|
|
10964
|
+
});
|
|
10965
|
+
continue;
|
|
10966
|
+
}
|
|
10967
|
+
if (!ids.has(dependencyId)) {
|
|
10968
|
+
issues.push({
|
|
10969
|
+
code: "UNKNOWN_DEPENDENCY",
|
|
10970
|
+
nodeId: node.id,
|
|
10971
|
+
dependencyId,
|
|
10972
|
+
message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
|
|
10973
|
+
});
|
|
10974
|
+
continue;
|
|
10975
|
+
}
|
|
10976
|
+
dependsOn.push(dependencyId);
|
|
10977
|
+
}
|
|
10978
|
+
return { ...node, dependsOn };
|
|
10979
|
+
});
|
|
10980
|
+
throwIfStrict(mode, issues);
|
|
10981
|
+
const cyclic = cycleNodes(sanitized);
|
|
10982
|
+
let repaired = sanitized;
|
|
10983
|
+
if (cyclic.size > 0) {
|
|
10984
|
+
const cycleIssues = [];
|
|
10985
|
+
repaired = sanitized.map((node) => {
|
|
10986
|
+
if (!cyclic.has(node.id)) return node;
|
|
10987
|
+
const dependsOn = node.dependsOn.filter((dependencyId) => {
|
|
10988
|
+
if (!cyclic.has(dependencyId)) return true;
|
|
10989
|
+
cycleIssues.push({
|
|
10990
|
+
code: "CYCLE_EDGE_REMOVED",
|
|
10991
|
+
nodeId: node.id,
|
|
10992
|
+
dependencyId,
|
|
10993
|
+
message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
|
|
10994
|
+
});
|
|
10995
|
+
return false;
|
|
10996
|
+
});
|
|
10997
|
+
return { ...node, dependsOn };
|
|
10998
|
+
});
|
|
10999
|
+
issues.push(...cycleIssues);
|
|
11000
|
+
throwIfStrict(mode, cycleIssues);
|
|
11001
|
+
}
|
|
11002
|
+
const remainingCycle = cycleNodes(repaired);
|
|
11003
|
+
if (remainingCycle.size > 0) {
|
|
11004
|
+
throw new TaskGraphValidationError([
|
|
11005
|
+
{
|
|
11006
|
+
code: "INVALID_NODE",
|
|
11007
|
+
message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
|
|
11008
|
+
}
|
|
11009
|
+
]);
|
|
11010
|
+
}
|
|
11011
|
+
return {
|
|
11012
|
+
graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
|
|
11013
|
+
issues
|
|
11014
|
+
};
|
|
11015
|
+
}
|
|
11016
|
+
|
|
11017
|
+
// src/core/orchestration/adapters.ts
|
|
11018
|
+
function compileSectionGraph(sections) {
|
|
11019
|
+
return compileTaskGraph(
|
|
11020
|
+
sections.map((section) => ({
|
|
11021
|
+
id: section.sectionId,
|
|
11022
|
+
title: section.sectionTitle,
|
|
11023
|
+
kind: "section",
|
|
11024
|
+
dependsOn: section.dependsOn,
|
|
11025
|
+
payload: section
|
|
11026
|
+
})),
|
|
11027
|
+
{ mode: "repair" }
|
|
11028
|
+
);
|
|
11029
|
+
}
|
|
11030
|
+
function compileSubtaskGraph(subtasks) {
|
|
11031
|
+
return compileTaskGraph(
|
|
11032
|
+
subtasks.map((subtask) => ({
|
|
11033
|
+
id: subtask.subtaskId,
|
|
11034
|
+
title: subtask.subtaskTitle,
|
|
11035
|
+
kind: "subtask",
|
|
11036
|
+
dependsOn: subtask.dependsOn,
|
|
11037
|
+
payload: subtask
|
|
11038
|
+
})),
|
|
11039
|
+
{ mode: "repair" }
|
|
11040
|
+
);
|
|
11041
|
+
}
|
|
11042
|
+
|
|
10642
11043
|
// src/core/tiers/t2-manager.ts
|
|
10643
11044
|
function buildT2SystemPrompt(has) {
|
|
10644
11045
|
const generation = describeGenerationForPlanner(has);
|
|
10645
11046
|
return [
|
|
10646
11047
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
10647
|
-
|
|
11048
|
+
// Must agree with the decomposition prompt in decomposeSection(), which is
|
|
11049
|
+
// the one carrying the actual instruction. It said 1-4 and "the FEWEST that
|
|
11050
|
+
// fully cover it"; this said 2-5. The model received both, so its floor was
|
|
11051
|
+
// simultaneously one and two, and the cheaper reading — pad to two — costs a
|
|
11052
|
+
// real worker call on every small section.
|
|
11053
|
+
"Your role is to analyze a section of a task and decompose it into 1-4 discrete subtasks for T3 Workers \u2014 the fewest that fully cover it.",
|
|
10648
11054
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
10649
11055
|
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
10650
11056
|
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
@@ -11040,21 +11446,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11040
11446
|
* then reduce in-degrees of their dependents and repeat.
|
|
11041
11447
|
*/
|
|
11042
11448
|
async runWithDependencies(assignments, workerMap, taskId) {
|
|
11449
|
+
const compiled = compileSubtaskGraph(assignments);
|
|
11450
|
+
for (const issue of compiled.issues) {
|
|
11451
|
+
this.log(`\u26A0 Subtask graph: ${issue.message}`);
|
|
11452
|
+
}
|
|
11453
|
+
const sanitizedAssignments = compiled.graph.nodes.map((node) => {
|
|
11454
|
+
const assignment = node.payload;
|
|
11455
|
+
const original = assignment.dependsOn ?? [];
|
|
11456
|
+
const repaired = node.dependsOn;
|
|
11457
|
+
const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
|
|
11458
|
+
return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
|
|
11459
|
+
});
|
|
11043
11460
|
const adj = /* @__PURE__ */ new Map();
|
|
11044
11461
|
const inDegree = /* @__PURE__ */ new Map();
|
|
11045
11462
|
const resultMap = /* @__PURE__ */ new Map();
|
|
11046
|
-
for (const a of
|
|
11463
|
+
for (const a of sanitizedAssignments) {
|
|
11047
11464
|
if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
|
|
11048
11465
|
inDegree.set(a.subtaskId, 0);
|
|
11049
11466
|
}
|
|
11050
|
-
for (const a of
|
|
11051
|
-
const
|
|
11052
|
-
for (const dep of deps) {
|
|
11467
|
+
for (const a of sanitizedAssignments) {
|
|
11468
|
+
for (const dep of a.dependsOn ?? []) {
|
|
11053
11469
|
adj.get(dep).add(a.subtaskId);
|
|
11054
11470
|
inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
|
|
11055
11471
|
}
|
|
11056
11472
|
}
|
|
11057
|
-
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
11058
11473
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
11059
11474
|
let wave = 0;
|
|
11060
11475
|
let respawnBudget = 1;
|
|
@@ -11261,49 +11676,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11261
11676
|
}
|
|
11262
11677
|
return [...resultMap.values()];
|
|
11263
11678
|
}
|
|
11264
|
-
/**
|
|
11265
|
-
* Detects cyclic dependencies using Kahn's algorithm and breaks them
|
|
11266
|
-
* by removing back-edges. Returns a sanitized copy of assignments.
|
|
11267
|
-
*
|
|
11268
|
-
* A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
|
|
11269
|
-
* meaning t3 will start without waiting for t1, preventing deadlock.
|
|
11270
|
-
*/
|
|
11271
|
-
breakCycles(assignments, adj, inDegree) {
|
|
11272
|
-
const degree = new Map(inDegree);
|
|
11273
|
-
const queue = [];
|
|
11274
|
-
const visited = /* @__PURE__ */ new Set();
|
|
11275
|
-
for (const [id, d] of degree) {
|
|
11276
|
-
if (d === 0) queue.push(id);
|
|
11277
|
-
}
|
|
11278
|
-
while (queue.length > 0) {
|
|
11279
|
-
const id = queue.shift();
|
|
11280
|
-
visited.add(id);
|
|
11281
|
-
for (const dep of adj.get(id) ?? []) {
|
|
11282
|
-
const newDeg = (degree.get(dep) ?? 1) - 1;
|
|
11283
|
-
degree.set(dep, newDeg);
|
|
11284
|
-
if (newDeg === 0) queue.push(dep);
|
|
11285
|
-
}
|
|
11286
|
-
}
|
|
11287
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
11288
|
-
if (cycleNodes.length === 0) return assignments;
|
|
11289
|
-
this.log(
|
|
11290
|
-
`\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
|
|
11291
|
-
);
|
|
11292
|
-
return assignments.map((a) => {
|
|
11293
|
-
if (!cycleNodes.includes(a.subtaskId)) return a;
|
|
11294
|
-
const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
11295
|
-
if (safeDeps.length !== (a.dependsOn ?? []).length) {
|
|
11296
|
-
this.log(
|
|
11297
|
-
` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
|
|
11298
|
-
);
|
|
11299
|
-
for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
11300
|
-
inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
|
|
11301
|
-
adj.get(removed)?.delete(a.subtaskId);
|
|
11302
|
-
}
|
|
11303
|
-
}
|
|
11304
|
-
return { ...a, dependsOn: safeDeps };
|
|
11305
|
-
});
|
|
11306
|
-
}
|
|
11307
11679
|
async retryT3(assignment, taskId) {
|
|
11308
11680
|
this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
|
|
11309
11681
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
@@ -11442,6 +11814,57 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11442
11814
|
// src/core/tiers/t1-administrator.ts
|
|
11443
11815
|
init_constants();
|
|
11444
11816
|
|
|
11817
|
+
// src/core/orchestration/scheduler.ts
|
|
11818
|
+
var DependencyScheduler = class {
|
|
11819
|
+
constructor(graph, hooks, options = {}) {
|
|
11820
|
+
this.graph = graph;
|
|
11821
|
+
this.hooks = hooks;
|
|
11822
|
+
this.options = options;
|
|
11823
|
+
}
|
|
11824
|
+
graph;
|
|
11825
|
+
hooks;
|
|
11826
|
+
options;
|
|
11827
|
+
async run() {
|
|
11828
|
+
const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
|
|
11829
|
+
const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
|
|
11830
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
11831
|
+
for (const node of nodes) dependents.set(node.id, []);
|
|
11832
|
+
for (const node of nodes) {
|
|
11833
|
+
for (const dependencyId of node.dependsOn) {
|
|
11834
|
+
dependents.get(dependencyId)?.push(node.id);
|
|
11835
|
+
}
|
|
11836
|
+
}
|
|
11837
|
+
const completed = /* @__PURE__ */ new Set();
|
|
11838
|
+
const results = /* @__PURE__ */ new Map();
|
|
11839
|
+
const waves = [];
|
|
11840
|
+
let wave = 0;
|
|
11841
|
+
while (completed.size < nodes.length) {
|
|
11842
|
+
const ready = nodes.filter((node) => !completed.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
11843
|
+
if (ready.length === 0) {
|
|
11844
|
+
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
11845
|
+
}
|
|
11846
|
+
waves.push(ready.map((node) => node.id));
|
|
11847
|
+
await this.hooks.onWaveStart?.(ready, wave);
|
|
11848
|
+
const runOne = async (node) => {
|
|
11849
|
+
const result = await this.hooks.execute(node);
|
|
11850
|
+
results.set(node.id, result);
|
|
11851
|
+
completed.add(node.id);
|
|
11852
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
11853
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
11854
|
+
}
|
|
11855
|
+
await this.hooks.onNodeComplete?.(node, result);
|
|
11856
|
+
};
|
|
11857
|
+
if ((this.options.mode ?? "parallel") === "sequential") {
|
|
11858
|
+
for (const node of ready) await runOne(node);
|
|
11859
|
+
} else {
|
|
11860
|
+
await Promise.all(ready.map(runOne));
|
|
11861
|
+
}
|
|
11862
|
+
wave++;
|
|
11863
|
+
}
|
|
11864
|
+
return { results, waves };
|
|
11865
|
+
}
|
|
11866
|
+
};
|
|
11867
|
+
|
|
11445
11868
|
// src/utils/json-extract.ts
|
|
11446
11869
|
function extractFirstJsonObject(input) {
|
|
11447
11870
|
if (!input) return null;
|
|
@@ -11513,7 +11936,7 @@ function buildT1SystemPrompt(has) {
|
|
|
11513
11936
|
Your responsibilities:
|
|
11514
11937
|
1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
|
|
11515
11938
|
2. Decompose the task into logical sections (one per T2 Manager)
|
|
11516
|
-
3. For each section, define
|
|
11939
|
+
3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
|
|
11517
11940
|
4. Return a structured plan as JSON
|
|
11518
11941
|
|
|
11519
11942
|
CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
|
|
@@ -11928,7 +12351,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
11928
12351
|
}
|
|
11929
12352
|
}
|
|
11930
12353
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
11931
|
-
if (plan.sections.length < min)
|
|
12354
|
+
if (plan.sections.length < min || plan.sections.length > max) {
|
|
12355
|
+
this.log(
|
|
12356
|
+
`\u26A0 Plan has ${plan.sections.length} section(s) but complexity "${plan.complexity}" expects ${min}-${max}. Keeping the plan as written \u2014 a right-sized plan is better than one padded to hit a floor.`
|
|
12357
|
+
);
|
|
12358
|
+
}
|
|
11932
12359
|
}
|
|
11933
12360
|
async dispatchT2Managers(sections) {
|
|
11934
12361
|
for (const section of sections) {
|
|
@@ -12040,114 +12467,91 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12040
12467
|
return t2Results;
|
|
12041
12468
|
}
|
|
12042
12469
|
/**
|
|
12043
|
-
* Runs T2 managers respecting dependsOn declarations
|
|
12470
|
+
* Runs T2 managers respecting dependsOn declarations.
|
|
12471
|
+
*
|
|
12472
|
+
* Graph construction, validation and cycle repair now live in
|
|
12473
|
+
* compileSectionGraph (orchestration/adapters.ts), and wave execution in
|
|
12474
|
+
* DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
|
|
12475
|
+
* ordered by one implementation rather than two that drifted.
|
|
12476
|
+
*
|
|
12477
|
+
* The hand-rolled Kahn pass this replaces treated every node it could not
|
|
12478
|
+
* reach as part of a cycle. That set also contains everything DOWNSTREAM of a
|
|
12479
|
+
* cycle, whose in-degree can never fall to zero while its dependency is stuck,
|
|
12480
|
+
* so breaking a cycle here also cut the dependencies of innocent later
|
|
12481
|
+
* sections and started them immediately — before the sections they consume.
|
|
12482
|
+
* compileTaskGraph uses strongly connected components, so only real cycle
|
|
12483
|
+
* members are touched.
|
|
12044
12484
|
*/
|
|
12045
12485
|
async runT2sWithDependencies(sections, managers, taskId) {
|
|
12046
|
-
const
|
|
12047
|
-
const
|
|
12048
|
-
|
|
12049
|
-
const allKeys = new Set(sections.map((s) => s.sectionId));
|
|
12050
|
-
for (const s of sections) {
|
|
12051
|
-
if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
|
|
12052
|
-
inDegree.set(s.sectionId, 0);
|
|
12053
|
-
s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
|
|
12054
|
-
}
|
|
12055
|
-
for (const s of sections) {
|
|
12056
|
-
for (const dep of s.dependsOn ?? []) {
|
|
12057
|
-
adj.get(dep).add(s.sectionId);
|
|
12058
|
-
inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
|
|
12059
|
-
}
|
|
12060
|
-
}
|
|
12061
|
-
const queue = [];
|
|
12062
|
-
const degree = new Map(inDegree);
|
|
12063
|
-
for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
|
|
12064
|
-
const visited = /* @__PURE__ */ new Set();
|
|
12065
|
-
while (queue.length > 0) {
|
|
12066
|
-
const u = queue.shift();
|
|
12067
|
-
visited.add(u);
|
|
12068
|
-
for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
|
|
12069
|
-
const newDeg = (degree.get(v) ?? 1) - 1;
|
|
12070
|
-
degree.set(v, newDeg);
|
|
12071
|
-
if (newDeg === 0) queue.push(v);
|
|
12072
|
-
}
|
|
12073
|
-
}
|
|
12074
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
12075
|
-
if (cycleNodes.length > 0) {
|
|
12076
|
-
this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
|
|
12077
|
-
for (const s of sections) {
|
|
12078
|
-
if (cycleNodes.includes(s.sectionId)) {
|
|
12079
|
-
const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
12080
|
-
for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
12081
|
-
inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
|
|
12082
|
-
adj.get(removed)?.delete(s.sectionId);
|
|
12083
|
-
}
|
|
12084
|
-
s.dependsOn = safeDeps;
|
|
12085
|
-
}
|
|
12086
|
-
}
|
|
12486
|
+
const compiled = compileSectionGraph(sections);
|
|
12487
|
+
for (const issue of compiled.issues) {
|
|
12488
|
+
this.log(`\u26A0 Section graph: ${issue.message}`);
|
|
12087
12489
|
}
|
|
12088
|
-
const
|
|
12490
|
+
const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
|
|
12491
|
+
for (const section of sections) {
|
|
12492
|
+
const node = byId.get(section.sectionId);
|
|
12493
|
+
if (node) section.dependsOn = [...node.dependsOn];
|
|
12494
|
+
}
|
|
12495
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
12496
|
+
const totalSections = compiled.graph.nodes.length;
|
|
12089
12497
|
let completedSections = 0;
|
|
12090
|
-
const
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12498
|
+
const scheduler = new DependencyScheduler(
|
|
12499
|
+
compiled.graph,
|
|
12500
|
+
{
|
|
12501
|
+
execute: async (node) => {
|
|
12502
|
+
const section = node.payload;
|
|
12503
|
+
const manager = managers[node.ordinal];
|
|
12504
|
+
const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
|
|
12505
|
+
this.sendStatusUpdate({
|
|
12506
|
+
progressPct,
|
|
12507
|
+
currentAction: `T2 working on: ${section.sectionTitle}`,
|
|
12508
|
+
status: "IN_PROGRESS"
|
|
12509
|
+
});
|
|
12510
|
+
this.throwIfCancelled();
|
|
12511
|
+
try {
|
|
12512
|
+
const result = await manager.execute(section, taskId, this.signal);
|
|
12513
|
+
manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
|
|
12514
|
+
if (result.status === "ESCALATED") {
|
|
12515
|
+
this.escalations.push({
|
|
12516
|
+
raisedBy: `T2_${section.sectionId}`,
|
|
12517
|
+
sectionId: section.sectionId,
|
|
12518
|
+
attempted: result.issues,
|
|
12519
|
+
blocker: result.issues.join("; "),
|
|
12520
|
+
needs: "Human review required"
|
|
12521
|
+
});
|
|
12522
|
+
}
|
|
12523
|
+
return result;
|
|
12524
|
+
} catch (err) {
|
|
12525
|
+
return {
|
|
12117
12526
|
sectionId: section.sectionId,
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12527
|
+
sectionTitle: section.sectionTitle,
|
|
12528
|
+
status: "FAILED",
|
|
12529
|
+
t3Results: [],
|
|
12530
|
+
sectionSummary: "",
|
|
12531
|
+
issues: [err instanceof Error ? err.message : String(err)]
|
|
12532
|
+
};
|
|
12122
12533
|
}
|
|
12123
|
-
}
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
resultMap.set(id, result);
|
|
12134
|
-
completedSections++;
|
|
12135
|
-
for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
|
|
12136
|
-
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
12137
|
-
}
|
|
12138
|
-
};
|
|
12139
|
-
if (this.router.getT3ExecutionMode?.() === "sequential") {
|
|
12140
|
-
for (const id of readyIds) {
|
|
12141
|
-
await runOne(id);
|
|
12534
|
+
},
|
|
12535
|
+
onNodeComplete: (node, result) => {
|
|
12536
|
+
resultMap.set(node.id, result);
|
|
12537
|
+
completedSections++;
|
|
12538
|
+
this.emit("section:complete", {
|
|
12539
|
+
id: node.id,
|
|
12540
|
+
title: result.sectionTitle,
|
|
12541
|
+
status: result.status,
|
|
12542
|
+
output: result.sectionSummary
|
|
12543
|
+
});
|
|
12142
12544
|
}
|
|
12143
|
-
}
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12545
|
+
},
|
|
12546
|
+
{
|
|
12547
|
+
// Cross-section concurrency respects the same t3Execution mode as the
|
|
12548
|
+
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
12549
|
+
// serialized T3 workers within a single section while every section's
|
|
12550
|
+
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
12551
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
|
|
12148
12552
|
}
|
|
12149
|
-
|
|
12150
|
-
await
|
|
12553
|
+
);
|
|
12554
|
+
await scheduler.run();
|
|
12151
12555
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
12152
12556
|
}
|
|
12153
12557
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
@@ -12770,6 +13174,700 @@ function buildMediaTools(deps, readFile) {
|
|
|
12770
13174
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
12771
13175
|
return tools;
|
|
12772
13176
|
}
|
|
13177
|
+
var WorkspaceSandboxError = class extends Error {
|
|
13178
|
+
constructor(attempted, workspaceRoot) {
|
|
13179
|
+
super(`Refusing access to "${attempted}" \u2014 outside workspace root "${workspaceRoot}".`);
|
|
13180
|
+
this.name = "WorkspaceSandboxError";
|
|
13181
|
+
}
|
|
13182
|
+
};
|
|
13183
|
+
function resolveInWorkspace(workspaceRoot, input) {
|
|
13184
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
13185
|
+
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
13186
|
+
}
|
|
13187
|
+
const root = path34.resolve(workspaceRoot);
|
|
13188
|
+
const abs = path34.isAbsolute(input) ? path34.resolve(input) : path34.resolve(root, input);
|
|
13189
|
+
const rel = path34.relative(root, abs);
|
|
13190
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
13191
|
+
throw new WorkspaceSandboxError(input, root);
|
|
13192
|
+
}
|
|
13193
|
+
try {
|
|
13194
|
+
const real = fs31.realpathSync(abs);
|
|
13195
|
+
const realRel = path34.relative(root, real);
|
|
13196
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34.isAbsolute(realRel))) {
|
|
13197
|
+
throw new WorkspaceSandboxError(input, root);
|
|
13198
|
+
}
|
|
13199
|
+
} catch (e) {
|
|
13200
|
+
if (e instanceof WorkspaceSandboxError) throw e;
|
|
13201
|
+
}
|
|
13202
|
+
return abs;
|
|
13203
|
+
}
|
|
13204
|
+
|
|
13205
|
+
// src/core/documents/blocks.ts
|
|
13206
|
+
function fileExt(name) {
|
|
13207
|
+
const base = name.split(/[\\/]/).pop() ?? name;
|
|
13208
|
+
const dot = base.lastIndexOf(".");
|
|
13209
|
+
return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
13210
|
+
}
|
|
13211
|
+
function parseDelimited(text, name = "") {
|
|
13212
|
+
const delim = fileExt(name) === "tsv" || text.indexOf(" ") !== -1 && text.indexOf(",") === -1 ? " " : ",";
|
|
13213
|
+
const rows = [];
|
|
13214
|
+
let row = [];
|
|
13215
|
+
let cell = "";
|
|
13216
|
+
let quoted = false;
|
|
13217
|
+
for (let i = 0; i < text.length; i++) {
|
|
13218
|
+
const ch = text[i];
|
|
13219
|
+
if (quoted) {
|
|
13220
|
+
if (ch === '"') {
|
|
13221
|
+
if (text[i + 1] === '"') {
|
|
13222
|
+
cell += '"';
|
|
13223
|
+
i++;
|
|
13224
|
+
} else quoted = false;
|
|
13225
|
+
} else cell += ch;
|
|
13226
|
+
} else if (ch === '"') {
|
|
13227
|
+
quoted = true;
|
|
13228
|
+
} else if (ch === delim) {
|
|
13229
|
+
row.push(cell);
|
|
13230
|
+
cell = "";
|
|
13231
|
+
} else if (ch === "\n") {
|
|
13232
|
+
row.push(cell);
|
|
13233
|
+
rows.push(row);
|
|
13234
|
+
row = [];
|
|
13235
|
+
cell = "";
|
|
13236
|
+
} else if (ch === "\r") ; else {
|
|
13237
|
+
cell += ch;
|
|
13238
|
+
}
|
|
13239
|
+
if (rows.length > 5e3) break;
|
|
13240
|
+
}
|
|
13241
|
+
if (cell !== "" || row.length) {
|
|
13242
|
+
row.push(cell);
|
|
13243
|
+
rows.push(row);
|
|
13244
|
+
}
|
|
13245
|
+
return rows.filter((r) => r.length > 1 || (r[0] ?? "") !== "");
|
|
13246
|
+
}
|
|
13247
|
+
var CHART_ALIASES = {
|
|
13248
|
+
bar: "bar",
|
|
13249
|
+
column: "bar",
|
|
13250
|
+
bars: "bar",
|
|
13251
|
+
barchart: "bar",
|
|
13252
|
+
horizontalbar: "bar",
|
|
13253
|
+
line: "line",
|
|
13254
|
+
lines: "line",
|
|
13255
|
+
linechart: "line",
|
|
13256
|
+
pie: "pie",
|
|
13257
|
+
piechart: "pie",
|
|
13258
|
+
doughnut: "doughnut",
|
|
13259
|
+
donut: "doughnut",
|
|
13260
|
+
area: "area",
|
|
13261
|
+
areachart: "area",
|
|
13262
|
+
scatter: "scatter",
|
|
13263
|
+
xy: "scatter"
|
|
13264
|
+
};
|
|
13265
|
+
function chartKind(raw) {
|
|
13266
|
+
return CHART_ALIASES[raw.trim().toLowerCase().replace(/[\s_-]/g, "")] ?? null;
|
|
13267
|
+
}
|
|
13268
|
+
function toNumber(cell) {
|
|
13269
|
+
const cleaned = cell.replace(/[$£€¥,\s%]/g, "").replace(/^\((.*)\)$/, "-$1");
|
|
13270
|
+
const n = Number.parseFloat(cleaned);
|
|
13271
|
+
return Number.isFinite(n) ? n : 0;
|
|
13272
|
+
}
|
|
13273
|
+
function parseChartSpec(kind, body) {
|
|
13274
|
+
const lines = body.replace(/\r\n/g, "\n").split("\n");
|
|
13275
|
+
let title;
|
|
13276
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
13277
|
+
const titleMatch = lines[0]?.match(/^\s*title\s*:\s*(.+)$/i);
|
|
13278
|
+
if (titleMatch) {
|
|
13279
|
+
title = titleMatch[1].trim();
|
|
13280
|
+
lines.shift();
|
|
13281
|
+
}
|
|
13282
|
+
const rows = parseDelimited(lines.join("\n").trim());
|
|
13283
|
+
if (rows.length < 2) return null;
|
|
13284
|
+
const header = rows[0].map((c) => c.trim());
|
|
13285
|
+
if (header.length < 2) return null;
|
|
13286
|
+
const dataRows = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ""));
|
|
13287
|
+
if (!dataRows.length) return null;
|
|
13288
|
+
const categories = dataRows.map((r, i) => (r[0] ?? "").trim() || `Item ${i + 1}`);
|
|
13289
|
+
const series = header.slice(1).map((name, col2) => ({
|
|
13290
|
+
name: name || `Series ${col2 + 1}`,
|
|
13291
|
+
values: dataRows.map((r) => toNumber((r[col2 + 1] ?? "").trim()))
|
|
13292
|
+
}));
|
|
13293
|
+
if (!series.length) return null;
|
|
13294
|
+
return {
|
|
13295
|
+
kind,
|
|
13296
|
+
...title ? { title } : {},
|
|
13297
|
+
categoryName: header[0] || "Category",
|
|
13298
|
+
categories,
|
|
13299
|
+
series
|
|
13300
|
+
};
|
|
13301
|
+
}
|
|
13302
|
+
function chartToTableRows(spec) {
|
|
13303
|
+
return [
|
|
13304
|
+
[spec.categoryName, ...spec.series.map((s) => s.name)],
|
|
13305
|
+
...spec.categories.map((cat, i) => [cat, ...spec.series.map((s) => String(s.values[i] ?? 0))])
|
|
13306
|
+
];
|
|
13307
|
+
}
|
|
13308
|
+
function parseBlocks(md) {
|
|
13309
|
+
const out = [];
|
|
13310
|
+
const lines = md.replace(/\r\n/g, "\n").split("\n");
|
|
13311
|
+
let i = 0;
|
|
13312
|
+
let ordinal = 0;
|
|
13313
|
+
const reset = () => {
|
|
13314
|
+
ordinal = 0;
|
|
13315
|
+
};
|
|
13316
|
+
while (i < lines.length) {
|
|
13317
|
+
const line = lines[i] ?? "";
|
|
13318
|
+
const fence = line.match(/^```+\s*(.*)$/);
|
|
13319
|
+
if (fence) {
|
|
13320
|
+
const info = (fence[1] ?? "").trim();
|
|
13321
|
+
i++;
|
|
13322
|
+
const code = [];
|
|
13323
|
+
while (i < lines.length && !/^```/.test(lines[i] ?? "")) {
|
|
13324
|
+
code.push(lines[i] ?? "");
|
|
13325
|
+
i++;
|
|
13326
|
+
}
|
|
13327
|
+
i++;
|
|
13328
|
+
const chartInfo = info.match(/^chart\s*:\s*(.+)$/i);
|
|
13329
|
+
const kind = chartInfo ? chartKind(chartInfo[1]) : null;
|
|
13330
|
+
const spec = kind ? parseChartSpec(kind, code.join("\n")) : null;
|
|
13331
|
+
out.push(spec ? { t: "chart", spec } : { t: "code", lines: code });
|
|
13332
|
+
reset();
|
|
13333
|
+
continue;
|
|
13334
|
+
}
|
|
13335
|
+
if (!line.trim()) {
|
|
13336
|
+
i++;
|
|
13337
|
+
reset();
|
|
13338
|
+
continue;
|
|
13339
|
+
}
|
|
13340
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
13341
|
+
if (h) {
|
|
13342
|
+
out.push({ t: "heading", level: h[1].length, text: h[2] });
|
|
13343
|
+
i++;
|
|
13344
|
+
reset();
|
|
13345
|
+
continue;
|
|
13346
|
+
}
|
|
13347
|
+
if (/^\s*([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
13348
|
+
out.push({ t: "hr" });
|
|
13349
|
+
i++;
|
|
13350
|
+
reset();
|
|
13351
|
+
continue;
|
|
13352
|
+
}
|
|
13353
|
+
const li = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
13354
|
+
if (li) {
|
|
13355
|
+
const ordered = /\d/.test(li[2]);
|
|
13356
|
+
ordinal = ordered ? ordinal + 1 : 0;
|
|
13357
|
+
out.push({ t: "bullet", ordered, index: ordered ? ordinal : 0, text: li[3] });
|
|
13358
|
+
i++;
|
|
13359
|
+
continue;
|
|
13360
|
+
}
|
|
13361
|
+
if (/^>\s?/.test(line)) {
|
|
13362
|
+
out.push({ t: "quote", text: line.replace(/^>\s?/, "") });
|
|
13363
|
+
i++;
|
|
13364
|
+
reset();
|
|
13365
|
+
continue;
|
|
13366
|
+
}
|
|
13367
|
+
if (/^\|.*\|/.test(line)) {
|
|
13368
|
+
const rows = [];
|
|
13369
|
+
while (i < lines.length && /^\|.*\|/.test(lines[i] ?? "")) {
|
|
13370
|
+
const r = lines[i];
|
|
13371
|
+
if (!/^\|[\s:|-]+\|?\s*$/.test(r)) rows.push(r.replace(/^\||\|\s*$/g, "").split("|").map((c) => c.trim()));
|
|
13372
|
+
i++;
|
|
13373
|
+
}
|
|
13374
|
+
out.push({ t: "table", rows });
|
|
13375
|
+
reset();
|
|
13376
|
+
continue;
|
|
13377
|
+
}
|
|
13378
|
+
out.push({ t: "para", text: line });
|
|
13379
|
+
i++;
|
|
13380
|
+
reset();
|
|
13381
|
+
}
|
|
13382
|
+
return out;
|
|
13383
|
+
}
|
|
13384
|
+
var IMAGE_LINE = /^!\[([^\]]*)\]\(([^)]+)\)$/;
|
|
13385
|
+
function matchImageLine(s) {
|
|
13386
|
+
const m = s.trim().match(IMAGE_LINE);
|
|
13387
|
+
return m ? { alt: m[1].trim(), url: m[2].trim() } : null;
|
|
13388
|
+
}
|
|
13389
|
+
function sniffImage(b) {
|
|
13390
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
13391
|
+
if (b.length > 24 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
|
|
13392
|
+
return { type: "png", mime: "image/png", width: dv.getUint32(16), height: dv.getUint32(20) };
|
|
13393
|
+
}
|
|
13394
|
+
if (b.length > 10 && b[0] === 71 && b[1] === 73 && b[2] === 70) {
|
|
13395
|
+
return { type: "gif", mime: "image/gif", width: dv.getUint16(6, true), height: dv.getUint16(8, true) };
|
|
13396
|
+
}
|
|
13397
|
+
if (b.length > 26 && b[0] === 66 && b[1] === 77) {
|
|
13398
|
+
return { type: "bmp", mime: "image/bmp", width: Math.abs(dv.getInt32(18, true)), height: Math.abs(dv.getInt32(22, true)) };
|
|
13399
|
+
}
|
|
13400
|
+
if (b.length > 4 && b[0] === 255 && b[1] === 216) {
|
|
13401
|
+
let i = 2;
|
|
13402
|
+
while (i + 9 < b.length) {
|
|
13403
|
+
if (b[i] !== 255) {
|
|
13404
|
+
i++;
|
|
13405
|
+
continue;
|
|
13406
|
+
}
|
|
13407
|
+
const marker = b[i + 1];
|
|
13408
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
13409
|
+
return { type: "jpg", mime: "image/jpeg", height: dv.getUint16(i + 5), width: dv.getUint16(i + 7) };
|
|
13410
|
+
}
|
|
13411
|
+
i += 2 + dv.getUint16(i + 2);
|
|
13412
|
+
}
|
|
13413
|
+
}
|
|
13414
|
+
return null;
|
|
13415
|
+
}
|
|
13416
|
+
function bytesToBase64(bytes) {
|
|
13417
|
+
const g = globalThis;
|
|
13418
|
+
if (g.Buffer) return g.Buffer.from(bytes).toString("base64");
|
|
13419
|
+
const CHUNK = 32768;
|
|
13420
|
+
let binary = "";
|
|
13421
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
13422
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
13423
|
+
}
|
|
13424
|
+
return g.btoa(binary);
|
|
13425
|
+
}
|
|
13426
|
+
function stripInline(s) {
|
|
13427
|
+
return s.replace(/\*\*(.+?)\*\*/g, "$1").replace(/(^|[^*])\*(?!\s)(.+?)\*/g, "$1$2").replace(/`([^`]+)`/g, "$1").replace(/\[(.+?)\]\((.+?)\)/g, "$1 ($2)");
|
|
13428
|
+
}
|
|
13429
|
+
function inlineRuns(text) {
|
|
13430
|
+
const acc = [];
|
|
13431
|
+
const re = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]{1,500}\]\([^)]{1,2000}\)|\*[^*\s][^*]*\*)/g;
|
|
13432
|
+
let last = 0;
|
|
13433
|
+
let m;
|
|
13434
|
+
while ((m = re.exec(text)) !== null) {
|
|
13435
|
+
if (m.index > last) acc.push({ text: text.slice(last, m.index) });
|
|
13436
|
+
const tok = m[0];
|
|
13437
|
+
if (tok.startsWith("**")) acc.push({ text: tok.slice(2, -2), bold: true });
|
|
13438
|
+
else if (tok.startsWith("`")) acc.push({ text: tok.slice(1, -1), code: true });
|
|
13439
|
+
else if (tok.startsWith("[")) {
|
|
13440
|
+
const l = tok.match(/\[([^\]]{1,500})\]\(([^)]{1,2000})\)/);
|
|
13441
|
+
acc.push({ text: l ? `${l[1]} (${l[2]})` : tok });
|
|
13442
|
+
} else acc.push({ text: tok.slice(1, -1), italic: true });
|
|
13443
|
+
last = re.lastIndex;
|
|
13444
|
+
}
|
|
13445
|
+
if (last < text.length) acc.push({ text: text.slice(last) });
|
|
13446
|
+
return acc.length ? acc : [{ text }];
|
|
13447
|
+
}
|
|
13448
|
+
function splitSlides(md) {
|
|
13449
|
+
const norm = md.replace(/\r\n/g, "\n").trim();
|
|
13450
|
+
let chunks = norm.split(/\n\s*-{3,}\s*\n/);
|
|
13451
|
+
if (chunks.length === 1) {
|
|
13452
|
+
const lines = norm.split("\n");
|
|
13453
|
+
const sections = [];
|
|
13454
|
+
let cur = [];
|
|
13455
|
+
for (const ln of lines) {
|
|
13456
|
+
if (/^#{1,2}\s+/.test(ln) && cur.some((l) => l.trim())) {
|
|
13457
|
+
sections.push(cur.join("\n"));
|
|
13458
|
+
cur = [];
|
|
13459
|
+
}
|
|
13460
|
+
cur.push(ln);
|
|
13461
|
+
}
|
|
13462
|
+
if (cur.length) sections.push(cur.join("\n"));
|
|
13463
|
+
if (sections.length > 1) chunks = sections;
|
|
13464
|
+
}
|
|
13465
|
+
return chunks.map(parseSlide).filter((s) => s.title || s.body.length || s.images.length || s.charts.length);
|
|
13466
|
+
}
|
|
13467
|
+
function parseSlide(chunk) {
|
|
13468
|
+
let title = "";
|
|
13469
|
+
const body = [];
|
|
13470
|
+
const images = [];
|
|
13471
|
+
const charts = [];
|
|
13472
|
+
const lines = chunk.split("\n");
|
|
13473
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13474
|
+
const raw = lines[i] ?? "";
|
|
13475
|
+
const fence = raw.match(/^[ \t]*```+[ \t]*chart[ \t]*:(.*)/i);
|
|
13476
|
+
if (fence) {
|
|
13477
|
+
const kind = chartKind(fence[1]);
|
|
13478
|
+
const bodyLines = [];
|
|
13479
|
+
i++;
|
|
13480
|
+
while (i < lines.length && !/^\s*```/.test(lines[i] ?? "")) {
|
|
13481
|
+
bodyLines.push(lines[i] ?? "");
|
|
13482
|
+
i++;
|
|
13483
|
+
}
|
|
13484
|
+
const spec = kind ? parseChartSpec(kind, bodyLines.join("\n")) : null;
|
|
13485
|
+
if (spec) charts.push(spec);
|
|
13486
|
+
else for (const bl of bodyLines) if (bl.trim()) body.push(stripInline(bl.trim()));
|
|
13487
|
+
continue;
|
|
13488
|
+
}
|
|
13489
|
+
const line = raw.trim();
|
|
13490
|
+
if (!line) continue;
|
|
13491
|
+
const h = line.match(/^#{1,6}[ \t]+(.*)/);
|
|
13492
|
+
if (h) {
|
|
13493
|
+
if (!title) title = stripInline(h[1]);
|
|
13494
|
+
else body.push(stripInline(h[1]));
|
|
13495
|
+
continue;
|
|
13496
|
+
}
|
|
13497
|
+
const img = matchImageLine(line);
|
|
13498
|
+
if (img) {
|
|
13499
|
+
images.push(img);
|
|
13500
|
+
continue;
|
|
13501
|
+
}
|
|
13502
|
+
const li = line.match(/^([-*+]|\d+[.)])[ \t]+(.*)/);
|
|
13503
|
+
if (li) {
|
|
13504
|
+
const bulletImg = matchImageLine(li[2]);
|
|
13505
|
+
if (bulletImg) {
|
|
13506
|
+
images.push(bulletImg);
|
|
13507
|
+
continue;
|
|
13508
|
+
}
|
|
13509
|
+
body.push(stripInline(li[2]));
|
|
13510
|
+
continue;
|
|
13511
|
+
}
|
|
13512
|
+
if (/^>\s?/.test(line)) {
|
|
13513
|
+
body.push(stripInline(line.replace(/^>\s?/, "")));
|
|
13514
|
+
continue;
|
|
13515
|
+
}
|
|
13516
|
+
body.push(stripInline(line));
|
|
13517
|
+
}
|
|
13518
|
+
return { title, body, images, charts };
|
|
13519
|
+
}
|
|
13520
|
+
function extractCharts(source) {
|
|
13521
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
13522
|
+
const charts = [];
|
|
13523
|
+
const rest = [];
|
|
13524
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13525
|
+
const fence = lines[i].match(/^[ \t]*```+[ \t]*chart[ \t]*:(.*)/i);
|
|
13526
|
+
if (!fence) {
|
|
13527
|
+
rest.push(lines[i]);
|
|
13528
|
+
continue;
|
|
13529
|
+
}
|
|
13530
|
+
const kind = chartKind(fence[1]);
|
|
13531
|
+
const body = [];
|
|
13532
|
+
i++;
|
|
13533
|
+
while (i < lines.length && !/^\s*```/.test(lines[i] ?? "")) {
|
|
13534
|
+
body.push(lines[i] ?? "");
|
|
13535
|
+
i++;
|
|
13536
|
+
}
|
|
13537
|
+
const spec = kind ? parseChartSpec(kind, body.join("\n")) : null;
|
|
13538
|
+
if (spec) charts.push(spec);
|
|
13539
|
+
else rest.push(...body);
|
|
13540
|
+
}
|
|
13541
|
+
return { charts, rest: rest.join("\n") };
|
|
13542
|
+
}
|
|
13543
|
+
|
|
13544
|
+
// src/core/documents/render.ts
|
|
13545
|
+
function isDocumentFormat(ext) {
|
|
13546
|
+
return ext === "docx" || ext === "pptx" || ext === "xlsx";
|
|
13547
|
+
}
|
|
13548
|
+
async function loadImage(ref, load) {
|
|
13549
|
+
if (!load) return null;
|
|
13550
|
+
try {
|
|
13551
|
+
const bytes = await load(ref.url);
|
|
13552
|
+
if (!bytes || !bytes.length) return null;
|
|
13553
|
+
const info = sniffImage(bytes);
|
|
13554
|
+
if (!info || info.width <= 0 || info.height <= 0) return null;
|
|
13555
|
+
return { ...info, bytes, alt: ref.alt };
|
|
13556
|
+
} catch {
|
|
13557
|
+
return null;
|
|
13558
|
+
}
|
|
13559
|
+
}
|
|
13560
|
+
async function renderXlsx(source, opts = {}) {
|
|
13561
|
+
const XLSX = await import('xlsx');
|
|
13562
|
+
const { charts, rest } = extractCharts(source);
|
|
13563
|
+
const name = opts.name ?? "";
|
|
13564
|
+
const rows = parseDelimited(rest, name.toLowerCase().endsWith(".tsv") ? "x.tsv" : "x.csv");
|
|
13565
|
+
const wb = XLSX.utils.book_new();
|
|
13566
|
+
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(rows.length ? rows : [[""]]), "Sheet1");
|
|
13567
|
+
const used = /* @__PURE__ */ new Set(["Sheet1"]);
|
|
13568
|
+
charts.forEach((spec, i) => {
|
|
13569
|
+
const base = (spec.title || `Chart ${i + 1}`).replace(/[:\\/?*[\]]/g, " ").trim().slice(0, 28) || `Chart ${i + 1}`;
|
|
13570
|
+
let sheetName = base;
|
|
13571
|
+
let n = 2;
|
|
13572
|
+
while (used.has(sheetName)) sheetName = `${base} ${n++}`.slice(0, 31);
|
|
13573
|
+
used.add(sheetName);
|
|
13574
|
+
const aoa = [
|
|
13575
|
+
[spec.categoryName, ...spec.series.map((s) => s.name)],
|
|
13576
|
+
...spec.categories.map((cat, r) => [cat, ...spec.series.map((s) => s.values[r] ?? 0)])
|
|
13577
|
+
];
|
|
13578
|
+
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sheetName);
|
|
13579
|
+
});
|
|
13580
|
+
return new Uint8Array(XLSX.write(wb, { type: "array", bookType: "xlsx" }));
|
|
13581
|
+
}
|
|
13582
|
+
var DOCX_MAX_W = 600;
|
|
13583
|
+
var DOCX_MAX_H = 864;
|
|
13584
|
+
async function renderDocx(md, opts = {}) {
|
|
13585
|
+
const d = await import('docx');
|
|
13586
|
+
const {
|
|
13587
|
+
Document,
|
|
13588
|
+
Packer,
|
|
13589
|
+
Paragraph,
|
|
13590
|
+
TextRun,
|
|
13591
|
+
ImageRun,
|
|
13592
|
+
HeadingLevel,
|
|
13593
|
+
Table,
|
|
13594
|
+
TableRow,
|
|
13595
|
+
TableCell,
|
|
13596
|
+
WidthType,
|
|
13597
|
+
BorderStyle,
|
|
13598
|
+
AlignmentType
|
|
13599
|
+
} = d;
|
|
13600
|
+
const HEADINGS = [
|
|
13601
|
+
HeadingLevel.HEADING_1,
|
|
13602
|
+
HeadingLevel.HEADING_2,
|
|
13603
|
+
HeadingLevel.HEADING_3,
|
|
13604
|
+
HeadingLevel.HEADING_4,
|
|
13605
|
+
HeadingLevel.HEADING_5,
|
|
13606
|
+
HeadingLevel.HEADING_6
|
|
13607
|
+
];
|
|
13608
|
+
const runs = (text) => inlineRuns(text).map((r) => new TextRun({
|
|
13609
|
+
text: r.text,
|
|
13610
|
+
...r.bold ? { bold: true } : {},
|
|
13611
|
+
...r.italic ? { italics: true } : {},
|
|
13612
|
+
...r.code ? { font: "Courier New" } : {}
|
|
13613
|
+
}));
|
|
13614
|
+
const tableOf = (rows) => new Table({
|
|
13615
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
13616
|
+
rows: rows.map((r) => new TableRow({
|
|
13617
|
+
children: r.map((c) => new TableCell({ children: [new Paragraph({ children: runs(c) })] }))
|
|
13618
|
+
}))
|
|
13619
|
+
});
|
|
13620
|
+
const imageParagraph = (img) => {
|
|
13621
|
+
const scale = Math.min(1, DOCX_MAX_W / img.width, DOCX_MAX_H / img.height);
|
|
13622
|
+
return new Paragraph({
|
|
13623
|
+
children: [new ImageRun({
|
|
13624
|
+
type: img.type,
|
|
13625
|
+
data: img.bytes,
|
|
13626
|
+
transformation: { width: Math.round(img.width * scale), height: Math.round(img.height * scale) },
|
|
13627
|
+
...img.alt ? { altText: { name: img.alt, description: img.alt, title: img.alt } } : {}
|
|
13628
|
+
})]
|
|
13629
|
+
});
|
|
13630
|
+
};
|
|
13631
|
+
const asImage = async (text) => {
|
|
13632
|
+
const ref = matchImageLine(text);
|
|
13633
|
+
return ref ? await loadImage(ref, opts.loadImageBytes) : null;
|
|
13634
|
+
};
|
|
13635
|
+
const children = [];
|
|
13636
|
+
for (const b of parseBlocks(md)) {
|
|
13637
|
+
switch (b.t) {
|
|
13638
|
+
case "heading":
|
|
13639
|
+
children.push(new Paragraph({ heading: HEADINGS[Math.min(b.level, 6) - 1], children: runs(b.text) }));
|
|
13640
|
+
break;
|
|
13641
|
+
case "para": {
|
|
13642
|
+
const img = await asImage(b.text);
|
|
13643
|
+
children.push(img ? imageParagraph(img) : new Paragraph({ children: runs(b.text) }));
|
|
13644
|
+
break;
|
|
13645
|
+
}
|
|
13646
|
+
case "bullet": {
|
|
13647
|
+
const img = await asImage(b.text);
|
|
13648
|
+
children.push(img ? imageParagraph(img) : new Paragraph({
|
|
13649
|
+
children: runs(b.text),
|
|
13650
|
+
...b.ordered ? { numbering: { reference: "ol", level: 0 } } : { bullet: { level: 0 } }
|
|
13651
|
+
}));
|
|
13652
|
+
break;
|
|
13653
|
+
}
|
|
13654
|
+
case "code":
|
|
13655
|
+
for (const cl of b.lines) {
|
|
13656
|
+
children.push(new Paragraph({ children: [new TextRun({ text: cl || " ", font: "Courier New", size: 18 })] }));
|
|
13657
|
+
}
|
|
13658
|
+
break;
|
|
13659
|
+
case "quote":
|
|
13660
|
+
children.push(new Paragraph({ indent: { left: 360 }, children: [new TextRun({ text: stripInline(b.text), italics: true })] }));
|
|
13661
|
+
break;
|
|
13662
|
+
case "table":
|
|
13663
|
+
children.push(tableOf(b.rows));
|
|
13664
|
+
break;
|
|
13665
|
+
case "chart":
|
|
13666
|
+
if (b.spec.title) {
|
|
13667
|
+
children.push(new Paragraph({ children: [new TextRun({ text: b.spec.title, bold: true })] }));
|
|
13668
|
+
}
|
|
13669
|
+
children.push(tableOf(chartToTableRows(b.spec)));
|
|
13670
|
+
break;
|
|
13671
|
+
case "hr":
|
|
13672
|
+
children.push(new Paragraph({
|
|
13673
|
+
children: [],
|
|
13674
|
+
border: { bottom: { color: "CCCCCC", space: 1, style: BorderStyle.SINGLE, size: 6 } }
|
|
13675
|
+
}));
|
|
13676
|
+
break;
|
|
13677
|
+
}
|
|
13678
|
+
}
|
|
13679
|
+
const doc = new Document({
|
|
13680
|
+
numbering: {
|
|
13681
|
+
config: [{ reference: "ol", levels: [{ level: 0, format: "decimal", text: "%1.", alignment: AlignmentType.START }] }]
|
|
13682
|
+
},
|
|
13683
|
+
sections: [{ children: children.length ? children : [new Paragraph({ children: [new TextRun("")] })] }]
|
|
13684
|
+
});
|
|
13685
|
+
return new Uint8Array(await Packer.toArrayBuffer(doc));
|
|
13686
|
+
}
|
|
13687
|
+
function pptxConstructor(mod) {
|
|
13688
|
+
let c = mod;
|
|
13689
|
+
for (let i = 0; i < 3 && c && typeof c !== "function"; i++) c = c.default;
|
|
13690
|
+
if (typeof c !== "function") throw new Error("pptxgenjs did not export a constructor.");
|
|
13691
|
+
return c;
|
|
13692
|
+
}
|
|
13693
|
+
var SLIDE_H = 7.5;
|
|
13694
|
+
var CONTENT_X = 0.6;
|
|
13695
|
+
var CONTENT_W = 12.1;
|
|
13696
|
+
var CONTENT_BOTTOM = 0.4;
|
|
13697
|
+
async function renderPptx(md, opts = {}) {
|
|
13698
|
+
const PptxGen = pptxConstructor(await import('pptxgenjs'));
|
|
13699
|
+
const pptx = new PptxGen();
|
|
13700
|
+
pptx.layout = "LAYOUT_WIDE";
|
|
13701
|
+
const CHART_TYPES = {
|
|
13702
|
+
bar: pptx.ChartType["bar"],
|
|
13703
|
+
line: pptx.ChartType["line"],
|
|
13704
|
+
pie: pptx.ChartType["pie"],
|
|
13705
|
+
doughnut: pptx.ChartType["doughnut"],
|
|
13706
|
+
area: pptx.ChartType["area"],
|
|
13707
|
+
scatter: pptx.ChartType["scatter"]
|
|
13708
|
+
};
|
|
13709
|
+
const slides = splitSlides(md);
|
|
13710
|
+
const blank = { title: "", body: [""], images: [], charts: [] };
|
|
13711
|
+
for (const s of slides.length ? slides : [blank]) {
|
|
13712
|
+
const slide = pptx.addSlide();
|
|
13713
|
+
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" });
|
|
13714
|
+
const top = s.title ? 1.5 : 0.5;
|
|
13715
|
+
const pics = (await Promise.all(s.images.map((r) => loadImage(r, opts.loadImageBytes)))).filter((p) => p !== null);
|
|
13716
|
+
const visuals = [
|
|
13717
|
+
...s.charts.map((spec) => ({ kind: "chart", spec })),
|
|
13718
|
+
...pics.map((img) => ({ kind: "image", img }))
|
|
13719
|
+
];
|
|
13720
|
+
const bodyH = visuals.length ? 2.2 : 5.4;
|
|
13721
|
+
if (s.body.length) {
|
|
13722
|
+
slide.addText(
|
|
13723
|
+
s.body.map((t) => ({ text: t, options: { bullet: true, breakLine: true } })),
|
|
13724
|
+
{ x: CONTENT_X, y: top, w: CONTENT_W, h: bodyH, fontSize: 16, color: "374151", valign: "top" }
|
|
13725
|
+
);
|
|
13726
|
+
}
|
|
13727
|
+
if (visuals.length) {
|
|
13728
|
+
const y = top + (s.body.length ? bodyH + 0.2 : 0);
|
|
13729
|
+
const h = Math.max(1, SLIDE_H - CONTENT_BOTTOM - y);
|
|
13730
|
+
const gap = 0.3;
|
|
13731
|
+
const w = (CONTENT_W - gap * (visuals.length - 1)) / visuals.length;
|
|
13732
|
+
visuals.forEach((v, i) => {
|
|
13733
|
+
const x = CONTENT_X + i * (w + gap);
|
|
13734
|
+
if (v.kind === "image") {
|
|
13735
|
+
slide.addImage({
|
|
13736
|
+
data: `data:${v.img.mime};base64,${bytesToBase64(v.img.bytes)}`,
|
|
13737
|
+
x,
|
|
13738
|
+
y,
|
|
13739
|
+
w,
|
|
13740
|
+
h,
|
|
13741
|
+
// 'contain' keeps the aspect ratio inside the box — a stretched
|
|
13742
|
+
// render looks worse than a smaller correct one.
|
|
13743
|
+
sizing: { type: "contain", w, h },
|
|
13744
|
+
...v.img.alt ? { altText: v.img.alt } : {}
|
|
13745
|
+
});
|
|
13746
|
+
} else {
|
|
13747
|
+
slide.addChart(CHART_TYPES[v.spec.kind], chartData(v.spec), {
|
|
13748
|
+
x,
|
|
13749
|
+
y,
|
|
13750
|
+
w,
|
|
13751
|
+
h,
|
|
13752
|
+
showLegend: v.spec.series.length > 1 || v.spec.kind === "pie" || v.spec.kind === "doughnut",
|
|
13753
|
+
legendPos: "b",
|
|
13754
|
+
showTitle: !!v.spec.title,
|
|
13755
|
+
...v.spec.title ? { title: v.spec.title } : {},
|
|
13756
|
+
showValue: v.spec.kind === "pie" || v.spec.kind === "doughnut"
|
|
13757
|
+
});
|
|
13758
|
+
}
|
|
13759
|
+
});
|
|
13760
|
+
}
|
|
13761
|
+
}
|
|
13762
|
+
return new Uint8Array(await pptx.write({ outputType: "arraybuffer" }));
|
|
13763
|
+
}
|
|
13764
|
+
function chartData(spec) {
|
|
13765
|
+
const series = spec.kind === "pie" || spec.kind === "doughnut" ? spec.series.slice(0, 1) : spec.series;
|
|
13766
|
+
return series.map((s) => ({ name: s.name, labels: spec.categories, values: s.values }));
|
|
13767
|
+
}
|
|
13768
|
+
async function renderDocument(format, source, opts = {}) {
|
|
13769
|
+
if (format === "docx") return renderDocx(source, opts);
|
|
13770
|
+
if (format === "pptx") return renderPptx(source, opts);
|
|
13771
|
+
return renderXlsx(source, opts);
|
|
13772
|
+
}
|
|
13773
|
+
|
|
13774
|
+
// src/tools/generate-document.ts
|
|
13775
|
+
var SOURCE_HINT = {
|
|
13776
|
+
docx: "Markdown",
|
|
13777
|
+
pptx: "Markdown slides (separate slides with a --- rule; each slide starts with a heading)",
|
|
13778
|
+
xlsx: "CSV (one header row, then data rows)"
|
|
13779
|
+
};
|
|
13780
|
+
var GenerateDocumentTool = class extends BaseTool {
|
|
13781
|
+
/**
|
|
13782
|
+
* `readFile` is injected so the image loader is testable and so a host with a
|
|
13783
|
+
* different workspace (not plain disk) can supply its own reader.
|
|
13784
|
+
*/
|
|
13785
|
+
constructor(readFile = async (p) => new Uint8Array(await fs11.readFile(p))) {
|
|
13786
|
+
super();
|
|
13787
|
+
this.readFile = readFile;
|
|
13788
|
+
}
|
|
13789
|
+
readFile;
|
|
13790
|
+
name = "generate_document";
|
|
13791
|
+
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.";
|
|
13792
|
+
inputSchema = {
|
|
13793
|
+
type: "object",
|
|
13794
|
+
properties: {
|
|
13795
|
+
path: {
|
|
13796
|
+
type: "string",
|
|
13797
|
+
description: "Where to write the file, relative to the workspace root. The extension decides the format: .docx, .pptx or .xlsx."
|
|
13798
|
+
},
|
|
13799
|
+
content: {
|
|
13800
|
+
type: "string",
|
|
13801
|
+
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.'
|
|
13802
|
+
},
|
|
13803
|
+
format: {
|
|
13804
|
+
type: "string",
|
|
13805
|
+
description: 'Optional override: "docx", "pptx" or "xlsx". Normally inferred from the path extension.'
|
|
13806
|
+
}
|
|
13807
|
+
},
|
|
13808
|
+
required: ["path", "content"]
|
|
13809
|
+
};
|
|
13810
|
+
isDangerous() {
|
|
13811
|
+
return true;
|
|
13812
|
+
}
|
|
13813
|
+
async execute(input, options) {
|
|
13814
|
+
const filePath = String(input["path"] ?? "").trim();
|
|
13815
|
+
if (!filePath) return 'Provide a "path" for the document, e.g. "report.docx".';
|
|
13816
|
+
const content = typeof input["content"] === "string" ? input["content"] : "";
|
|
13817
|
+
if (!content.trim()) return 'Provide "content" \u2014 the Markdown (docx/pptx) or CSV (xlsx) source for the document.';
|
|
13818
|
+
const requested = String(input["format"] ?? "").trim().toLowerCase().replace(/^\./, "");
|
|
13819
|
+
const ext = requested || fileExt(filePath);
|
|
13820
|
+
if (!isDocumentFormat(ext)) {
|
|
13821
|
+
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.`;
|
|
13822
|
+
}
|
|
13823
|
+
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
13824
|
+
const embedded = [];
|
|
13825
|
+
const skipped = [];
|
|
13826
|
+
const loadImageBytes = async (url) => {
|
|
13827
|
+
if (/^(https?|ftp):\/\//i.test(url)) {
|
|
13828
|
+
skipped.push(`${url} (remote URL \u2014 download it into the workspace first)`);
|
|
13829
|
+
return null;
|
|
13830
|
+
}
|
|
13831
|
+
const local = url.replace(/^file:\/\//i, "").split("?")[0].split("#")[0];
|
|
13832
|
+
try {
|
|
13833
|
+
const bytes2 = await this.readFile(resolveInWorkspace(this.workspaceRoot, decodeURIComponent(local)));
|
|
13834
|
+
embedded.push(local);
|
|
13835
|
+
return bytes2;
|
|
13836
|
+
} catch {
|
|
13837
|
+
skipped.push(`${local} (not found in the workspace)`);
|
|
13838
|
+
return null;
|
|
13839
|
+
}
|
|
13840
|
+
};
|
|
13841
|
+
if (options.saveSnapshot) {
|
|
13842
|
+
try {
|
|
13843
|
+
const old = await fs11.readFile(absPath, "utf-8");
|
|
13844
|
+
await options.saveSnapshot(absPath, old);
|
|
13845
|
+
} catch {
|
|
13846
|
+
}
|
|
13847
|
+
}
|
|
13848
|
+
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
13849
|
+
await fs11.mkdir(path34.dirname(absPath), { recursive: true });
|
|
13850
|
+
await fs11.writeFile(absPath, bytes);
|
|
13851
|
+
const charts = countCharts(content);
|
|
13852
|
+
return [
|
|
13853
|
+
`Wrote a real ${LABEL[ext]} file to ${filePath} (${bytes.length} bytes) from ${SOURCE_HINT[ext]}.`,
|
|
13854
|
+
embedded.length ? `Embedded ${embedded.length} image(s): ${embedded.join(", ")}.` : "",
|
|
13855
|
+
// Named, not counted: a dropped image is the failure the user actually
|
|
13856
|
+
// notices, and the model can only fix it if it is told which reference died.
|
|
13857
|
+
skipped.length ? `Could NOT embed ${skipped.length} image reference(s): ${skipped.join("; ")}.` : "",
|
|
13858
|
+
charts && ext === "pptx" ? `Rendered ${charts} chart block(s) as real, editable PowerPoint charts.` : "",
|
|
13859
|
+
charts && ext === "docx" ? `Rendered ${charts} chart block(s) as data tables \u2014 Word documents cannot hold a native chart object.` : "",
|
|
13860
|
+
charts && ext === "xlsx" ? `Wrote ${charts} chart block(s) onto their own worksheets as real rows the user can chart in Excel.` : ""
|
|
13861
|
+
].filter(Boolean).join("\n");
|
|
13862
|
+
}
|
|
13863
|
+
};
|
|
13864
|
+
var LABEL = { docx: "Word", pptx: "PowerPoint", xlsx: "Excel" };
|
|
13865
|
+
function countCharts(source) {
|
|
13866
|
+
return (source.match(/^\s*```+\s*chart\s*:/gim) ?? []).length;
|
|
13867
|
+
}
|
|
13868
|
+
function buildDocumentTools(readFile) {
|
|
13869
|
+
return readFile ? [new GenerateDocumentTool(readFile)] : [];
|
|
13870
|
+
}
|
|
12773
13871
|
|
|
12774
13872
|
// src/core/run-breaker.ts
|
|
12775
13873
|
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
@@ -12918,35 +14016,6 @@ ${[e.stdout, e.stderr].filter(Boolean).join("\n").trim()}`;
|
|
|
12918
14016
|
}
|
|
12919
14017
|
}
|
|
12920
14018
|
};
|
|
12921
|
-
var WorkspaceSandboxError = class extends Error {
|
|
12922
|
-
constructor(attempted, workspaceRoot) {
|
|
12923
|
-
super(`Refusing access to "${attempted}" \u2014 outside workspace root "${workspaceRoot}".`);
|
|
12924
|
-
this.name = "WorkspaceSandboxError";
|
|
12925
|
-
}
|
|
12926
|
-
};
|
|
12927
|
-
function resolveInWorkspace(workspaceRoot, input) {
|
|
12928
|
-
if (typeof input !== "string" || input.length === 0) {
|
|
12929
|
-
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12930
|
-
}
|
|
12931
|
-
const root = path32.resolve(workspaceRoot);
|
|
12932
|
-
const abs = path32.isAbsolute(input) ? path32.resolve(input) : path32.resolve(root, input);
|
|
12933
|
-
const rel = path32.relative(root, abs);
|
|
12934
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path32.isAbsolute(rel)) {
|
|
12935
|
-
throw new WorkspaceSandboxError(input, root);
|
|
12936
|
-
}
|
|
12937
|
-
try {
|
|
12938
|
-
const real = fs29.realpathSync(abs);
|
|
12939
|
-
const realRel = path32.relative(root, real);
|
|
12940
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path32.isAbsolute(realRel))) {
|
|
12941
|
-
throw new WorkspaceSandboxError(input, root);
|
|
12942
|
-
}
|
|
12943
|
-
} catch (e) {
|
|
12944
|
-
if (e instanceof WorkspaceSandboxError) throw e;
|
|
12945
|
-
}
|
|
12946
|
-
return abs;
|
|
12947
|
-
}
|
|
12948
|
-
|
|
12949
|
-
// src/tools/file.ts
|
|
12950
14019
|
var FileReadTool = class extends BaseTool {
|
|
12951
14020
|
name = "file_read";
|
|
12952
14021
|
description = "Read the contents of a file. Returns the file content as a string.";
|
|
@@ -12964,7 +14033,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
12964
14033
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
12965
14034
|
const offset = input["offset"] ?? 1;
|
|
12966
14035
|
const limit = input["limit"];
|
|
12967
|
-
const content = await
|
|
14036
|
+
const content = await fs11.readFile(absPath, "utf-8");
|
|
12968
14037
|
const lines = content.split("\n");
|
|
12969
14038
|
const start = Math.max(0, offset - 1);
|
|
12970
14039
|
const end = limit ? start + limit : lines.length;
|
|
@@ -12993,13 +14062,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
12993
14062
|
const content = input["content"];
|
|
12994
14063
|
if (options.saveSnapshot) {
|
|
12995
14064
|
try {
|
|
12996
|
-
const oldContent = await
|
|
14065
|
+
const oldContent = await fs11.readFile(absPath, "utf-8");
|
|
12997
14066
|
await options.saveSnapshot(absPath, oldContent);
|
|
12998
14067
|
} catch {
|
|
12999
14068
|
}
|
|
13000
14069
|
}
|
|
13001
|
-
await
|
|
13002
|
-
await
|
|
14070
|
+
await fs11.mkdir(path34.dirname(absPath), { recursive: true });
|
|
14071
|
+
await fs11.writeFile(absPath, content, "utf-8");
|
|
13003
14072
|
return `Written ${content.length} characters to ${filePath}`;
|
|
13004
14073
|
}
|
|
13005
14074
|
};
|
|
@@ -13025,7 +14094,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
13025
14094
|
const oldString = input["old_string"];
|
|
13026
14095
|
const newString = input["new_string"];
|
|
13027
14096
|
const replaceAll = input["replace_all"] ?? false;
|
|
13028
|
-
const rawContent = await
|
|
14097
|
+
const rawContent = await fs11.readFile(absPath, "utf-8");
|
|
13029
14098
|
if (options.saveSnapshot) {
|
|
13030
14099
|
await options.saveSnapshot(absPath, rawContent);
|
|
13031
14100
|
}
|
|
@@ -13037,7 +14106,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
13037
14106
|
);
|
|
13038
14107
|
}
|
|
13039
14108
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
13040
|
-
await
|
|
14109
|
+
await fs11.writeFile(absPath, updated, "utf-8");
|
|
13041
14110
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
13042
14111
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
13043
14112
|
}
|
|
@@ -13060,12 +14129,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
13060
14129
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
13061
14130
|
if (options.saveSnapshot) {
|
|
13062
14131
|
try {
|
|
13063
|
-
const oldContent = await
|
|
14132
|
+
const oldContent = await fs11.readFile(absPath, "utf-8");
|
|
13064
14133
|
await options.saveSnapshot(absPath, oldContent);
|
|
13065
14134
|
} catch {
|
|
13066
14135
|
}
|
|
13067
14136
|
}
|
|
13068
|
-
await
|
|
14137
|
+
await fs11.rm(absPath, { recursive: false });
|
|
13069
14138
|
return `Deleted ${filePath}`;
|
|
13070
14139
|
}
|
|
13071
14140
|
};
|
|
@@ -13082,7 +14151,7 @@ var FileListTool = class extends BaseTool {
|
|
|
13082
14151
|
async execute(input, _options) {
|
|
13083
14152
|
const inputPath = input["path"] || ".";
|
|
13084
14153
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
13085
|
-
const entries = await
|
|
14154
|
+
const entries = await fs11.readdir(absPath, { withFileTypes: true });
|
|
13086
14155
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
13087
14156
|
}
|
|
13088
14157
|
};
|
|
@@ -13495,8 +14564,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
13495
14564
|
}
|
|
13496
14565
|
};
|
|
13497
14566
|
async function fileToImageAttachment(filePath) {
|
|
13498
|
-
const data = await
|
|
13499
|
-
const ext =
|
|
14567
|
+
const data = await fs11.readFile(filePath);
|
|
14568
|
+
const ext = path34.extname(filePath).toLowerCase();
|
|
13500
14569
|
const mimeMap = {
|
|
13501
14570
|
".jpg": "image/jpeg",
|
|
13502
14571
|
".jpeg": "image/jpeg",
|
|
@@ -13530,14 +14599,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
13530
14599
|
const filePath = input["path"];
|
|
13531
14600
|
const content = input["content"];
|
|
13532
14601
|
const title = input["title"];
|
|
13533
|
-
const dir =
|
|
13534
|
-
if (!
|
|
13535
|
-
|
|
14602
|
+
const dir = path34.dirname(filePath);
|
|
14603
|
+
if (!fs31.existsSync(dir)) {
|
|
14604
|
+
fs31.mkdirSync(dir, { recursive: true });
|
|
13536
14605
|
}
|
|
13537
14606
|
return new Promise((resolve2, reject) => {
|
|
13538
14607
|
try {
|
|
13539
14608
|
const doc = new PDFDocument({ margin: 50 });
|
|
13540
|
-
const stream =
|
|
14609
|
+
const stream = fs31.createWriteStream(filePath);
|
|
13541
14610
|
doc.pipe(stream);
|
|
13542
14611
|
if (title) {
|
|
13543
14612
|
doc.info["Title"] = title;
|
|
@@ -13615,22 +14684,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
13615
14684
|
}
|
|
13616
14685
|
cmdPrefix = NODE_CMD;
|
|
13617
14686
|
}
|
|
13618
|
-
const tmpDir =
|
|
13619
|
-
if (!
|
|
13620
|
-
|
|
14687
|
+
const tmpDir = path34.join(this.workspaceRoot, ".cascade", "tmp");
|
|
14688
|
+
if (!fs31.existsSync(tmpDir)) {
|
|
14689
|
+
fs31.mkdirSync(tmpDir, { recursive: true });
|
|
13621
14690
|
}
|
|
13622
14691
|
const extension = language === "python" ? "py" : "js";
|
|
13623
14692
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
13624
|
-
const filePath =
|
|
13625
|
-
|
|
14693
|
+
const filePath = path34.join(tmpDir, fileName);
|
|
14694
|
+
fs31.writeFileSync(filePath, code, "utf-8");
|
|
13626
14695
|
const execArgs = [filePath, ...args];
|
|
13627
14696
|
return new Promise((resolve2) => {
|
|
13628
14697
|
const startMs = Date.now();
|
|
13629
14698
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
13630
14699
|
const duration = Date.now() - startMs;
|
|
13631
14700
|
try {
|
|
13632
|
-
if (
|
|
13633
|
-
|
|
14701
|
+
if (fs31.existsSync(filePath)) {
|
|
14702
|
+
fs31.unlinkSync(filePath);
|
|
13634
14703
|
}
|
|
13635
14704
|
} catch (cleanupErr) {
|
|
13636
14705
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -13942,7 +15011,7 @@ var GlobTool = class extends BaseTool {
|
|
|
13942
15011
|
};
|
|
13943
15012
|
async execute(input, _options) {
|
|
13944
15013
|
const pattern = input["pattern"];
|
|
13945
|
-
const searchPath = input["path"] ?
|
|
15014
|
+
const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
13946
15015
|
const matches = await glob(pattern, {
|
|
13947
15016
|
cwd: searchPath,
|
|
13948
15017
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -13955,7 +15024,7 @@ var GlobTool = class extends BaseTool {
|
|
|
13955
15024
|
const withMtime = await Promise.all(
|
|
13956
15025
|
matches.map(async (rel) => {
|
|
13957
15026
|
try {
|
|
13958
|
-
const stat = await
|
|
15027
|
+
const stat = await fs11.stat(path34.join(searchPath, rel));
|
|
13959
15028
|
return { rel, mtime: stat.mtimeMs };
|
|
13960
15029
|
} catch {
|
|
13961
15030
|
return { rel, mtime: 0 };
|
|
@@ -14004,7 +15073,7 @@ var GrepTool = class extends BaseTool {
|
|
|
14004
15073
|
};
|
|
14005
15074
|
async execute(input, _options) {
|
|
14006
15075
|
const pattern = input["pattern"];
|
|
14007
|
-
const searchPath = input["path"] ?
|
|
15076
|
+
const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
14008
15077
|
const globPattern = input["glob"];
|
|
14009
15078
|
const outputMode = input["output_mode"] ?? "content";
|
|
14010
15079
|
const context = input["context"] ?? 0;
|
|
@@ -14058,15 +15127,15 @@ var GrepTool = class extends BaseTool {
|
|
|
14058
15127
|
nodir: true
|
|
14059
15128
|
});
|
|
14060
15129
|
} catch {
|
|
14061
|
-
files = [
|
|
15130
|
+
files = [path34.relative(searchPath, searchPath) || "."];
|
|
14062
15131
|
}
|
|
14063
15132
|
const results = [];
|
|
14064
15133
|
let totalCount = 0;
|
|
14065
15134
|
for (const rel of files) {
|
|
14066
|
-
const abs =
|
|
15135
|
+
const abs = path34.join(searchPath, rel);
|
|
14067
15136
|
let content;
|
|
14068
15137
|
try {
|
|
14069
|
-
content = await
|
|
15138
|
+
content = await fs11.readFile(abs, "utf-8");
|
|
14070
15139
|
} catch {
|
|
14071
15140
|
continue;
|
|
14072
15141
|
}
|
|
@@ -14450,10 +15519,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
14450
15519
|
}
|
|
14451
15520
|
isIgnored(filePath) {
|
|
14452
15521
|
if (!filePath) return false;
|
|
14453
|
-
const abs =
|
|
14454
|
-
const rel =
|
|
14455
|
-
if (!rel || rel.startsWith("..") ||
|
|
14456
|
-
const posixRel = rel.split(
|
|
15522
|
+
const abs = path34.resolve(this.workspaceRoot, filePath);
|
|
15523
|
+
const rel = path34.relative(this.workspaceRoot, abs);
|
|
15524
|
+
if (!rel || rel.startsWith("..") || path34.isAbsolute(rel)) return true;
|
|
15525
|
+
const posixRel = rel.split(path34.sep).join("/");
|
|
14457
15526
|
return this.ignoreMatcher.ignores(posixRel);
|
|
14458
15527
|
}
|
|
14459
15528
|
};
|
|
@@ -14687,22 +15756,22 @@ var FileMcpOAuthStore = class {
|
|
|
14687
15756
|
filePath;
|
|
14688
15757
|
load() {
|
|
14689
15758
|
try {
|
|
14690
|
-
return JSON.parse(
|
|
15759
|
+
return JSON.parse(fs31.readFileSync(this.filePath, "utf-8"));
|
|
14691
15760
|
} catch {
|
|
14692
15761
|
return void 0;
|
|
14693
15762
|
}
|
|
14694
15763
|
}
|
|
14695
15764
|
save(state) {
|
|
14696
|
-
|
|
14697
|
-
|
|
15765
|
+
fs31.mkdirSync(path34.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
15766
|
+
fs31.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
14698
15767
|
try {
|
|
14699
|
-
|
|
15768
|
+
fs31.chmodSync(this.filePath, 384);
|
|
14700
15769
|
} catch {
|
|
14701
15770
|
}
|
|
14702
15771
|
}
|
|
14703
15772
|
clear() {
|
|
14704
15773
|
try {
|
|
14705
|
-
|
|
15774
|
+
fs31.rmSync(this.filePath);
|
|
14706
15775
|
} catch {
|
|
14707
15776
|
}
|
|
14708
15777
|
}
|
|
@@ -15165,6 +16234,14 @@ function heuristicAnalyze(prompt) {
|
|
|
15165
16234
|
const estimatedTokens = wordCount * 5;
|
|
15166
16235
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
15167
16236
|
}
|
|
16237
|
+
function hashPrompt(prompt) {
|
|
16238
|
+
let hash = 2166136261;
|
|
16239
|
+
for (let index = 0; index < prompt.length; index++) {
|
|
16240
|
+
hash ^= prompt.charCodeAt(index);
|
|
16241
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
16242
|
+
}
|
|
16243
|
+
return `${prompt.length}:${hash.toString(36)}`;
|
|
16244
|
+
}
|
|
15168
16245
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
15169
16246
|
var TASK_TYPE_TAGS = {
|
|
15170
16247
|
code: ["code", "instruction"],
|
|
@@ -15208,7 +16285,7 @@ var TaskAnalyzer = class {
|
|
|
15208
16285
|
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
15209
16286
|
*/
|
|
15210
16287
|
async analyze(prompt) {
|
|
15211
|
-
const cacheKey = prompt
|
|
16288
|
+
const cacheKey = hashPrompt(prompt);
|
|
15212
16289
|
const cached = analysisCache.get(cacheKey);
|
|
15213
16290
|
if (cached) {
|
|
15214
16291
|
this.lastProfile = cached;
|
|
@@ -15310,7 +16387,7 @@ var TaskAnalyzer = class {
|
|
|
15310
16387
|
|
|
15311
16388
|
// src/core/router/model-performance-tracker.ts
|
|
15312
16389
|
init_pricing();
|
|
15313
|
-
var DEFAULT_STATS_FILE =
|
|
16390
|
+
var DEFAULT_STATS_FILE = path34.join(os10.homedir(), ".cascade", "model-perf.json");
|
|
15314
16391
|
var ModelPerformanceTracker = class {
|
|
15315
16392
|
stats = /* @__PURE__ */ new Map();
|
|
15316
16393
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -15330,7 +16407,7 @@ var ModelPerformanceTracker = class {
|
|
|
15330
16407
|
if (this.loaded) return;
|
|
15331
16408
|
this.loaded = true;
|
|
15332
16409
|
try {
|
|
15333
|
-
const raw = await
|
|
16410
|
+
const raw = await fs11.readFile(this.statsFile, "utf-8");
|
|
15334
16411
|
const parsed = JSON.parse(raw);
|
|
15335
16412
|
if (parsed.models) {
|
|
15336
16413
|
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
@@ -15350,15 +16427,15 @@ var ModelPerformanceTracker = class {
|
|
|
15350
16427
|
async save() {
|
|
15351
16428
|
if (this.readOnly) return;
|
|
15352
16429
|
try {
|
|
15353
|
-
await
|
|
16430
|
+
await fs11.mkdir(path34.dirname(this.statsFile), { recursive: true });
|
|
15354
16431
|
const modelsObj = {};
|
|
15355
16432
|
const featuresObj = {};
|
|
15356
16433
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
15357
16434
|
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
15358
16435
|
const json = JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2);
|
|
15359
16436
|
const tmp = `${this.statsFile}.${process.pid}.${Date.now()}.tmp`;
|
|
15360
|
-
await
|
|
15361
|
-
await
|
|
16437
|
+
await fs11.writeFile(tmp, json, "utf-8");
|
|
16438
|
+
await fs11.rename(tmp, this.statsFile);
|
|
15362
16439
|
} catch {
|
|
15363
16440
|
}
|
|
15364
16441
|
}
|
|
@@ -15898,9 +16975,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
15898
16975
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
15899
16976
|
async loadPersistedTools() {
|
|
15900
16977
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
15901
|
-
const file =
|
|
16978
|
+
const file = path34.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
15902
16979
|
try {
|
|
15903
|
-
const raw = await
|
|
16980
|
+
const raw = await fs11.readFile(file, "utf-8");
|
|
15904
16981
|
const specs = JSON.parse(raw);
|
|
15905
16982
|
if (!Array.isArray(specs)) return;
|
|
15906
16983
|
let loaded = 0;
|
|
@@ -15922,11 +16999,11 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
15922
16999
|
}
|
|
15923
17000
|
async persist() {
|
|
15924
17001
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
15925
|
-
const dir =
|
|
15926
|
-
const file =
|
|
17002
|
+
const dir = path34.join(this.workspacePath, ".cascade");
|
|
17003
|
+
const file = path34.join(dir, DYNAMIC_TOOLS_FILE);
|
|
15927
17004
|
try {
|
|
15928
|
-
await
|
|
15929
|
-
await
|
|
17005
|
+
await fs11.mkdir(dir, { recursive: true });
|
|
17006
|
+
await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
15930
17007
|
} catch (err) {
|
|
15931
17008
|
this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
15932
17009
|
}
|
|
@@ -15943,12 +17020,12 @@ var WorldStateDB = class {
|
|
|
15943
17020
|
constructor(workspacePath, debugMode = false) {
|
|
15944
17021
|
this.workspacePath = workspacePath;
|
|
15945
17022
|
this.debugMode = debugMode;
|
|
15946
|
-
const cascadeDir =
|
|
15947
|
-
if (!
|
|
15948
|
-
|
|
17023
|
+
const cascadeDir = path34.join(workspacePath, ".cascade");
|
|
17024
|
+
if (!fs31.existsSync(cascadeDir)) {
|
|
17025
|
+
fs31.mkdirSync(cascadeDir, { recursive: true });
|
|
15949
17026
|
}
|
|
15950
|
-
this.keyPath =
|
|
15951
|
-
this.dbPath =
|
|
17027
|
+
this.keyPath = path34.join(cascadeDir, "world_state.key");
|
|
17028
|
+
this.dbPath = path34.join(cascadeDir, "world_state.db");
|
|
15952
17029
|
this.initEncryptionKey();
|
|
15953
17030
|
this.db = new Database2(this.dbPath);
|
|
15954
17031
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -16005,11 +17082,11 @@ var WorldStateDB = class {
|
|
|
16005
17082
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
16006
17083
|
}
|
|
16007
17084
|
initEncryptionKey() {
|
|
16008
|
-
if (
|
|
16009
|
-
this.encryptionKey =
|
|
17085
|
+
if (fs31.existsSync(this.keyPath)) {
|
|
17086
|
+
this.encryptionKey = fs31.readFileSync(this.keyPath);
|
|
16010
17087
|
} else {
|
|
16011
17088
|
this.encryptionKey = crypto.randomBytes(32);
|
|
16012
|
-
|
|
17089
|
+
fs31.writeFileSync(this.keyPath, this.encryptionKey);
|
|
16013
17090
|
}
|
|
16014
17091
|
}
|
|
16015
17092
|
encrypt(text) {
|
|
@@ -16295,9 +17372,9 @@ var WorldStateDB = class {
|
|
|
16295
17372
|
dumpDebugIfNeeded() {
|
|
16296
17373
|
if (!this.debugMode) return;
|
|
16297
17374
|
try {
|
|
16298
|
-
const dumpPath =
|
|
17375
|
+
const dumpPath = path34.join(os10.tmpdir(), "cascade_world_state_debug.json");
|
|
16299
17376
|
const entries = this.getAllEntries();
|
|
16300
|
-
|
|
17377
|
+
fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
16301
17378
|
} catch (err) {
|
|
16302
17379
|
console.error("Failed to dump debug world state", err);
|
|
16303
17380
|
}
|
|
@@ -16306,6 +17383,128 @@ var WorldStateDB = class {
|
|
|
16306
17383
|
this.db.close();
|
|
16307
17384
|
}
|
|
16308
17385
|
};
|
|
17386
|
+
var RESUME_SCHEMA_VERSION = 1;
|
|
17387
|
+
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
17388
|
+
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17389
|
+
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
17390
|
+
function safeId(taskId) {
|
|
17391
|
+
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
17392
|
+
return cleaned.slice(0, 100) || "run";
|
|
17393
|
+
}
|
|
17394
|
+
function isCheckpoint(value) {
|
|
17395
|
+
if (typeof value !== "object" || value === null) return false;
|
|
17396
|
+
const c = value;
|
|
17397
|
+
return c.version === RESUME_SCHEMA_VERSION && typeof c.taskId === "string" && typeof c.prompt === "string" && typeof c.createdAt === "string" && typeof c.partialOutput === "string" && Array.isArray(c.completed);
|
|
17398
|
+
}
|
|
17399
|
+
var ResumeStore = class {
|
|
17400
|
+
dir;
|
|
17401
|
+
maxCheckpoints;
|
|
17402
|
+
maxAgeMs;
|
|
17403
|
+
now;
|
|
17404
|
+
constructor(options) {
|
|
17405
|
+
this.dir = options.dir;
|
|
17406
|
+
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
17407
|
+
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
17408
|
+
this.now = options.now ?? Date.now;
|
|
17409
|
+
}
|
|
17410
|
+
/**
|
|
17411
|
+
* Persist a checkpoint and prune. Never throws: failing to save a resume file
|
|
17412
|
+
* must not turn a recoverable stop into a crash, which would be a strictly
|
|
17413
|
+
* worse outcome than the one this store exists to improve.
|
|
17414
|
+
*/
|
|
17415
|
+
async save(checkpoint) {
|
|
17416
|
+
try {
|
|
17417
|
+
const timestamp = this.now();
|
|
17418
|
+
const record = {
|
|
17419
|
+
...checkpoint,
|
|
17420
|
+
version: RESUME_SCHEMA_VERSION,
|
|
17421
|
+
createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
|
|
17422
|
+
};
|
|
17423
|
+
await promises.mkdir(this.dir, { recursive: true });
|
|
17424
|
+
const finalPath = path34.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
|
|
17425
|
+
const tempPath = `${finalPath}.${process.pid}.tmp`;
|
|
17426
|
+
await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
17427
|
+
await promises.rename(tempPath, finalPath);
|
|
17428
|
+
await this.prune();
|
|
17429
|
+
} catch {
|
|
17430
|
+
}
|
|
17431
|
+
}
|
|
17432
|
+
/** Valid, unexpired checkpoints, newest first. */
|
|
17433
|
+
async list() {
|
|
17434
|
+
let entries;
|
|
17435
|
+
try {
|
|
17436
|
+
entries = await promises.readdir(this.dir);
|
|
17437
|
+
} catch {
|
|
17438
|
+
return [];
|
|
17439
|
+
}
|
|
17440
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
17441
|
+
const candidates2 = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).filter((e) => e.timestamp >= cutoff).sort((a, b) => b.timestamp - a.timestamp);
|
|
17442
|
+
const checkpoints = [];
|
|
17443
|
+
for (const candidate of candidates2) {
|
|
17444
|
+
try {
|
|
17445
|
+
const raw = await promises.readFile(path34.join(this.dir, candidate.name), "utf-8");
|
|
17446
|
+
const parsed = JSON.parse(raw);
|
|
17447
|
+
if (isCheckpoint(parsed)) checkpoints.push(parsed);
|
|
17448
|
+
} catch {
|
|
17449
|
+
continue;
|
|
17450
|
+
}
|
|
17451
|
+
}
|
|
17452
|
+
return checkpoints;
|
|
17453
|
+
}
|
|
17454
|
+
/** The most recent resumable run, or null. */
|
|
17455
|
+
async latest() {
|
|
17456
|
+
return (await this.list())[0] ?? null;
|
|
17457
|
+
}
|
|
17458
|
+
/**
|
|
17459
|
+
* Load a checkpoint and delete it in the same step. Resuming twice from one
|
|
17460
|
+
* checkpoint would re-run restored work, so consumption is part of reading.
|
|
17461
|
+
*/
|
|
17462
|
+
async consume(taskId) {
|
|
17463
|
+
const all = await this.list();
|
|
17464
|
+
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
17465
|
+
if (!target) return null;
|
|
17466
|
+
await this.delete(target.taskId);
|
|
17467
|
+
return target;
|
|
17468
|
+
}
|
|
17469
|
+
/** Remove every checkpoint for a task id. */
|
|
17470
|
+
async delete(taskId) {
|
|
17471
|
+
try {
|
|
17472
|
+
const entries = await promises.readdir(this.dir);
|
|
17473
|
+
const wanted = safeId(taskId);
|
|
17474
|
+
await Promise.all(entries.map(async (name) => {
|
|
17475
|
+
const match = FILE_PATTERN.exec(name);
|
|
17476
|
+
if (match?.[2] === wanted) {
|
|
17477
|
+
await promises.rm(path34.join(this.dir, name), { force: true });
|
|
17478
|
+
}
|
|
17479
|
+
}));
|
|
17480
|
+
} catch {
|
|
17481
|
+
}
|
|
17482
|
+
}
|
|
17483
|
+
/** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
|
|
17484
|
+
async prune() {
|
|
17485
|
+
try {
|
|
17486
|
+
const entries = await promises.readdir(this.dir);
|
|
17487
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
17488
|
+
const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
|
|
17489
|
+
const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
|
|
17490
|
+
const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
|
|
17491
|
+
await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
|
|
17492
|
+
(name) => promises.rm(path34.join(this.dir, name), { force: true })
|
|
17493
|
+
));
|
|
17494
|
+
} catch {
|
|
17495
|
+
}
|
|
17496
|
+
}
|
|
17497
|
+
};
|
|
17498
|
+
function summarizeCompleted(completed, perNodeChars = 500) {
|
|
17499
|
+
if (!completed.length) return "";
|
|
17500
|
+
const lines = completed.map((node) => {
|
|
17501
|
+
const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
|
|
17502
|
+
return `- [${node.status}] ${node.title}
|
|
17503
|
+
${body.replace(/\n/g, "\n ")}`;
|
|
17504
|
+
});
|
|
17505
|
+
return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
|
|
17506
|
+
${lines.join("\n")}`;
|
|
17507
|
+
}
|
|
16309
17508
|
var ignore3 = _ignoreModule.default ?? _ignoreModule;
|
|
16310
17509
|
var PrivacyPaths = class {
|
|
16311
17510
|
localOnly;
|
|
@@ -16667,7 +17866,7 @@ var WorkspaceIndex = class {
|
|
|
16667
17866
|
isIgnored;
|
|
16668
17867
|
chunker;
|
|
16669
17868
|
constructor(opts) {
|
|
16670
|
-
this.root =
|
|
17869
|
+
this.root = path34.resolve(opts.root);
|
|
16671
17870
|
this.db = opts.db;
|
|
16672
17871
|
this.namespace = opts.namespace ?? "code";
|
|
16673
17872
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -16694,19 +17893,19 @@ var WorkspaceIndex = class {
|
|
|
16694
17893
|
const walk = async (dir) => {
|
|
16695
17894
|
let entries;
|
|
16696
17895
|
try {
|
|
16697
|
-
entries = await
|
|
17896
|
+
entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
16698
17897
|
} catch {
|
|
16699
17898
|
return;
|
|
16700
17899
|
}
|
|
16701
17900
|
for (const entry of entries) {
|
|
16702
|
-
const abs =
|
|
17901
|
+
const abs = path34.join(dir, entry.name);
|
|
16703
17902
|
if (entry.isDirectory()) {
|
|
16704
17903
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
16705
17904
|
await walk(abs);
|
|
16706
17905
|
} else if (entry.isFile()) {
|
|
16707
|
-
const ext =
|
|
17906
|
+
const ext = path34.extname(entry.name).slice(1).toLowerCase();
|
|
16708
17907
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
16709
|
-
out.push(
|
|
17908
|
+
out.push(path34.relative(this.root, abs));
|
|
16710
17909
|
}
|
|
16711
17910
|
}
|
|
16712
17911
|
};
|
|
@@ -16736,10 +17935,10 @@ var WorkspaceIndex = class {
|
|
|
16736
17935
|
const entries = [];
|
|
16737
17936
|
for (const rel of rels) {
|
|
16738
17937
|
try {
|
|
16739
|
-
const abs =
|
|
16740
|
-
const stat = await
|
|
17938
|
+
const abs = path34.join(this.root, rel);
|
|
17939
|
+
const stat = await fs11.stat(abs);
|
|
16741
17940
|
if (stat.size > this.maxFileBytes) continue;
|
|
16742
|
-
const buf = await
|
|
17941
|
+
const buf = await fs11.readFile(abs);
|
|
16743
17942
|
if (buf.includes(0)) continue;
|
|
16744
17943
|
entries.push({ path: rel, hash: hashContent(buf), text: buf.toString("utf8") });
|
|
16745
17944
|
} catch {
|
|
@@ -17274,8 +18473,16 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
17274
18473
|
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
17275
18474
|
decisionLog = [];
|
|
17276
18475
|
initialized = false;
|
|
17277
|
-
/**
|
|
18476
|
+
/**
|
|
18477
|
+
* Last interrupted task — powers /continue (resumeRun). Kept in memory as a
|
|
18478
|
+
* fast path for the same-process case; the durable copy lives in `resumeStore`
|
|
18479
|
+
* so a crash or an exit no longer throws away everything already finished.
|
|
18480
|
+
*/
|
|
17278
18481
|
lastInterruptedRun;
|
|
18482
|
+
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
18483
|
+
resumeStore;
|
|
18484
|
+
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
18485
|
+
currentRunCompleted = [];
|
|
17279
18486
|
initPromise;
|
|
17280
18487
|
store;
|
|
17281
18488
|
audit;
|
|
@@ -17308,14 +18515,21 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
17308
18515
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
17309
18516
|
try {
|
|
17310
18517
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
17311
|
-
fileDeadModelPersistence(
|
|
18518
|
+
fileDeadModelPersistence(path34.join(workspacePath, ".cascade", "dead-models.json"))
|
|
17312
18519
|
));
|
|
17313
18520
|
} catch {
|
|
17314
18521
|
}
|
|
18522
|
+
try {
|
|
18523
|
+
this.resumeStore = new ResumeStore({
|
|
18524
|
+
dir: path34.join(workspacePath, ".cascade", "resume")
|
|
18525
|
+
});
|
|
18526
|
+
} catch {
|
|
18527
|
+
}
|
|
17315
18528
|
this.multimodal = new MultimodalRegistry(
|
|
17316
18529
|
(this.config.providers ?? []).map((p) => p.type)
|
|
17317
18530
|
);
|
|
17318
18531
|
this.registerMediaTools(workspacePath);
|
|
18532
|
+
this.registerDocumentTools(workspacePath);
|
|
17319
18533
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
17320
18534
|
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
17321
18535
|
this.router.setWorldStateDB(this.worldStateDB);
|
|
@@ -17395,8 +18609,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
17395
18609
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
17396
18610
|
return;
|
|
17397
18611
|
}
|
|
17398
|
-
const dbPath = ci.dbPath ||
|
|
17399
|
-
|
|
18612
|
+
const dbPath = ci.dbPath || path34.join(this.workspacePath, ".cascade", "code-index.db");
|
|
18613
|
+
fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
|
|
17400
18614
|
const db = new Database2(dbPath);
|
|
17401
18615
|
db.pragma("journal_mode = WAL");
|
|
17402
18616
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -17736,6 +18950,35 @@ ${input}` }],
|
|
|
17736
18950
|
this.toolRegistry.register(tool);
|
|
17737
18951
|
}
|
|
17738
18952
|
}
|
|
18953
|
+
/**
|
|
18954
|
+
* Register `generate_document` where there is a real workspace to write into.
|
|
18955
|
+
*
|
|
18956
|
+
* A `.docx`/`.pptx`/`.xlsx` is a ZIP of OOXML parts, so `file_write` — which
|
|
18957
|
+
* does exactly what it says and writes the model's text verbatim — produced
|
|
18958
|
+
* files Word/Excel/PowerPoint correctly reported as corrupted. This tool owns
|
|
18959
|
+
* the conversion (see tools/generate-document.ts for why it isn't folded into
|
|
18960
|
+
* file_write).
|
|
18961
|
+
*
|
|
18962
|
+
* Registered OUTSIDE `enabledTools` for the same reason the media tools are:
|
|
18963
|
+
* that allowlist is a blast-radius control for tools that reach the machine
|
|
18964
|
+
* or the network, and this one only writes the artifact the user asked for,
|
|
18965
|
+
* into the workspace the run already owns. `disabledTools` still removes it.
|
|
18966
|
+
* A host with no filesystem (the cloud) has its own browser-side exporter and
|
|
18967
|
+
* simply never reaches this path — `buildDocumentTools` returns nothing
|
|
18968
|
+
* without a file reader, mirroring `transcribe_audio`.
|
|
18969
|
+
*/
|
|
18970
|
+
registerDocumentTools(workspacePath) {
|
|
18971
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
18972
|
+
const tools = buildDocumentTools(async (p) => {
|
|
18973
|
+
const fsp = await import('fs/promises');
|
|
18974
|
+
return new Uint8Array(await fsp.readFile(p));
|
|
18975
|
+
});
|
|
18976
|
+
for (const tool of tools) {
|
|
18977
|
+
if (denied.has(tool.name)) continue;
|
|
18978
|
+
tool.setWorkspaceRoot(workspacePath);
|
|
18979
|
+
this.toolRegistry.register(tool);
|
|
18980
|
+
}
|
|
18981
|
+
}
|
|
17739
18982
|
/**
|
|
17740
18983
|
* Supply per-model thumbs-up/down counts so Auto routing can weigh how models
|
|
17741
18984
|
* have actually performed for this user, on top of public benchmarks.
|
|
@@ -17747,10 +18990,43 @@ ${input}` }],
|
|
|
17747
18990
|
setFeedbackSource(source) {
|
|
17748
18991
|
this.router.setFeedbackSource(source);
|
|
17749
18992
|
}
|
|
17750
|
-
/**
|
|
18993
|
+
/**
|
|
18994
|
+
* Write a durable checkpoint for an interrupted run.
|
|
18995
|
+
*
|
|
18996
|
+
* Best-effort throughout: a run is already ending badly on every path that
|
|
18997
|
+
* calls this, and failing to record a resume file must not make that ending
|
|
18998
|
+
* worse. Nothing here is allowed to throw.
|
|
18999
|
+
*/
|
|
19000
|
+
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
19001
|
+
if (!this.currentRunCompleted.length && !partialOutput) return;
|
|
19002
|
+
try {
|
|
19003
|
+
await this.resumeStore?.save({
|
|
19004
|
+
taskId,
|
|
19005
|
+
prompt,
|
|
19006
|
+
reason,
|
|
19007
|
+
...detail ? { detail } : {},
|
|
19008
|
+
partialOutput,
|
|
19009
|
+
completed: [...this.currentRunCompleted]
|
|
19010
|
+
});
|
|
19011
|
+
} catch {
|
|
19012
|
+
}
|
|
19013
|
+
}
|
|
19014
|
+
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
17751
19015
|
hasResumableRun() {
|
|
17752
19016
|
return this.lastInterruptedRun != null;
|
|
17753
19017
|
}
|
|
19018
|
+
/**
|
|
19019
|
+
* True when a checkpoint from ANY process is resumable. Async because the
|
|
19020
|
+
* durable check touches disk; `hasResumableRun` remains the sync in-memory
|
|
19021
|
+
* answer for callers already on a hot path.
|
|
19022
|
+
*/
|
|
19023
|
+
async hasDurableResume() {
|
|
19024
|
+
return await this.resumeStore?.latest() != null;
|
|
19025
|
+
}
|
|
19026
|
+
/** Interrupted runs available to resume, newest first. */
|
|
19027
|
+
async listResumableRuns() {
|
|
19028
|
+
return await this.resumeStore?.list() ?? [];
|
|
19029
|
+
}
|
|
17754
19030
|
/**
|
|
17755
19031
|
* Raise the per-run token budget for a resume and return the continuation
|
|
17756
19032
|
* prompt (or null when nothing is resumable). Consumes the interrupted-run
|
|
@@ -17761,22 +19037,54 @@ ${input}` }],
|
|
|
17761
19037
|
const last = this.lastInterruptedRun;
|
|
17762
19038
|
if (!last) return null;
|
|
17763
19039
|
this.lastInterruptedRun = void 0;
|
|
17764
|
-
|
|
19040
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19041
|
+
return this.buildResumePrompt(last.prompt, last.partialOutput, []);
|
|
19042
|
+
}
|
|
19043
|
+
/**
|
|
19044
|
+
* Durable counterpart to prepareResume: recovers a run interrupted in ANY
|
|
19045
|
+
* process, including one that crashed before it could return. Consumes the
|
|
19046
|
+
* checkpoint, so the same finished work is never restored into two runs.
|
|
19047
|
+
*/
|
|
19048
|
+
async prepareDurableResume(opts = {}) {
|
|
19049
|
+
const checkpoint = await this.resumeStore?.consume(opts.taskId);
|
|
19050
|
+
if (!checkpoint) return null;
|
|
19051
|
+
this.lastInterruptedRun = void 0;
|
|
19052
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19053
|
+
return this.buildResumePrompt(checkpoint.prompt, checkpoint.partialOutput, checkpoint.completed);
|
|
19054
|
+
}
|
|
19055
|
+
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
19056
|
+
raiseBudgetForResume(maxTokens) {
|
|
19057
|
+
const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
|
|
17765
19058
|
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
|
|
17766
19059
|
this.router.setMaxTokensPerRun(raised);
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
19060
|
+
}
|
|
19061
|
+
/**
|
|
19062
|
+
* The resume prompt. Completed sections are named explicitly rather than
|
|
19063
|
+
* gestured at: "do not recreate the files" is an instruction a planner can
|
|
19064
|
+
* only guess at, whereas a list of what is already done, and what it produced,
|
|
19065
|
+
* is something it can act on. That difference is the entire reason node
|
|
19066
|
+
* results are kept — a re-plan that cannot see the finished work will plan the
|
|
19067
|
+
* finished work again, and the user pays for it a second time.
|
|
19068
|
+
*/
|
|
19069
|
+
buildResumePrompt(prompt, partialOutput, completed) {
|
|
19070
|
+
const done = summarizeCompleted(completed);
|
|
19071
|
+
return [
|
|
19072
|
+
"Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Plan ONLY the remaining work.",
|
|
19073
|
+
"",
|
|
19074
|
+
`Original task: ${prompt}`,
|
|
19075
|
+
done ? `
|
|
19076
|
+
${done}` : "",
|
|
19077
|
+
partialOutput ? `
|
|
17771
19078
|
Partial result so far:
|
|
17772
|
-
${
|
|
19079
|
+
${partialOutput}` : ""
|
|
19080
|
+
].filter(Boolean).join("\n");
|
|
17773
19081
|
}
|
|
17774
19082
|
/**
|
|
17775
19083
|
* Resume the last budget-capped task with a raised budget (SDK/headless).
|
|
17776
19084
|
* Returns null when there is nothing to resume.
|
|
17777
19085
|
*/
|
|
17778
19086
|
async resumeRun(opts = {}) {
|
|
17779
|
-
const prompt = this.prepareResume(opts);
|
|
19087
|
+
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
17780
19088
|
if (!prompt) return null;
|
|
17781
19089
|
return this.run({ prompt });
|
|
17782
19090
|
}
|
|
@@ -18147,6 +19455,7 @@ ${prompt}` : prompt;
|
|
|
18147
19455
|
const startMs = Date.now();
|
|
18148
19456
|
const taskId = randomUUID();
|
|
18149
19457
|
this.decisionLog = [];
|
|
19458
|
+
this.currentRunCompleted = [];
|
|
18150
19459
|
if (options.fastAnswer) {
|
|
18151
19460
|
return this.runFastAnswer(options, startMs, taskId);
|
|
18152
19461
|
}
|
|
@@ -18261,6 +19570,11 @@ ${prompt}` : prompt;
|
|
|
18261
19570
|
});
|
|
18262
19571
|
tier.on("log", (e) => this.emit("log", e));
|
|
18263
19572
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
19573
|
+
tier.on("section:complete", (e) => {
|
|
19574
|
+
if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
|
|
19575
|
+
this.currentRunCompleted.push(e);
|
|
19576
|
+
this.emit("section:complete", e);
|
|
19577
|
+
});
|
|
18264
19578
|
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
18265
19579
|
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
18266
19580
|
tier.on("tool:approval-request", async (request) => {
|
|
@@ -18407,6 +19721,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
18407
19721
|
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
18408
19722
|
const trip = runBreaker.reason();
|
|
18409
19723
|
if (trip) {
|
|
19724
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
|
|
18410
19725
|
finalOutput = [
|
|
18411
19726
|
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
18412
19727
|
"",
|
|
@@ -18426,6 +19741,13 @@ ${finalOutput}` : ""
|
|
|
18426
19741
|
reason: err instanceof Error ? err.message : "Task cancelled",
|
|
18427
19742
|
partialOutput: finalOutput || ""
|
|
18428
19743
|
});
|
|
19744
|
+
await this.checkpointRun(
|
|
19745
|
+
taskId,
|
|
19746
|
+
options.prompt,
|
|
19747
|
+
finalOutput || "",
|
|
19748
|
+
"cancelled",
|
|
19749
|
+
err instanceof Error ? err.message : "Task cancelled"
|
|
19750
|
+
);
|
|
18429
19751
|
runError = null;
|
|
18430
19752
|
} else if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
18431
19753
|
this.emit("run:budget-exceeded", {
|
|
@@ -18434,9 +19756,17 @@ ${finalOutput}` : ""
|
|
|
18434
19756
|
partialOutput: finalOutput || ""
|
|
18435
19757
|
});
|
|
18436
19758
|
this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
|
|
19759
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
|
|
18437
19760
|
if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
|
|
18438
19761
|
runError = null;
|
|
18439
19762
|
} else {
|
|
19763
|
+
await this.checkpointRun(
|
|
19764
|
+
taskId,
|
|
19765
|
+
options.prompt,
|
|
19766
|
+
finalOutput || "",
|
|
19767
|
+
"error",
|
|
19768
|
+
err instanceof Error ? err.message : String(err)
|
|
19769
|
+
);
|
|
18440
19770
|
runError = err;
|
|
18441
19771
|
throw err;
|
|
18442
19772
|
}
|
|
@@ -18648,14 +19978,14 @@ ${finalOutput}` : ""
|
|
|
18648
19978
|
init_constants();
|
|
18649
19979
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
18650
19980
|
function globalDir(homeDir = os10.homedir()) {
|
|
18651
|
-
return
|
|
19981
|
+
return path34.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
18652
19982
|
}
|
|
18653
19983
|
function cloudSessionPath(dir = globalDir()) {
|
|
18654
|
-
return
|
|
19984
|
+
return path34.join(dir, CLOUD_SESSION_FILE);
|
|
18655
19985
|
}
|
|
18656
19986
|
function loadCloudSession(dir = globalDir()) {
|
|
18657
19987
|
try {
|
|
18658
|
-
const raw =
|
|
19988
|
+
const raw = fs31.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
18659
19989
|
const parsed = JSON.parse(raw);
|
|
18660
19990
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
18661
19991
|
return parsed;
|
|
@@ -18664,17 +19994,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
18664
19994
|
}
|
|
18665
19995
|
}
|
|
18666
19996
|
function saveCloudSession(session, dir = globalDir()) {
|
|
18667
|
-
|
|
19997
|
+
fs31.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
18668
19998
|
const file = cloudSessionPath(dir);
|
|
18669
|
-
|
|
19999
|
+
fs31.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
18670
20000
|
try {
|
|
18671
|
-
|
|
20001
|
+
fs31.chmodSync(file, 384);
|
|
18672
20002
|
} catch {
|
|
18673
20003
|
}
|
|
18674
20004
|
}
|
|
18675
20005
|
function clearCloudSession(dir = globalDir()) {
|
|
18676
20006
|
try {
|
|
18677
|
-
|
|
20007
|
+
fs31.rmSync(cloudSessionPath(dir));
|
|
18678
20008
|
} catch {
|
|
18679
20009
|
}
|
|
18680
20010
|
}
|
|
@@ -18846,16 +20176,16 @@ var CloudClient = class _CloudClient {
|
|
|
18846
20176
|
return (await this.refresh()).accessToken;
|
|
18847
20177
|
}
|
|
18848
20178
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
18849
|
-
async authed(
|
|
18850
|
-
const call = (token) => fetch(this.url(
|
|
20179
|
+
async authed(path39, init = {}) {
|
|
20180
|
+
const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
18851
20181
|
let res = await call(await this.accessToken());
|
|
18852
20182
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
18853
20183
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
18854
20184
|
const text = await res.text();
|
|
18855
20185
|
return text ? JSON.parse(text) : {};
|
|
18856
20186
|
}
|
|
18857
|
-
authedGet(
|
|
18858
|
-
return this.authed(
|
|
20187
|
+
authedGet(path39) {
|
|
20188
|
+
return this.authed(path39);
|
|
18859
20189
|
}
|
|
18860
20190
|
async fetchMe(token) {
|
|
18861
20191
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -20630,10 +21960,10 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20630
21960
|
}
|
|
20631
21961
|
} catch {
|
|
20632
21962
|
}
|
|
20633
|
-
const configPath =
|
|
21963
|
+
const configPath = path34.join(workspacePath, ".cascade", "config.json");
|
|
20634
21964
|
try {
|
|
20635
|
-
await
|
|
20636
|
-
await
|
|
21965
|
+
await fs11.mkdir(path34.dirname(configPath), { recursive: true });
|
|
21966
|
+
await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
20637
21967
|
} catch (err) {
|
|
20638
21968
|
const msg = err instanceof Error ? err.message : String(err);
|
|
20639
21969
|
dispatch({
|
|
@@ -20684,9 +22014,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20684
22014
|
if (msg.includes("non-text parts")) return;
|
|
20685
22015
|
originalLog(...args);
|
|
20686
22016
|
};
|
|
20687
|
-
const store = new MemoryStore(
|
|
22017
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
20688
22018
|
storeRef.current = store;
|
|
20689
|
-
globalStoreRef.current = new MemoryStore(
|
|
22019
|
+
globalStoreRef.current = new MemoryStore(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
20690
22020
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
20691
22021
|
setIdentities(identityRows);
|
|
20692
22022
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -20754,14 +22084,14 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
20754
22084
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
20755
22085
|
onExport: async (fmt) => {
|
|
20756
22086
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20757
|
-
const exportPath =
|
|
22087
|
+
const exportPath = path34.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
20758
22088
|
if (fmt === "json") {
|
|
20759
|
-
await
|
|
22089
|
+
await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
20760
22090
|
} else {
|
|
20761
22091
|
const markdown = state.messages.map((msg) => `## ${msg.role.toUpperCase()} \u2014 ${msg.timestamp}
|
|
20762
22092
|
|
|
20763
22093
|
${msg.content}`).join("\n\n");
|
|
20764
|
-
await
|
|
22094
|
+
await fs11.writeFile(exportPath, markdown, "utf-8");
|
|
20765
22095
|
}
|
|
20766
22096
|
},
|
|
20767
22097
|
onRollback: async () => {
|
|
@@ -20771,7 +22101,7 @@ ${msg.content}`).join("\n\n");
|
|
|
20771
22101
|
if (!snapshots.length) return "No file snapshots found for this session.";
|
|
20772
22102
|
for (const { filePath, content } of snapshots) {
|
|
20773
22103
|
try {
|
|
20774
|
-
await
|
|
22104
|
+
await fs11.writeFile(filePath, content, "utf-8");
|
|
20775
22105
|
} catch (err) {
|
|
20776
22106
|
console.error(`Restore failed: ${filePath}`, err);
|
|
20777
22107
|
}
|
|
@@ -20996,10 +22326,12 @@ ${lastUser.content}`;
|
|
|
20996
22326
|
onContinue: async (args) => {
|
|
20997
22327
|
const cascade = cascadeRef.current;
|
|
20998
22328
|
if (!cascade) return "Not ready yet.";
|
|
20999
|
-
if (!cascade.hasResumableRun()) return "Nothing to continue \u2014 no task was stopped by the budget cap.";
|
|
21000
22329
|
const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
|
|
21001
|
-
const
|
|
21002
|
-
|
|
22330
|
+
const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
|
|
22331
|
+
const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
|
|
22332
|
+
if (!prompt) {
|
|
22333
|
+
return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
|
|
22334
|
+
}
|
|
21003
22335
|
await handleSubmit(prompt);
|
|
21004
22336
|
return "";
|
|
21005
22337
|
}
|
|
@@ -21652,9 +22984,9 @@ function stringifySlashOutput(val) {
|
|
|
21652
22984
|
}
|
|
21653
22985
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
21654
22986
|
if (!query) return "Usage: /search <query>";
|
|
21655
|
-
const dbPath =
|
|
22987
|
+
const dbPath = path34.join(workspacePath, CASCADE_DB_FILE);
|
|
21656
22988
|
try {
|
|
21657
|
-
await
|
|
22989
|
+
await fs11.access(dbPath);
|
|
21658
22990
|
} catch {
|
|
21659
22991
|
return "No database found. Start a conversation first.";
|
|
21660
22992
|
}
|
|
@@ -21686,7 +23018,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
21686
23018
|
async function diagnoseRuntime(config, workspacePath) {
|
|
21687
23019
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
21688
23020
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
21689
|
-
const store = new MemoryStore(
|
|
23021
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
21690
23022
|
try {
|
|
21691
23023
|
const sessions2 = store.listSessions(void 0, 3);
|
|
21692
23024
|
return [
|
|
@@ -21705,7 +23037,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
21705
23037
|
}
|
|
21706
23038
|
async function showRecentLogs(args, workspacePath) {
|
|
21707
23039
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
21708
|
-
const store = new MemoryStore(
|
|
23040
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
21709
23041
|
try {
|
|
21710
23042
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
21711
23043
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -21717,7 +23049,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
21717
23049
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
21718
23050
|
const sessionId = args[0];
|
|
21719
23051
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
21720
|
-
const store = new MemoryStore(
|
|
23052
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
21721
23053
|
try {
|
|
21722
23054
|
const session = store.getSession(sessionId);
|
|
21723
23055
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -22145,10 +23477,10 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
22145
23477
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
22146
23478
|
};
|
|
22147
23479
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
22148
|
-
const configDir =
|
|
22149
|
-
await
|
|
22150
|
-
const configPath =
|
|
22151
|
-
await
|
|
23480
|
+
const configDir = path34.join(workspacePath, ".cascade");
|
|
23481
|
+
await fs11.mkdir(configDir, { recursive: true });
|
|
23482
|
+
const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
23483
|
+
await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
22152
23484
|
savedConfigRef.current = config;
|
|
22153
23485
|
dispatchRef.current({ type: "GO_DONE" });
|
|
22154
23486
|
} catch (err) {
|
|
@@ -22489,14 +23821,14 @@ function printTelemetryBanner() {
|
|
|
22489
23821
|
async function initCommand(workspacePath = process.cwd()) {
|
|
22490
23822
|
const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
22491
23823
|
try {
|
|
22492
|
-
const configDir =
|
|
22493
|
-
await
|
|
22494
|
-
const mdPath =
|
|
23824
|
+
const configDir = path34.join(workspacePath, ".cascade");
|
|
23825
|
+
await fs11.mkdir(configDir, { recursive: true });
|
|
23826
|
+
const mdPath = path34.join(workspacePath, "CASCADE.md");
|
|
22495
23827
|
if (!await fileExists(mdPath)) {
|
|
22496
23828
|
await createDefaultCascadeMd(workspacePath);
|
|
22497
23829
|
spin.succeed(chalk7.green("Created CASCADE.md"));
|
|
22498
23830
|
}
|
|
22499
|
-
const ignorePath =
|
|
23831
|
+
const ignorePath = path34.join(workspacePath, ".cascadeignore");
|
|
22500
23832
|
if (!await fileExists(ignorePath)) {
|
|
22501
23833
|
await createDefaultIgnoreFile(workspacePath);
|
|
22502
23834
|
spin.succeed(chalk7.green("Created .cascadeignore"));
|
|
@@ -22505,7 +23837,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
22505
23837
|
console.log();
|
|
22506
23838
|
console.log(chalk7.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
22507
23839
|
console.log();
|
|
22508
|
-
const configPath =
|
|
23840
|
+
const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22509
23841
|
if (await fileExists(configPath)) {
|
|
22510
23842
|
console.log(chalk7.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
22511
23843
|
console.log();
|
|
@@ -22524,7 +23856,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
22524
23856
|
}
|
|
22525
23857
|
async function fileExists(p) {
|
|
22526
23858
|
try {
|
|
22527
|
-
await
|
|
23859
|
+
await fs11.access(p);
|
|
22528
23860
|
return true;
|
|
22529
23861
|
} catch {
|
|
22530
23862
|
return false;
|
|
@@ -22536,7 +23868,7 @@ init_constants();
|
|
|
22536
23868
|
var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
|
|
22537
23869
|
async function readJson(file) {
|
|
22538
23870
|
try {
|
|
22539
|
-
const raw = await
|
|
23871
|
+
const raw = await fs11.readFile(file, "utf-8");
|
|
22540
23872
|
return JSON.parse(raw);
|
|
22541
23873
|
} catch {
|
|
22542
23874
|
return null;
|
|
@@ -22563,7 +23895,7 @@ function fromEnv(env) {
|
|
|
22563
23895
|
return out;
|
|
22564
23896
|
}
|
|
22565
23897
|
async function fromClaudeCode(home) {
|
|
22566
|
-
const file =
|
|
23898
|
+
const file = path34.join(home, ".claude", ".credentials.json");
|
|
22567
23899
|
const data = await readJson(file);
|
|
22568
23900
|
if (!data) return [];
|
|
22569
23901
|
const oauth = data["claudeAiOauth"];
|
|
@@ -22586,7 +23918,7 @@ async function fromClaudeCode(home) {
|
|
|
22586
23918
|
return [];
|
|
22587
23919
|
}
|
|
22588
23920
|
async function fromCodex(home) {
|
|
22589
|
-
const file =
|
|
23921
|
+
const file = path34.join(home, ".codex", "auth.json");
|
|
22590
23922
|
const data = await readJson(file);
|
|
22591
23923
|
if (!data) return [];
|
|
22592
23924
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -22609,7 +23941,7 @@ async function fromCodex(home) {
|
|
|
22609
23941
|
return [];
|
|
22610
23942
|
}
|
|
22611
23943
|
async function fromGemini(home) {
|
|
22612
|
-
const file =
|
|
23944
|
+
const file = path34.join(home, ".gemini", "oauth_creds.json");
|
|
22613
23945
|
const data = await readJson(file);
|
|
22614
23946
|
const accessToken = str(data?.["access_token"]);
|
|
22615
23947
|
if (!accessToken) return [];
|
|
@@ -22625,7 +23957,7 @@ async function fromGemini(home) {
|
|
|
22625
23957
|
}
|
|
22626
23958
|
async function fromCopilot(home) {
|
|
22627
23959
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
22628
|
-
const file =
|
|
23960
|
+
const file = path34.join(home, ".config", "github-copilot", name);
|
|
22629
23961
|
const data = await readJson(file);
|
|
22630
23962
|
if (!data) continue;
|
|
22631
23963
|
for (const value of Object.values(data)) {
|
|
@@ -22679,7 +24011,7 @@ async function doctorCommand() {
|
|
|
22679
24011
|
checks.push({
|
|
22680
24012
|
label: "Cascade config",
|
|
22681
24013
|
ok: true,
|
|
22682
|
-
detail: `Loaded ${
|
|
24014
|
+
detail: `Loaded ${path34.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
22683
24015
|
});
|
|
22684
24016
|
const providers = [
|
|
22685
24017
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -22762,17 +24094,17 @@ async function doctorCommand() {
|
|
|
22762
24094
|
const detail = c.detail ? chalk7.gray(` \u2014 ${c.detail}`) : "";
|
|
22763
24095
|
console.log(`${icon} ${label}${detail}`);
|
|
22764
24096
|
}
|
|
22765
|
-
const
|
|
24097
|
+
const failures2 = checks.filter((c) => !c.ok);
|
|
22766
24098
|
console.log();
|
|
22767
|
-
if (
|
|
24099
|
+
if (failures2.length === 0) {
|
|
22768
24100
|
console.log(chalk7.green(" All checks passed!\n"));
|
|
22769
24101
|
} else {
|
|
22770
|
-
const critical =
|
|
24102
|
+
const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
|
|
22771
24103
|
if (critical.length) {
|
|
22772
24104
|
console.log(chalk7.yellow(` ${critical.length} issue(s) need attention.
|
|
22773
24105
|
`));
|
|
22774
24106
|
} else {
|
|
22775
|
-
console.log(chalk7.gray(` ${
|
|
24107
|
+
console.log(chalk7.gray(` ${failures2.length} optional item(s) not configured.
|
|
22776
24108
|
`));
|
|
22777
24109
|
}
|
|
22778
24110
|
}
|
|
@@ -22790,7 +24122,7 @@ async function checkEndpoint(url) {
|
|
|
22790
24122
|
}
|
|
22791
24123
|
}
|
|
22792
24124
|
async function indexCommand(dirPath) {
|
|
22793
|
-
const workspace =
|
|
24125
|
+
const workspace = path34.resolve(dirPath || process.cwd());
|
|
22794
24126
|
const cm = new ConfigManager(workspace);
|
|
22795
24127
|
await cm.load();
|
|
22796
24128
|
const config = cm.getConfig();
|
|
@@ -22801,8 +24133,8 @@ async function indexCommand(dirPath) {
|
|
|
22801
24133
|
process.exitCode = 1;
|
|
22802
24134
|
return;
|
|
22803
24135
|
}
|
|
22804
|
-
const dbPath = config.codeIndex?.dbPath ||
|
|
22805
|
-
|
|
24136
|
+
const dbPath = config.codeIndex?.dbPath || path34.join(workspace, ".cascade", "code-index.db");
|
|
24137
|
+
fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
|
|
22806
24138
|
const db = new Database2(dbPath);
|
|
22807
24139
|
db.pragma("journal_mode = WAL");
|
|
22808
24140
|
const complete = chatCompleterFromProviders(config.providers);
|
|
@@ -23291,7 +24623,7 @@ function openBrowser2(url) {
|
|
|
23291
24623
|
}
|
|
23292
24624
|
function storePathFor(name) {
|
|
23293
24625
|
const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
|
|
23294
|
-
return
|
|
24626
|
+
return path34.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
|
|
23295
24627
|
}
|
|
23296
24628
|
function defaultName(url) {
|
|
23297
24629
|
try {
|
|
@@ -23793,7 +25125,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
23793
25125
|
}
|
|
23794
25126
|
|
|
23795
25127
|
// src/dashboard/server.ts
|
|
23796
|
-
var __dirname$1 =
|
|
25128
|
+
var __dirname$1 = path34.dirname(fileURLToPath(import.meta.url));
|
|
23797
25129
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
23798
25130
|
var DashboardServer = class {
|
|
23799
25131
|
app;
|
|
@@ -24073,14 +25405,14 @@ var DashboardServer = class {
|
|
|
24073
25405
|
*/
|
|
24074
25406
|
persistConfig() {
|
|
24075
25407
|
try {
|
|
24076
|
-
const configPath =
|
|
24077
|
-
|
|
24078
|
-
|
|
25408
|
+
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
25409
|
+
fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
|
|
25410
|
+
fs31.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
24079
25411
|
} catch (err) {
|
|
24080
25412
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
24081
25413
|
}
|
|
24082
25414
|
try {
|
|
24083
|
-
saveGlobalCredentials(
|
|
25415
|
+
saveGlobalCredentials(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
24084
25416
|
} catch (err) {
|
|
24085
25417
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
24086
25418
|
}
|
|
@@ -24095,15 +25427,15 @@ var DashboardServer = class {
|
|
|
24095
25427
|
resolveDashboardSecret() {
|
|
24096
25428
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
24097
25429
|
if (fromConfig) return fromConfig;
|
|
24098
|
-
const secretPath =
|
|
25430
|
+
const secretPath = path34.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
24099
25431
|
try {
|
|
24100
|
-
if (
|
|
24101
|
-
const existing =
|
|
25432
|
+
if (fs31.existsSync(secretPath)) {
|
|
25433
|
+
const existing = fs31.readFileSync(secretPath, "utf-8").trim();
|
|
24102
25434
|
if (existing.length >= 16) return existing;
|
|
24103
25435
|
}
|
|
24104
25436
|
const generated = randomUUID();
|
|
24105
|
-
|
|
24106
|
-
|
|
25437
|
+
fs31.mkdirSync(path34.dirname(secretPath), { recursive: true });
|
|
25438
|
+
fs31.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
24107
25439
|
if (this.config.dashboard.auth) {
|
|
24108
25440
|
console.warn(
|
|
24109
25441
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -24130,7 +25462,7 @@ var DashboardServer = class {
|
|
|
24130
25462
|
// ── Setup ─────────────────────────────────────
|
|
24131
25463
|
getGlobalStore() {
|
|
24132
25464
|
if (!this.globalStore) {
|
|
24133
|
-
const globalDbPath =
|
|
25465
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24134
25466
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
24135
25467
|
}
|
|
24136
25468
|
return this.globalStore;
|
|
@@ -24510,12 +25842,12 @@ ${prompt}`;
|
|
|
24510
25842
|
}
|
|
24511
25843
|
}
|
|
24512
25844
|
watchRuntimeChanges() {
|
|
24513
|
-
const workspaceDbPath =
|
|
24514
|
-
const globalDbPath =
|
|
25845
|
+
const workspaceDbPath = path34.join(this.workspacePath, CASCADE_DB_FILE);
|
|
25846
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24515
25847
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
24516
25848
|
for (const watchPath of watchPaths) {
|
|
24517
|
-
if (!
|
|
24518
|
-
|
|
25849
|
+
if (!fs31.existsSync(watchPath)) continue;
|
|
25850
|
+
fs31.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
24519
25851
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
24520
25852
|
});
|
|
24521
25853
|
}
|
|
@@ -24637,7 +25969,7 @@ ${prompt}`;
|
|
|
24637
25969
|
const sessionId = req.params.id;
|
|
24638
25970
|
this.store.deleteSession(sessionId);
|
|
24639
25971
|
this.store.deleteRuntimeSession(sessionId);
|
|
24640
|
-
const globalDbPath =
|
|
25972
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24641
25973
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24642
25974
|
try {
|
|
24643
25975
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -24730,7 +26062,7 @@ ${prompt}`;
|
|
|
24730
26062
|
});
|
|
24731
26063
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
24732
26064
|
const body = req.body;
|
|
24733
|
-
const globalDbPath =
|
|
26065
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24734
26066
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
24735
26067
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24736
26068
|
try {
|
|
@@ -24753,7 +26085,7 @@ ${prompt}`;
|
|
|
24753
26085
|
});
|
|
24754
26086
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
24755
26087
|
this.store.deleteAllRuntimeNodes();
|
|
24756
|
-
const globalDbPath =
|
|
26088
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24757
26089
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24758
26090
|
try {
|
|
24759
26091
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -24839,12 +26171,12 @@ ${prompt}`;
|
|
|
24839
26171
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
24840
26172
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
24841
26173
|
try {
|
|
24842
|
-
const configPath =
|
|
24843
|
-
const existing =
|
|
26174
|
+
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
26175
|
+
const existing = fs31.existsSync(configPath) ? JSON.parse(fs31.readFileSync(configPath, "utf-8")) : {};
|
|
24844
26176
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
24845
26177
|
const tmp = configPath + ".tmp";
|
|
24846
|
-
|
|
24847
|
-
|
|
26178
|
+
fs31.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
26179
|
+
fs31.renameSync(tmp, configPath);
|
|
24848
26180
|
res.json({ ok: true });
|
|
24849
26181
|
} catch (err) {
|
|
24850
26182
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -24872,7 +26204,7 @@ ${prompt}`;
|
|
|
24872
26204
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
24873
26205
|
const scope = req.query["scope"] ?? "workspace";
|
|
24874
26206
|
if (scope === "global") {
|
|
24875
|
-
const globalDbPath =
|
|
26207
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24876
26208
|
const globalStore = new MemoryStore(globalDbPath);
|
|
24877
26209
|
try {
|
|
24878
26210
|
res.json({
|
|
@@ -25213,13 +26545,13 @@ ${prompt}`;
|
|
|
25213
26545
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
25214
26546
|
}
|
|
25215
26547
|
});
|
|
25216
|
-
const prodPath =
|
|
25217
|
-
const devPath =
|
|
25218
|
-
const webDistPath =
|
|
25219
|
-
if (
|
|
26548
|
+
const prodPath = path34.resolve(__dirname$1, "../web/dist");
|
|
26549
|
+
const devPath = path34.resolve(__dirname$1, "../../web/dist");
|
|
26550
|
+
const webDistPath = fs31.existsSync(prodPath) ? prodPath : devPath;
|
|
26551
|
+
if (fs31.existsSync(webDistPath)) {
|
|
25220
26552
|
this.app.use(express.static(webDistPath));
|
|
25221
26553
|
this.app.get("*", (_req, res) => {
|
|
25222
|
-
res.sendFile(
|
|
26554
|
+
res.sendFile(path34.join(webDistPath, "index.html"));
|
|
25223
26555
|
});
|
|
25224
26556
|
} else {
|
|
25225
26557
|
this.app.get("/", (_req, res) => {
|
|
@@ -25240,7 +26572,7 @@ init_constants();
|
|
|
25240
26572
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
25241
26573
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
25242
26574
|
const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
25243
|
-
const store = new MemoryStore(
|
|
26575
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
25244
26576
|
const server = new DashboardServer(config, store, workspacePath);
|
|
25245
26577
|
server.watchRuntimeChanges();
|
|
25246
26578
|
const gThis = globalThis;
|
|
@@ -25272,7 +26604,7 @@ init_constants();
|
|
|
25272
26604
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
25273
26605
|
const identity = new Command("identity").alias("id").description("Manage Cascade identities");
|
|
25274
26606
|
identity.command("list").description("List all available identities").action(() => {
|
|
25275
|
-
const store = new MemoryStore(
|
|
26607
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
25276
26608
|
try {
|
|
25277
26609
|
const identities = store.listIdentities();
|
|
25278
26610
|
console.log(chalk7.bold("\n Identities:"));
|
|
@@ -25292,7 +26624,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
25292
26624
|
}
|
|
25293
26625
|
});
|
|
25294
26626
|
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) => {
|
|
25295
|
-
const store = new MemoryStore(
|
|
26627
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
25296
26628
|
try {
|
|
25297
26629
|
if (options.default) {
|
|
25298
26630
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -25318,7 +26650,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
25318
26650
|
}
|
|
25319
26651
|
});
|
|
25320
26652
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
25321
|
-
const store = new MemoryStore(
|
|
26653
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
25322
26654
|
try {
|
|
25323
26655
|
const identities = store.listIdentities();
|
|
25324
26656
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -25467,11 +26799,11 @@ async function exportCommand(options = {}) {
|
|
|
25467
26799
|
let store;
|
|
25468
26800
|
try {
|
|
25469
26801
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
25470
|
-
const workspaceDbPath =
|
|
25471
|
-
const globalDbPath =
|
|
26802
|
+
const workspaceDbPath = path34.join(workspacePath, CASCADE_DB_FILE);
|
|
26803
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
25472
26804
|
let dbPath = globalDbPath;
|
|
25473
26805
|
try {
|
|
25474
|
-
await
|
|
26806
|
+
await fs11.access(workspaceDbPath);
|
|
25475
26807
|
dbPath = workspaceDbPath;
|
|
25476
26808
|
} catch {
|
|
25477
26809
|
}
|
|
@@ -25510,8 +26842,8 @@ async function exportCommand(options = {}) {
|
|
|
25510
26842
|
const ext = format === "json" ? ".json" : ".md";
|
|
25511
26843
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
25512
26844
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
25513
|
-
const outPath = options.output ?
|
|
25514
|
-
await
|
|
26845
|
+
const outPath = options.output ? path34.resolve(options.output) : path34.join(process.cwd(), defaultFile);
|
|
26846
|
+
await fs11.writeFile(outPath, content, "utf-8");
|
|
25515
26847
|
spin.succeed(chalk7.green(`Exported to ${chalk7.white(outPath)}`));
|
|
25516
26848
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
25517
26849
|
console.log();
|
|
@@ -25738,11 +27070,11 @@ async function statsCommand() {
|
|
|
25738
27070
|
dotenv.config();
|
|
25739
27071
|
function warnIfBuildIsStale() {
|
|
25740
27072
|
try {
|
|
25741
|
-
const here =
|
|
27073
|
+
const here = path34.dirname(fileURLToPath(import.meta.url));
|
|
25742
27074
|
for (const rel of ["..", "../.."]) {
|
|
25743
|
-
const pkgPath =
|
|
25744
|
-
if (!
|
|
25745
|
-
const pkg = JSON.parse(
|
|
27075
|
+
const pkgPath = path34.join(here, rel, "package.json");
|
|
27076
|
+
if (!fs31.existsSync(pkgPath)) continue;
|
|
27077
|
+
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
|
|
25746
27078
|
if (pkg.name !== "cascade-ai") continue;
|
|
25747
27079
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
25748
27080
|
console.error(
|