openfleet 0.3.12 → 0.3.14
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/dist/index.js +154 -18
- package/dist/lib/fallback.d.ts +21 -0
- package/dist/models.d.ts +1 -0
- package/dist/transcript/hooks.d.ts +21 -0
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -59,6 +59,7 @@ var models = {
|
|
|
59
59
|
var defaultModel = models.anthropic.sonnet;
|
|
60
60
|
var bigModel = defaultModel;
|
|
61
61
|
var smallModel = defaultModel;
|
|
62
|
+
var fallbackModel = models.freeModels.minimaxM25Free;
|
|
62
63
|
|
|
63
64
|
// src/agents/names.ts
|
|
64
65
|
var AGENT_NAMES = {
|
|
@@ -1121,6 +1122,84 @@ var logger = {
|
|
|
1121
1122
|
// src/tools/save-conversation/index.ts
|
|
1122
1123
|
import { tool } from "@opencode-ai/plugin";
|
|
1123
1124
|
|
|
1125
|
+
// src/transcript/hooks.ts
|
|
1126
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
1127
|
+
import path3 from "path";
|
|
1128
|
+
|
|
1129
|
+
// src/lib/fallback.ts
|
|
1130
|
+
var CREDIT_BALANCE_PATTERNS = [
|
|
1131
|
+
"credit balance is too low",
|
|
1132
|
+
"insufficient credits",
|
|
1133
|
+
"please go to plans & billing",
|
|
1134
|
+
"purchase credits"
|
|
1135
|
+
];
|
|
1136
|
+
var fallbackInProgress = new Set;
|
|
1137
|
+
var fallbackSessions = new Set;
|
|
1138
|
+
var lastFallbackTime = new Map;
|
|
1139
|
+
var COOLDOWN_MS = 30000;
|
|
1140
|
+
function isCreditBalanceError(message) {
|
|
1141
|
+
const lower = message.toLowerCase();
|
|
1142
|
+
return CREDIT_BALANCE_PATTERNS.some((p) => lower.includes(p));
|
|
1143
|
+
}
|
|
1144
|
+
function isSessionInFallback(sessionID) {
|
|
1145
|
+
return fallbackSessions.has(sessionID);
|
|
1146
|
+
}
|
|
1147
|
+
function getFallbackModelOverride() {
|
|
1148
|
+
const [providerID, modelID] = fallbackModel.split("/");
|
|
1149
|
+
return { providerID, modelID };
|
|
1150
|
+
}
|
|
1151
|
+
async function handleCreditBalanceFallback(client, sessionID) {
|
|
1152
|
+
if (fallbackInProgress.has(sessionID))
|
|
1153
|
+
return;
|
|
1154
|
+
const last = lastFallbackTime.get(sessionID);
|
|
1155
|
+
if (last && Date.now() - last < COOLDOWN_MS)
|
|
1156
|
+
return;
|
|
1157
|
+
fallbackInProgress.add(sessionID);
|
|
1158
|
+
fallbackSessions.add(sessionID);
|
|
1159
|
+
lastFallbackTime.set(sessionID, Date.now());
|
|
1160
|
+
try {
|
|
1161
|
+
await client.session.abort({ path: { id: sessionID } });
|
|
1162
|
+
const { data: messages } = await client.session.messages({
|
|
1163
|
+
path: { id: sessionID }
|
|
1164
|
+
});
|
|
1165
|
+
if (!messages || messages.length === 0) {
|
|
1166
|
+
throw new Error("No messages found after abort");
|
|
1167
|
+
}
|
|
1168
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.info.role === "user");
|
|
1169
|
+
if (!lastUserMsg) {
|
|
1170
|
+
throw new Error("No user message found to revert to");
|
|
1171
|
+
}
|
|
1172
|
+
const textPart = lastUserMsg.parts.find((p) => p.type === "text");
|
|
1173
|
+
if (!textPart || textPart.type !== "text")
|
|
1174
|
+
return;
|
|
1175
|
+
const messageID = lastUserMsg.info.id;
|
|
1176
|
+
const text = textPart.text;
|
|
1177
|
+
await client.session.revert({
|
|
1178
|
+
path: { id: sessionID },
|
|
1179
|
+
body: { messageID }
|
|
1180
|
+
});
|
|
1181
|
+
const [providerID, modelID] = fallbackModel.split("/");
|
|
1182
|
+
await client.session.prompt({
|
|
1183
|
+
path: { id: sessionID },
|
|
1184
|
+
body: {
|
|
1185
|
+
model: { providerID, modelID },
|
|
1186
|
+
parts: [{ type: "text", text }]
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
logger.info("Credit balance fallback triggered", { sessionID, fallbackModel });
|
|
1190
|
+
await client.tui.showToast({
|
|
1191
|
+
body: {
|
|
1192
|
+
message: "\u26A0\uFE0F Anthropic credit balance low \u2014 switched to Minimax M2.5 Free",
|
|
1193
|
+
variant: "warning"
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
logger.error("Credit balance fallback failed", { sessionID, err });
|
|
1198
|
+
} finally {
|
|
1199
|
+
fallbackInProgress.delete(sessionID);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1124
1203
|
// src/transcript/writer.ts
|
|
1125
1204
|
import { existsSync as existsSync2 } from "fs";
|
|
1126
1205
|
import { appendFile, mkdir } from "fs/promises";
|
|
@@ -1292,6 +1371,9 @@ function extractContentFromParts(parts) {
|
|
|
1292
1371
|
|
|
1293
1372
|
// src/transcript/hooks.ts
|
|
1294
1373
|
var sessionInfoCache = new Map;
|
|
1374
|
+
var sessionAgentMap = new Map;
|
|
1375
|
+
var WORKSPACE_DIR = process.env.WORKSPACE_DIR ?? process.cwd();
|
|
1376
|
+
var AGENT_OVERRIDE_DIR = path3.join(WORKSPACE_DIR, ".opencode", "agents");
|
|
1295
1377
|
async function getSessionInfo(ctx, sessionID) {
|
|
1296
1378
|
const cached = sessionInfoCache.get(sessionID);
|
|
1297
1379
|
if (cached)
|
|
@@ -1316,9 +1398,17 @@ async function getSessionInfo(ctx, sessionID) {
|
|
|
1316
1398
|
function createTranscriptHooks(ctx) {
|
|
1317
1399
|
return {
|
|
1318
1400
|
"chat.message": async (input, output) => {
|
|
1401
|
+
if (input.agent) {
|
|
1402
|
+
sessionAgentMap.set(input.sessionID, input.agent);
|
|
1403
|
+
}
|
|
1319
1404
|
const session = await getSessionInfo(ctx, input.sessionID);
|
|
1320
1405
|
await recordUserMessage(session, output.message, output.parts);
|
|
1321
1406
|
},
|
|
1407
|
+
"chat.model": async (input, output) => {
|
|
1408
|
+
if (isSessionInFallback(input.sessionID)) {
|
|
1409
|
+
output.model = getFallbackModelOverride();
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1322
1412
|
"tool.execute.before": async (input, output) => {
|
|
1323
1413
|
const session = await getSessionInfo(ctx, input.sessionID);
|
|
1324
1414
|
await recordToolUse(session, input.tool, input.callID, output.args);
|
|
@@ -1326,18 +1416,62 @@ function createTranscriptHooks(ctx) {
|
|
|
1326
1416
|
"tool.execute.after": async (input, output) => {
|
|
1327
1417
|
const session = await getSessionInfo(ctx, input.sessionID);
|
|
1328
1418
|
await recordToolResult(session, input.tool, input.callID, output);
|
|
1419
|
+
},
|
|
1420
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
1421
|
+
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
1422
|
+
if (!agentName)
|
|
1423
|
+
return;
|
|
1424
|
+
const overridePath = path3.join(AGENT_OVERRIDE_DIR, agentName, "system_prompt.md");
|
|
1425
|
+
if (!overridePath.startsWith(AGENT_OVERRIDE_DIR + path3.sep))
|
|
1426
|
+
return;
|
|
1427
|
+
if (!existsSync3(overridePath))
|
|
1428
|
+
return;
|
|
1429
|
+
try {
|
|
1430
|
+
const content = readFileSync(overridePath, "utf-8").trim();
|
|
1431
|
+
if (content) {
|
|
1432
|
+
output.system.push(content);
|
|
1433
|
+
}
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
logger.error("Failed to read agent system prompt override", {
|
|
1436
|
+
agentName,
|
|
1437
|
+
overridePath,
|
|
1438
|
+
err
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
},
|
|
1442
|
+
event: async ({ event }) => {
|
|
1443
|
+
if (event.type === "session.status") {
|
|
1444
|
+
const { sessionID, status } = event.properties;
|
|
1445
|
+
if (status.type === "retry" && isCreditBalanceError(status.message)) {
|
|
1446
|
+
await handleCreditBalanceFallback(ctx.client, sessionID);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (event.type === "session.error") {
|
|
1450
|
+
const { sessionID, error } = event.properties;
|
|
1451
|
+
if (sessionID && error && "message" in error.data && isCreditBalanceError(String(error.data.message))) {
|
|
1452
|
+
await handleCreditBalanceFallback(ctx.client, sessionID);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (event.type === "message.updated") {
|
|
1456
|
+
const { info } = event.properties;
|
|
1457
|
+
if (info.role === "assistant" && info.error) {
|
|
1458
|
+
if ("message" in info.error.data && isCreditBalanceError(String(info.error.data.message))) {
|
|
1459
|
+
await handleCreditBalanceFallback(ctx.client, info.sessionID);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1329
1463
|
}
|
|
1330
1464
|
};
|
|
1331
1465
|
}
|
|
1332
1466
|
// src/tools/save-conversation/counter.ts
|
|
1333
1467
|
import * as fs from "fs";
|
|
1334
|
-
import * as
|
|
1468
|
+
import * as path4 from "path";
|
|
1335
1469
|
var SESSIONS_DIR = PATHS.sessions;
|
|
1336
1470
|
var MAX_COUNTER = 999;
|
|
1337
1471
|
var FILENAME_PATTERN = /^(\d{3})_(.+)\.md$/;
|
|
1338
1472
|
async function getNextCounter(date) {
|
|
1339
1473
|
try {
|
|
1340
|
-
const dateDir =
|
|
1474
|
+
const dateDir = path4.join(SESSIONS_DIR, date);
|
|
1341
1475
|
ensureDateDir(dateDir);
|
|
1342
1476
|
const files = fs.readdirSync(dateDir);
|
|
1343
1477
|
const counters = files.map((file) => parseFilename(file)).filter((parsed) => parsed !== null).map((parsed) => parsed.counter);
|
|
@@ -1380,13 +1514,13 @@ function getCurrentDate() {
|
|
|
1380
1514
|
|
|
1381
1515
|
// src/tools/save-conversation/session-writer.ts
|
|
1382
1516
|
import * as fs2 from "fs";
|
|
1383
|
-
import * as
|
|
1517
|
+
import * as path5 from "path";
|
|
1384
1518
|
var SESSIONS_DIR2 = PATHS.sessions;
|
|
1385
1519
|
function writeSession(entry) {
|
|
1386
|
-
const dateDir =
|
|
1520
|
+
const dateDir = path5.join(SESSIONS_DIR2, entry.date);
|
|
1387
1521
|
ensureDateDir2(dateDir);
|
|
1388
1522
|
const filename = `${entry.counter}_${entry.slug}.md`;
|
|
1389
|
-
const filepath =
|
|
1523
|
+
const filepath = path5.join(dateDir, filename);
|
|
1390
1524
|
const content = buildSessionContent(entry);
|
|
1391
1525
|
try {
|
|
1392
1526
|
fs2.writeFileSync(filepath, content, { encoding: "utf8" });
|
|
@@ -1698,9 +1832,9 @@ function buildContextString(messages, note) {
|
|
|
1698
1832
|
|
|
1699
1833
|
// src/utils/directory-init.ts
|
|
1700
1834
|
import * as fs3 from "fs";
|
|
1701
|
-
import * as
|
|
1835
|
+
import * as path6 from "path";
|
|
1702
1836
|
import { fileURLToPath } from "url";
|
|
1703
|
-
var TEMPLATES_DIR =
|
|
1837
|
+
var TEMPLATES_DIR = path6.join(path6.dirname(fileURLToPath(import.meta.url)), "templates", ".openfleet");
|
|
1704
1838
|
function initializeDirectories() {
|
|
1705
1839
|
if (fs3.existsSync(OPENFLEET_DIR)) {
|
|
1706
1840
|
return;
|
|
@@ -1712,9 +1846,9 @@ function copyDirectorySync(src, dest) {
|
|
|
1712
1846
|
fs3.mkdirSync(dest, { recursive: true });
|
|
1713
1847
|
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
1714
1848
|
for (const entry of entries) {
|
|
1715
|
-
const srcPath =
|
|
1849
|
+
const srcPath = path6.join(src, entry.name);
|
|
1716
1850
|
const destName = entry.name === "gitignore.template" ? ".gitignore" : entry.name;
|
|
1717
|
-
const destPath =
|
|
1851
|
+
const destPath = path6.join(dest, destName);
|
|
1718
1852
|
if (entry.isDirectory()) {
|
|
1719
1853
|
copyDirectorySync(srcPath, destPath);
|
|
1720
1854
|
} else {
|
|
@@ -1756,6 +1890,7 @@ var OpenfleetPlugin = async (ctx) => {
|
|
|
1756
1890
|
logger.info("Plugin loaded");
|
|
1757
1891
|
const saveConversation = createSaveConversationTool(ctx);
|
|
1758
1892
|
const transcriptHooks = createTranscriptHooks(ctx);
|
|
1893
|
+
const { event: transcriptEvent, ...otherTranscriptHooks } = transcriptHooks;
|
|
1759
1894
|
return {
|
|
1760
1895
|
tool: {
|
|
1761
1896
|
save_conversation: saveConversation
|
|
@@ -1764,16 +1899,17 @@ var OpenfleetPlugin = async (ctx) => {
|
|
|
1764
1899
|
configureAgents(config);
|
|
1765
1900
|
},
|
|
1766
1901
|
event: async ({ event }) => {
|
|
1767
|
-
if (event.type
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
}
|
|
1902
|
+
if (event.type === "session.created") {
|
|
1903
|
+
const props = event.properties;
|
|
1904
|
+
if (!props?.info?.parentID) {
|
|
1905
|
+
setTimeout(async () => {
|
|
1906
|
+
await showFleetToast(ctx);
|
|
1907
|
+
}, 0);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
await transcriptEvent({ event });
|
|
1775
1911
|
},
|
|
1776
|
-
...
|
|
1912
|
+
...otherTranscriptHooks
|
|
1777
1913
|
};
|
|
1778
1914
|
};
|
|
1779
1915
|
async function showFleetToast(ctx) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
/** Returns true if the message contains a known Anthropic credit balance error pattern. */
|
|
3
|
+
export declare function isCreditBalanceError(message: string): boolean;
|
|
4
|
+
/** Returns true if the given session has previously fallen back to the free model. */
|
|
5
|
+
export declare function isSessionInFallback(sessionID: string): boolean;
|
|
6
|
+
/** Returns the fallback model split into providerID and modelID. */
|
|
7
|
+
export declare function getFallbackModelOverride(): {
|
|
8
|
+
providerID: string;
|
|
9
|
+
modelID: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Handles automatic fallback to free model when Anthropic credit balance is low.
|
|
13
|
+
*
|
|
14
|
+
* This function:
|
|
15
|
+
* 1. Guards against re-entrant or rapid repeated fallbacks via in-progress set and cooldown
|
|
16
|
+
* 2. Aborts the current retry loop for the session
|
|
17
|
+
* 3. Finds and reverts to before the last user message
|
|
18
|
+
* 4. Re-sends that message using the fallback model
|
|
19
|
+
* 5. Shows a warning toast to the user
|
|
20
|
+
*/
|
|
21
|
+
export declare function handleCreditBalanceFallback(client: PluginInput["client"], sessionID: string): Promise<void>;
|
package/dist/models.d.ts
CHANGED
|
@@ -28,3 +28,4 @@ export declare const models: {
|
|
|
28
28
|
export declare const defaultModel: "anthropic/claude-sonnet-4-6";
|
|
29
29
|
export declare const bigModel: "anthropic/claude-sonnet-4-6";
|
|
30
30
|
export declare const smallModel: "anthropic/claude-sonnet-4-6";
|
|
31
|
+
export declare const fallbackModel: "opencode/minimax-m2.5-free";
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import type { Event } from "@opencode-ai/sdk";
|
|
2
3
|
export declare function createTranscriptHooks(ctx: PluginInput): {
|
|
3
4
|
"chat.message": (input: {
|
|
4
5
|
sessionID: string;
|
|
6
|
+
agent?: string;
|
|
5
7
|
}, output: {
|
|
6
8
|
message: unknown;
|
|
7
9
|
parts: unknown[];
|
|
8
10
|
}) => Promise<void>;
|
|
11
|
+
"chat.model": (input: {
|
|
12
|
+
sessionID: string;
|
|
13
|
+
agent: string;
|
|
14
|
+
model: {
|
|
15
|
+
providerID: string;
|
|
16
|
+
modelID: string;
|
|
17
|
+
};
|
|
18
|
+
}, output: {
|
|
19
|
+
model: {
|
|
20
|
+
providerID: string;
|
|
21
|
+
modelID: string;
|
|
22
|
+
};
|
|
23
|
+
}) => Promise<void>;
|
|
9
24
|
"tool.execute.before": (input: {
|
|
10
25
|
sessionID: string;
|
|
11
26
|
tool: string;
|
|
@@ -22,4 +37,10 @@ export declare function createTranscriptHooks(ctx: PluginInput): {
|
|
|
22
37
|
output: string;
|
|
23
38
|
metadata?: unknown;
|
|
24
39
|
}) => Promise<void>;
|
|
40
|
+
"experimental.chat.system.transform": (input: {}, output: {
|
|
41
|
+
system: string[];
|
|
42
|
+
}) => Promise<void>;
|
|
43
|
+
event: ({ event }: {
|
|
44
|
+
event: Event;
|
|
45
|
+
}) => Promise<void>;
|
|
25
46
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openfleet",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "SPARR framework agents + infinite context for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk --external @anthropic-ai/sdk && tsc --emitDeclarationOnly && cp -r src/templates dist/",
|
|
23
23
|
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"test:watch": "bun test --watch",
|
|
25
27
|
"format": "prettier --write .",
|
|
26
28
|
"format:check": "prettier --check .",
|
|
27
29
|
"prepare": "husky"
|
|
@@ -36,9 +38,10 @@
|
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
41
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
39
42
|
"@opencode-ai/plugin": "~1.0.224",
|
|
40
43
|
"@opencode-ai/sdk": "~1.0.224",
|
|
41
|
-
"@
|
|
44
|
+
"@types/bun": "^1.3.10",
|
|
42
45
|
"@types/node": "^22",
|
|
43
46
|
"husky": "^9.1.7",
|
|
44
47
|
"lint-staged": "^16.2.7",
|