farming-code 2.2.7 → 2.2.11
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 +110 -199
- package/README.zh_cn.md +106 -237
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1496 -737
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-iA2BU9P6.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-DpYuBonn.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const TERMINAL_STATE_VERSION = 1;
|
|
2
|
+
const MAX_SERIALIZED_TERMINAL_STATE_BYTES = 32 * 1024 * 1024;
|
|
3
|
+
const MAX_REPLAY_EVENT_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
function finitePositiveInteger(value, fallback) {
|
|
6
|
+
const parsed = Math.floor(Number(value));
|
|
7
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeReplayEvent(replayEvent) {
|
|
11
|
+
const events = Array.isArray(replayEvent?.events) ? replayEvent.events : [];
|
|
12
|
+
if (events.length !== 1) {
|
|
13
|
+
throw new Error('Serialized terminal state requires exactly one replay event');
|
|
14
|
+
}
|
|
15
|
+
const event = events[0] || {};
|
|
16
|
+
const data = typeof event.data === 'string' ? event.data : '';
|
|
17
|
+
if (Buffer.byteLength(data, 'utf8') > MAX_REPLAY_EVENT_BYTES) {
|
|
18
|
+
throw new Error('Serialized terminal replay event exceeds the size limit');
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
events: [{
|
|
22
|
+
data,
|
|
23
|
+
cols: finitePositiveInteger(event.cols, 80),
|
|
24
|
+
rows: finitePositiveInteger(event.rows, 30),
|
|
25
|
+
}],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeTerminalStateEntry(entry) {
|
|
30
|
+
if (!entry || typeof entry !== 'object') {
|
|
31
|
+
throw new Error('Serialized terminal state entry must be an object');
|
|
32
|
+
}
|
|
33
|
+
const id = typeof entry.id === 'string' ? entry.id.trim() : '';
|
|
34
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/.test(id)) {
|
|
35
|
+
throw new Error('Serialized terminal state has an invalid session id');
|
|
36
|
+
}
|
|
37
|
+
const metadata = entry.metadata && typeof entry.metadata === 'object' && !Array.isArray(entry.metadata)
|
|
38
|
+
? { ...entry.metadata }
|
|
39
|
+
: {};
|
|
40
|
+
const processDetails = entry.processDetails && typeof entry.processDetails === 'object'
|
|
41
|
+
? entry.processDetails
|
|
42
|
+
: {};
|
|
43
|
+
const processLaunchConfig = entry.processLaunchConfig && typeof entry.processLaunchConfig === 'object'
|
|
44
|
+
? entry.processLaunchConfig
|
|
45
|
+
: {};
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
metadata: {
|
|
49
|
+
...metadata,
|
|
50
|
+
agentId: id,
|
|
51
|
+
},
|
|
52
|
+
processDetails: {
|
|
53
|
+
cwd: typeof processDetails.cwd === 'string' ? processDetails.cwd : '',
|
|
54
|
+
title: typeof processDetails.title === 'string' ? processDetails.title : '',
|
|
55
|
+
},
|
|
56
|
+
processLaunchConfig: {
|
|
57
|
+
command: typeof processLaunchConfig.command === 'string' ? processLaunchConfig.command : '',
|
|
58
|
+
args: Array.isArray(processLaunchConfig.args)
|
|
59
|
+
? processLaunchConfig.args.filter(arg => typeof arg === 'string')
|
|
60
|
+
: [],
|
|
61
|
+
category: typeof processLaunchConfig.category === 'string' ? processLaunchConfig.category : '',
|
|
62
|
+
},
|
|
63
|
+
replayEvent: normalizeReplayEvent(entry.replayEvent),
|
|
64
|
+
timestamp: Number.isFinite(Number(entry.timestamp))
|
|
65
|
+
? Math.max(0, Math.floor(Number(entry.timestamp)))
|
|
66
|
+
: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function serializeTerminalState(entries) {
|
|
71
|
+
const state = (Array.isArray(entries) ? entries : []).map(normalizeTerminalStateEntry);
|
|
72
|
+
const ids = new Set();
|
|
73
|
+
for (const entry of state) {
|
|
74
|
+
if (ids.has(entry.id)) {
|
|
75
|
+
throw new Error(`Serialized terminal state contains duplicate session id ${entry.id}`);
|
|
76
|
+
}
|
|
77
|
+
ids.add(entry.id);
|
|
78
|
+
}
|
|
79
|
+
const serialized = JSON.stringify({
|
|
80
|
+
version: TERMINAL_STATE_VERSION,
|
|
81
|
+
state,
|
|
82
|
+
});
|
|
83
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_SERIALIZED_TERMINAL_STATE_BYTES) {
|
|
84
|
+
throw new Error('Serialized terminal state exceeds the size limit');
|
|
85
|
+
}
|
|
86
|
+
return serialized;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function deserializeTerminalState(serialized) {
|
|
90
|
+
if (typeof serialized !== 'string' || !serialized) return [];
|
|
91
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_SERIALIZED_TERMINAL_STATE_BYTES) {
|
|
92
|
+
throw new Error('Serialized terminal state exceeds the size limit');
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(serialized);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error('Serialized terminal state is not valid JSON');
|
|
99
|
+
}
|
|
100
|
+
if (!parsed || typeof parsed !== 'object' || parsed.version !== TERMINAL_STATE_VERSION || !Array.isArray(parsed.state)) {
|
|
101
|
+
throw new Error('Serialized terminal state has an unsupported format or version');
|
|
102
|
+
}
|
|
103
|
+
const state = parsed.state.map(normalizeTerminalStateEntry);
|
|
104
|
+
const ids = new Set();
|
|
105
|
+
for (const entry of state) {
|
|
106
|
+
if (ids.has(entry.id)) {
|
|
107
|
+
throw new Error(`Serialized terminal state contains duplicate session id ${entry.id}`);
|
|
108
|
+
}
|
|
109
|
+
ids.add(entry.id);
|
|
110
|
+
}
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function terminalReplayText(entry) {
|
|
115
|
+
const normalized = normalizeTerminalStateEntry(entry);
|
|
116
|
+
return normalized.replayEvent.events[0].data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
MAX_REPLAY_EVENT_BYTES,
|
|
121
|
+
MAX_SERIALIZED_TERMINAL_STATE_BYTES,
|
|
122
|
+
TERMINAL_STATE_VERSION,
|
|
123
|
+
deserializeTerminalState,
|
|
124
|
+
normalizeTerminalStateEntry,
|
|
125
|
+
serializeTerminalState,
|
|
126
|
+
terminalReplayText,
|
|
127
|
+
};
|
|
@@ -150,13 +150,12 @@ function latestTerminalKindFromText(title, previewText) {
|
|
|
150
150
|
|
|
151
151
|
function inferKindFromText(title, previewText, command) {
|
|
152
152
|
const commandName = executableName(command).toLowerCase();
|
|
153
|
-
if (commandName === 'claude') return 'claude';
|
|
154
|
-
if (commandName === 'codex') return 'codex';
|
|
155
153
|
if (DIRECT_PROCESS_AGENTS.has(commandName)) return 'process';
|
|
156
|
-
if (commandName && !SHELL_COMMANDS.has(commandName)) return 'process';
|
|
157
|
-
|
|
154
|
+
if (commandName && !SHELL_COMMANDS.has(commandName) && commandName !== 'codex' && commandName !== 'claude') return 'process';
|
|
158
155
|
const terminalKind = latestTerminalKindFromText(title, previewText);
|
|
159
156
|
if (terminalKind) return terminalKind;
|
|
157
|
+
if (commandName === 'claude') return 'claude';
|
|
158
|
+
if (commandName === 'codex') return 'codex';
|
|
160
159
|
if (SHELL_COMMANDS.has(commandName)) return 'shell';
|
|
161
160
|
return commandName ? 'process' : 'unknown';
|
|
162
161
|
}
|
|
@@ -350,7 +349,27 @@ function deriveTerminalStatus(options = {}) {
|
|
|
350
349
|
return status;
|
|
351
350
|
}
|
|
352
351
|
|
|
352
|
+
function terminalInputReady(options = {}) {
|
|
353
|
+
const status = deriveTerminalStatus(options);
|
|
354
|
+
if (status.activity !== 'idle') return false;
|
|
355
|
+
|
|
356
|
+
const previewText = typeof options.previewText === 'string' ? options.previewText : '';
|
|
357
|
+
if (status.kind === 'codex') {
|
|
358
|
+
const text = stripTerminalControlSequences(previewText).replace(/\r/g, '').toLowerCase();
|
|
359
|
+
return lastCodexIdleFooterIndex(text) >= 0 || codexBlockedIndex(text) >= 0;
|
|
360
|
+
}
|
|
361
|
+
if (status.kind === 'claude') {
|
|
362
|
+
const text = stripTerminalControlSequences(previewText).replace(/\r/g, '');
|
|
363
|
+
return /(?:^|\n)\s*❯(?:\s|$)/u.test(text);
|
|
364
|
+
}
|
|
365
|
+
if (status.kind === 'shell') {
|
|
366
|
+
return options.terminalBusy === false || terminalTextLooksIdleShellPrompt(previewText);
|
|
367
|
+
}
|
|
368
|
+
return Boolean(stripTerminalControlSequences(previewText).trim());
|
|
369
|
+
}
|
|
370
|
+
|
|
353
371
|
module.exports = {
|
|
354
372
|
deriveTerminalStatus,
|
|
373
|
+
terminalInputReady,
|
|
355
374
|
terminalTextLooksIdleShellPrompt,
|
|
356
375
|
};
|
package/backend/usage-monitor.js
CHANGED
|
@@ -13,6 +13,7 @@ const USAGE_TIMELINE_WINDOW_MS = 60 * 60 * 1000;
|
|
|
13
13
|
const USAGE_TIMELINE_BUCKET_COUNT = 30;
|
|
14
14
|
const USAGE_DAILY_DAYS = 52 * 7;
|
|
15
15
|
const USAGE_DAILY_CACHE_MS = 5 * 60 * 1000;
|
|
16
|
+
const USAGE_LIVE_DAY_CACHE_MS = 5 * 1000;
|
|
16
17
|
const JSONL_FILE_LIMIT = 60;
|
|
17
18
|
const JSONL_SCAN_LIMIT = 2000;
|
|
18
19
|
const DAILY_JSONL_FILE_LIMIT = 5000;
|
|
@@ -847,6 +848,7 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
847
848
|
const sessions = [];
|
|
848
849
|
const seenSessionIds = new Set();
|
|
849
850
|
let successfulHomes = 0;
|
|
851
|
+
let successfulExports = 0;
|
|
850
852
|
let partial = false;
|
|
851
853
|
let reason = '';
|
|
852
854
|
|
|
@@ -880,6 +882,7 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
880
882
|
const cacheKey = `${session.openCodeHome}:${session.id}`;
|
|
881
883
|
const cached = openCodeSessionEventCache.get(cacheKey);
|
|
882
884
|
if (cached?.updatedAt === session.updatedAt) {
|
|
885
|
+
successfulExports += 1;
|
|
883
886
|
events.push(...cached.events);
|
|
884
887
|
continue;
|
|
885
888
|
}
|
|
@@ -890,6 +893,7 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
890
893
|
);
|
|
891
894
|
const exported = JSON.parse(String(result?.stdout || '{}'));
|
|
892
895
|
const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now });
|
|
896
|
+
successfulExports += 1;
|
|
893
897
|
openCodeSessionEventCache.set(cacheKey, { updatedAt: session.updatedAt, events: sessionEvents });
|
|
894
898
|
events.push(...sessionEvents);
|
|
895
899
|
} catch (error) {
|
|
@@ -906,9 +910,11 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
906
910
|
return {
|
|
907
911
|
events,
|
|
908
912
|
partial,
|
|
909
|
-
available: successfulHomes
|
|
913
|
+
available: successfulHomes === homePaths.length
|
|
914
|
+
&& (sessions.length === 0 || successfulExports === sessions.length),
|
|
910
915
|
reason,
|
|
911
916
|
sessionCount: sessions.length,
|
|
917
|
+
exportCount: successfulExports,
|
|
912
918
|
};
|
|
913
919
|
}
|
|
914
920
|
|
|
@@ -980,6 +986,8 @@ async function collectUsageHistory(options = {}) {
|
|
|
980
986
|
available: openCode.available,
|
|
981
987
|
homeCount: openCodeHomes.length,
|
|
982
988
|
sessionCount: openCode.sessionCount,
|
|
989
|
+
exportCount: openCode.exportCount,
|
|
990
|
+
partial: openCode.partial,
|
|
983
991
|
source: 'opencode session export',
|
|
984
992
|
...(openCode.reason ? { reason: openCode.reason } : {}),
|
|
985
993
|
},
|
|
@@ -1195,10 +1203,13 @@ class UsageMonitor {
|
|
|
1195
1203
|
this.dailyDays = options.dailyDays ?? USAGE_DAILY_DAYS;
|
|
1196
1204
|
this.dailyCacheMs = options.dailyCacheMs ?? USAGE_DAILY_CACHE_MS;
|
|
1197
1205
|
this.dailyCache = { value: null, fetchedAt: 0, pending: null };
|
|
1206
|
+
this.liveDayCacheMs = options.liveDayCacheMs ?? USAGE_LIVE_DAY_CACHE_MS;
|
|
1207
|
+
this.liveDayCache = { date: '', value: null, fetchedAt: 0, pending: null };
|
|
1198
1208
|
}
|
|
1199
1209
|
|
|
1200
1210
|
invalidateDailyCache() {
|
|
1201
1211
|
this.dailyCache.fetchedAt = 0;
|
|
1212
|
+
this.liveDayCache.fetchedAt = 0;
|
|
1202
1213
|
}
|
|
1203
1214
|
|
|
1204
1215
|
getDailyUsage(options = {}) {
|
|
@@ -1232,8 +1243,60 @@ class UsageMonitor {
|
|
|
1232
1243
|
}
|
|
1233
1244
|
|
|
1234
1245
|
async getUsageDay(date, options = {}) {
|
|
1246
|
+
const now = options.now ?? Date.now();
|
|
1247
|
+
if (options.live === true && String(date || '').trim() === localDateKey(now)) {
|
|
1248
|
+
const liveDate = String(date).trim();
|
|
1249
|
+
const dailyFallback = this.dailyCache.value?.providerEvents
|
|
1250
|
+
? buildUsageDayDetail(this.dailyCache.value.providerEvents, { date: liveDate })
|
|
1251
|
+
: null;
|
|
1252
|
+
const cachedFallback = this.liveDayCache.date === liveDate
|
|
1253
|
+
? this.liveDayCache.value
|
|
1254
|
+
: null;
|
|
1255
|
+
const fallback = cachedFallback || dailyFallback;
|
|
1256
|
+
const recoverWithFallback = error => {
|
|
1257
|
+
if (!fallback) throw error;
|
|
1258
|
+
if (this.liveDayCache.date === liveDate) {
|
|
1259
|
+
this.liveDayCache.value = fallback;
|
|
1260
|
+
this.liveDayCache.fetchedAt = now;
|
|
1261
|
+
}
|
|
1262
|
+
return fallback;
|
|
1263
|
+
};
|
|
1264
|
+
if (
|
|
1265
|
+
options.fresh !== true
|
|
1266
|
+
&& this.liveDayCache.date === liveDate
|
|
1267
|
+
&& this.liveDayCache.value
|
|
1268
|
+
&& now - this.liveDayCache.fetchedAt < this.liveDayCacheMs
|
|
1269
|
+
) {
|
|
1270
|
+
return this.liveDayCache.value;
|
|
1271
|
+
}
|
|
1272
|
+
if (this.liveDayCache.pending && this.liveDayCache.date === liveDate) {
|
|
1273
|
+
return this.liveDayCache.pending.catch(recoverWithFallback);
|
|
1274
|
+
}
|
|
1275
|
+
this.liveDayCache.date = liveDate;
|
|
1276
|
+
const pending = collectUsageHistory({
|
|
1277
|
+
codexHome: this.codexHome,
|
|
1278
|
+
claudeHome: this.claudeHome,
|
|
1279
|
+
openCodeHome: this.openCodeHome,
|
|
1280
|
+
qoderHome: this.qoderHome,
|
|
1281
|
+
providerHomes: this.getProviderHomes ? this.getProviderHomes() : undefined,
|
|
1282
|
+
openCodeCommandRunner: this.openCodeCommandRunner,
|
|
1283
|
+
now,
|
|
1284
|
+
days: 1,
|
|
1285
|
+
}).then(history => {
|
|
1286
|
+
const detail = buildUsageDayDetail(history.providerEvents, { date: liveDate });
|
|
1287
|
+
if (this.liveDayCache.date === liveDate) {
|
|
1288
|
+
this.liveDayCache.value = detail;
|
|
1289
|
+
this.liveDayCache.fetchedAt = now;
|
|
1290
|
+
}
|
|
1291
|
+
return detail;
|
|
1292
|
+
}).finally(() => {
|
|
1293
|
+
if (this.liveDayCache.pending === pending) this.liveDayCache.pending = null;
|
|
1294
|
+
});
|
|
1295
|
+
this.liveDayCache.pending = pending;
|
|
1296
|
+
return pending.catch(recoverWithFallback);
|
|
1297
|
+
}
|
|
1235
1298
|
const history = await this.getDailyUsage({
|
|
1236
|
-
now
|
|
1299
|
+
now,
|
|
1237
1300
|
force: options.fresh === true,
|
|
1238
1301
|
});
|
|
1239
1302
|
return buildUsageDayDetail(history.providerEvents, { date });
|
|
@@ -1272,6 +1335,7 @@ class UsageMonitor {
|
|
|
1272
1335
|
claude: claudeUsage.tokenEvents,
|
|
1273
1336
|
opencode: openCodeUsage.tokenEvents,
|
|
1274
1337
|
}, { now, windowMs: historyWindowMs });
|
|
1338
|
+
const openCodeCoverage = history.coverage.find(entry => entry.provider === 'opencode');
|
|
1275
1339
|
const qoderCoverage = history.coverage.find(entry => entry.provider === 'qoder');
|
|
1276
1340
|
|
|
1277
1341
|
return {
|
|
@@ -1297,13 +1361,25 @@ class UsageMonitor {
|
|
|
1297
1361
|
{
|
|
1298
1362
|
provider: 'opencode',
|
|
1299
1363
|
providerName: 'OpenCode',
|
|
1300
|
-
auth: {
|
|
1364
|
+
auth: {
|
|
1365
|
+
available: openCodeCoverage?.available === true,
|
|
1366
|
+
status: openCodeCoverage?.available === true
|
|
1367
|
+
? 'Local session export'
|
|
1368
|
+
: openCodeCoverage?.reason || 'OpenCode unavailable',
|
|
1369
|
+
source: 'opencode session export',
|
|
1370
|
+
},
|
|
1301
1371
|
quota: {
|
|
1302
1372
|
available: false,
|
|
1303
1373
|
source: 'opencode session export',
|
|
1304
1374
|
reason: 'OpenCode session exports do not expose quota remaining.',
|
|
1305
1375
|
},
|
|
1306
|
-
tokenUsage:
|
|
1376
|
+
tokenUsage: openCodeCoverage?.available === true
|
|
1377
|
+
? openCodeUsage.tokenUsage
|
|
1378
|
+
: {
|
|
1379
|
+
...openCodeUsage.tokenUsage,
|
|
1380
|
+
available: false,
|
|
1381
|
+
reason: openCodeCoverage?.reason || 'OpenCode token usage is unavailable.',
|
|
1382
|
+
},
|
|
1307
1383
|
},
|
|
1308
1384
|
{
|
|
1309
1385
|
provider: 'qoder',
|
|
@@ -1340,6 +1416,7 @@ module.exports = {
|
|
|
1340
1416
|
USAGE_TIMELINE_BUCKET_COUNT,
|
|
1341
1417
|
USAGE_DAILY_DAYS,
|
|
1342
1418
|
USAGE_DAILY_CACHE_MS,
|
|
1419
|
+
USAGE_LIVE_DAY_CACHE_MS,
|
|
1343
1420
|
UsageMonitor,
|
|
1344
1421
|
buildUsageTimeline,
|
|
1345
1422
|
buildDailyUsage,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const CREATE_FORBIDDEN_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
|
|
7
|
+
const INVALID_PATH_CODES = new Set(['EINVAL', 'ENAMETOOLONG']);
|
|
8
|
+
|
|
9
|
+
function resolveWorkspaceDirectory(value, homeDir = process.env.HOME || os.homedir()) {
|
|
10
|
+
const input = typeof value === 'string' ? value.trim() : '';
|
|
11
|
+
if (!input) return '';
|
|
12
|
+
const expanded = input === '~'
|
|
13
|
+
? homeDir
|
|
14
|
+
: input.startsWith('~/')
|
|
15
|
+
? path.join(homeDir, input.slice(2))
|
|
16
|
+
: input;
|
|
17
|
+
return path.resolve(expanded);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function workspaceDirectoryError(error, workspace) {
|
|
21
|
+
const code = error?.code || '';
|
|
22
|
+
if (CREATE_FORBIDDEN_CODES.has(code)) {
|
|
23
|
+
return {
|
|
24
|
+
status: 403,
|
|
25
|
+
body: {
|
|
26
|
+
status: 'rejected',
|
|
27
|
+
code: 'workspace-create-forbidden',
|
|
28
|
+
workspace,
|
|
29
|
+
message: `Farming does not have permission to create this directory: ${workspace}`,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (code === 'ENOTDIR') {
|
|
34
|
+
return {
|
|
35
|
+
status: 409,
|
|
36
|
+
body: {
|
|
37
|
+
status: 'rejected',
|
|
38
|
+
code: 'workspace-parent-not-directory',
|
|
39
|
+
workspace,
|
|
40
|
+
message: `A parent path is not a directory: ${workspace}`,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (code === 'EEXIST') {
|
|
45
|
+
return {
|
|
46
|
+
status: 409,
|
|
47
|
+
body: {
|
|
48
|
+
status: 'rejected',
|
|
49
|
+
code: 'workspace-not-directory',
|
|
50
|
+
workspace,
|
|
51
|
+
message: `Workspace path is not a directory: ${workspace}`,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (INVALID_PATH_CODES.has(code)) {
|
|
56
|
+
return {
|
|
57
|
+
status: 400,
|
|
58
|
+
body: {
|
|
59
|
+
status: 'rejected',
|
|
60
|
+
code: 'workspace-invalid-path',
|
|
61
|
+
workspace,
|
|
62
|
+
message: 'Workspace path is invalid',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
status: 500,
|
|
68
|
+
body: {
|
|
69
|
+
status: 'rejected',
|
|
70
|
+
code: 'workspace-create-failed',
|
|
71
|
+
workspace,
|
|
72
|
+
message: `Failed to create workspace directory: ${workspace}`,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function prepareWorkspaceDirectory(value, options = {}) {
|
|
78
|
+
const workspace = resolveWorkspaceDirectory(value, options.homeDir);
|
|
79
|
+
if (!workspace) {
|
|
80
|
+
return {
|
|
81
|
+
status: 400,
|
|
82
|
+
body: {
|
|
83
|
+
status: 'rejected',
|
|
84
|
+
code: 'workspace-path-required',
|
|
85
|
+
workspace: '',
|
|
86
|
+
message: 'Workspace path is required',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fileSystem = options.fileSystem || fs.promises;
|
|
92
|
+
try {
|
|
93
|
+
const stat = await fileSystem.stat(workspace);
|
|
94
|
+
if (!stat.isDirectory()) {
|
|
95
|
+
return {
|
|
96
|
+
status: 409,
|
|
97
|
+
body: {
|
|
98
|
+
status: 'rejected',
|
|
99
|
+
code: 'workspace-not-directory',
|
|
100
|
+
workspace,
|
|
101
|
+
message: `Workspace path is not a directory: ${workspace}`,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { status: 200, body: { status: 'ready', workspace } };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.code !== 'ENOENT') return workspaceDirectoryError(error, workspace);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.create !== true) {
|
|
111
|
+
return {
|
|
112
|
+
status: 409,
|
|
113
|
+
body: {
|
|
114
|
+
status: 'missing',
|
|
115
|
+
code: 'workspace-not-found',
|
|
116
|
+
workspace,
|
|
117
|
+
message: `Workspace directory does not exist: ${workspace}`,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await fileSystem.mkdir(workspace, { recursive: true });
|
|
124
|
+
const stat = await fileSystem.stat(workspace);
|
|
125
|
+
if (!stat.isDirectory()) {
|
|
126
|
+
return workspaceDirectoryError({ code: 'EEXIST' }, workspace);
|
|
127
|
+
}
|
|
128
|
+
return { status: 201, body: { status: 'created', workspace } };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return workspaceDirectoryError(error, workspace);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createWorkspaceDirectoryRouter(options = {}) {
|
|
135
|
+
const router = express.Router();
|
|
136
|
+
router.use(express.json({ limit: '8kb' }));
|
|
137
|
+
router.post('/prepare', async (req, res) => {
|
|
138
|
+
const result = await prepareWorkspaceDirectory(req.body?.workspace, {
|
|
139
|
+
create: req.body?.create === true,
|
|
140
|
+
fileSystem: options.fileSystem,
|
|
141
|
+
homeDir: options.homeDir,
|
|
142
|
+
});
|
|
143
|
+
res.status(result.status).json(result.body);
|
|
144
|
+
});
|
|
145
|
+
return router;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
createWorkspaceDirectoryRouter,
|
|
150
|
+
prepareWorkspaceDirectory,
|
|
151
|
+
resolveWorkspaceDirectory,
|
|
152
|
+
};
|