tabminal 3.0.13 → 3.0.15
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/AGENTS.md +5 -0
- package/package.json +1 -1
- package/public/app.js +2754 -259
- package/public/index.html +137 -35
- package/public/styles.css +380 -0
- package/src/acp-manager.mjs +180 -18
- package/src/fs-routes.mjs +148 -39
- package/src/server.mjs +69 -25
- package/src/terminal-manager.mjs +97 -2
- package/src/terminal-session.mjs +24 -1
package/src/acp-manager.mjs
CHANGED
|
@@ -16,6 +16,11 @@ const DEFAULT_TERMINAL_OUTPUT_LIMIT = 256 * 1024;
|
|
|
16
16
|
const DEFAULT_AVAILABILITY_OVERRIDE_TTL_MS = 30 * 1000;
|
|
17
17
|
const DEFAULT_PROBE_CACHE_TTL_MS = 15 * 1000;
|
|
18
18
|
const DEFAULT_TRANSCRIPT_PERSIST_DELAY_MS = 250;
|
|
19
|
+
const ALL_SESSION_AGENT_IDS = new Set([
|
|
20
|
+
'claude',
|
|
21
|
+
'codex',
|
|
22
|
+
'copilot'
|
|
23
|
+
]);
|
|
19
24
|
const TEXT_ATTACHMENT_EXTENSIONS = new Set([
|
|
20
25
|
'txt', 'md', 'markdown', 'json', 'jsonl', 'yaml', 'yml', 'toml',
|
|
21
26
|
'ini', 'env', 'xml', 'html', 'htm', 'css', 'scss', 'less', 'csv',
|
|
@@ -104,7 +109,8 @@ function buildAgentConfigSummary(agentId, config = {}) {
|
|
|
104
109
|
|
|
105
110
|
function normalizeAgentSessionCapabilities(
|
|
106
111
|
agentCapabilities = null,
|
|
107
|
-
connection = null
|
|
112
|
+
connection = null,
|
|
113
|
+
definitionId = ''
|
|
108
114
|
) {
|
|
109
115
|
const sessionCapabilities = (
|
|
110
116
|
agentCapabilities?.sessionCapabilities
|
|
@@ -121,6 +127,11 @@ function normalizeAgentSessionCapabilities(
|
|
|
121
127
|
sessionCapabilities?.list
|
|
122
128
|
&& typeof connection?.listSessions === 'function'
|
|
123
129
|
),
|
|
130
|
+
listAll: !!(
|
|
131
|
+
sessionCapabilities?.list
|
|
132
|
+
&& typeof connection?.listSessions === 'function'
|
|
133
|
+
&& ALL_SESSION_AGENT_IDS.has(String(definitionId || '').trim())
|
|
134
|
+
),
|
|
124
135
|
resume: !!(
|
|
125
136
|
sessionCapabilities?.resume
|
|
126
137
|
&& typeof connection?.unstable_resumeSession === 'function'
|
|
@@ -1514,17 +1525,23 @@ class AcpRuntime extends EventEmitter {
|
|
|
1514
1525
|
#getSessionCapabilities() {
|
|
1515
1526
|
const capabilities = normalizeAgentSessionCapabilities(
|
|
1516
1527
|
this.agentCapabilities,
|
|
1517
|
-
this.connection
|
|
1528
|
+
this.connection,
|
|
1529
|
+
this.definition?.id
|
|
1518
1530
|
);
|
|
1519
1531
|
if (
|
|
1520
1532
|
this.definition?.id === 'gemini'
|
|
1521
1533
|
&& this.#supportsGeminiCliSessionListing()
|
|
1522
1534
|
) {
|
|
1523
1535
|
capabilities.list = true;
|
|
1536
|
+
capabilities.listAll = false;
|
|
1524
1537
|
}
|
|
1525
1538
|
return capabilities;
|
|
1526
1539
|
}
|
|
1527
1540
|
|
|
1541
|
+
getSessionCapabilities() {
|
|
1542
|
+
return { ...this.#getSessionCapabilities() };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1528
1545
|
#supportsGeminiCliSessionListing() {
|
|
1529
1546
|
if (this.definition?.id !== 'gemini') return false;
|
|
1530
1547
|
const command = this.definition.command;
|
|
@@ -1577,6 +1594,32 @@ class AcpRuntime extends EventEmitter {
|
|
|
1577
1594
|
}));
|
|
1578
1595
|
}
|
|
1579
1596
|
|
|
1597
|
+
#supportsAllSessionListing() {
|
|
1598
|
+
return !!(
|
|
1599
|
+
this.#getSessionCapabilities().listAll
|
|
1600
|
+
&& typeof this.connection?.listSessions === 'function'
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
async #listSessionsViaConnection(options = {}) {
|
|
1605
|
+
const response = await this.connection.listSessions({
|
|
1606
|
+
cwd: options.all ? null : (options.cwd || this.cwd),
|
|
1607
|
+
cursor: options.cursor || null
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
sessions: Array.isArray(response?.sessions)
|
|
1611
|
+
? response.sessions
|
|
1612
|
+
.map(normalizeListedSessionInfo)
|
|
1613
|
+
.filter(
|
|
1614
|
+
(session) => session.sessionId && session.cwd
|
|
1615
|
+
)
|
|
1616
|
+
: [],
|
|
1617
|
+
nextCursor: typeof response?.nextCursor === 'string'
|
|
1618
|
+
? response.nextCursor
|
|
1619
|
+
: ''
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1580
1623
|
#resolveAvailableModes(availableModes, existingModes = []) {
|
|
1581
1624
|
if (Array.isArray(availableModes) && availableModes.length > 0) {
|
|
1582
1625
|
this.cachedAvailableModes = availableModes;
|
|
@@ -2023,27 +2066,24 @@ class AcpRuntime extends EventEmitter {
|
|
|
2023
2066
|
this.clearIdleShutdown();
|
|
2024
2067
|
|
|
2025
2068
|
const sessionCapabilities = this.#getSessionCapabilities();
|
|
2069
|
+
const wantsAll = !!options.all;
|
|
2026
2070
|
if (
|
|
2027
2071
|
sessionCapabilities.list
|
|
2028
2072
|
&& typeof this.connection?.listSessions === 'function'
|
|
2029
2073
|
) {
|
|
2074
|
+
if (wantsAll && this.#supportsAllSessionListing()) {
|
|
2075
|
+
return await this.#listSessionsViaConnection({
|
|
2076
|
+
cwd: options.cwd || this.cwd,
|
|
2077
|
+
cursor: options.cursor || '',
|
|
2078
|
+
all: true
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2030
2081
|
try {
|
|
2031
|
-
|
|
2082
|
+
return await this.#listSessionsViaConnection({
|
|
2032
2083
|
cwd: options.cwd || this.cwd,
|
|
2033
|
-
cursor: options.cursor ||
|
|
2084
|
+
cursor: options.cursor || '',
|
|
2085
|
+
all: false
|
|
2034
2086
|
});
|
|
2035
|
-
return {
|
|
2036
|
-
sessions: Array.isArray(response?.sessions)
|
|
2037
|
-
? response.sessions
|
|
2038
|
-
.map(normalizeListedSessionInfo)
|
|
2039
|
-
.filter(
|
|
2040
|
-
(session) => session.sessionId && session.cwd
|
|
2041
|
-
)
|
|
2042
|
-
: [],
|
|
2043
|
-
nextCursor: typeof response?.nextCursor === 'string'
|
|
2044
|
-
? response.nextCursor
|
|
2045
|
-
: ''
|
|
2046
|
-
};
|
|
2047
2087
|
} catch (error) {
|
|
2048
2088
|
const message = String(error?.message || '');
|
|
2049
2089
|
const canFallbackToCli = this.#supportsGeminiCliSessionListing();
|
|
@@ -3391,7 +3431,8 @@ export class AcpManager {
|
|
|
3391
3431
|
|
|
3392
3432
|
const sessionCapabilities = normalizeAgentSessionCapabilities(
|
|
3393
3433
|
runtime?.agentCapabilities,
|
|
3394
|
-
runtime?.connection
|
|
3434
|
+
runtime?.connection,
|
|
3435
|
+
runtime?.definition?.id
|
|
3395
3436
|
);
|
|
3396
3437
|
const hasSessionCapabilities = serialized.sessionCapabilities
|
|
3397
3438
|
&& typeof serialized.sessionCapabilities === 'object';
|
|
@@ -3671,11 +3712,19 @@ export class AcpManager {
|
|
|
3671
3712
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
3672
3713
|
const { runtimeEntry, createdRuntime, runtimeStoreKey } =
|
|
3673
3714
|
this.#ensureRuntimeEntry(definition, cwd);
|
|
3715
|
+
const cursor = typeof options.cursor === 'string' ? options.cursor : '';
|
|
3716
|
+
const sessionCapabilities = (
|
|
3717
|
+
typeof runtimeEntry.runtime.getSessionCapabilities === 'function'
|
|
3718
|
+
? runtimeEntry.runtime.getSessionCapabilities()
|
|
3719
|
+
: {}
|
|
3720
|
+
);
|
|
3721
|
+
const canListAll = !!(options.all && sessionCapabilities.listAll);
|
|
3674
3722
|
|
|
3675
3723
|
try {
|
|
3676
3724
|
const result = await runtimeEntry.runtime.listSessions({
|
|
3677
3725
|
cwd,
|
|
3678
|
-
|
|
3726
|
+
all: canListAll,
|
|
3727
|
+
cursor
|
|
3679
3728
|
});
|
|
3680
3729
|
this.#clearDefinitionAvailabilityOverride(definition.id);
|
|
3681
3730
|
if (runtimeEntry.runtime.tabs.size === 0) {
|
|
@@ -3696,6 +3745,119 @@ export class AcpManager {
|
|
|
3696
3745
|
}
|
|
3697
3746
|
}
|
|
3698
3747
|
|
|
3748
|
+
async listResumeSessions(options) {
|
|
3749
|
+
await this.ensureConfigsLoaded();
|
|
3750
|
+
const definition = this.definitions.find(
|
|
3751
|
+
(entry) => entry.id === options.agentId
|
|
3752
|
+
);
|
|
3753
|
+
if (!definition) {
|
|
3754
|
+
throw new Error('Unknown agent');
|
|
3755
|
+
}
|
|
3756
|
+
const availability = this.getDefinitionAvailability(definition);
|
|
3757
|
+
if (!availability.available) {
|
|
3758
|
+
throw new Error(availability.reason || 'Agent unavailable');
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
3762
|
+
const { runtimeEntry, createdRuntime, runtimeStoreKey } =
|
|
3763
|
+
this.#ensureRuntimeEntry(definition, cwd);
|
|
3764
|
+
const branchLimit = 500;
|
|
3765
|
+
const mergedLimit = 300;
|
|
3766
|
+
|
|
3767
|
+
const listBranch = async (all) => {
|
|
3768
|
+
const sessions = [];
|
|
3769
|
+
let nextCursor = '';
|
|
3770
|
+
const seenCursors = new Set();
|
|
3771
|
+
for (;;) {
|
|
3772
|
+
const result = await runtimeEntry.runtime.listSessions({
|
|
3773
|
+
cwd,
|
|
3774
|
+
all,
|
|
3775
|
+
cursor: nextCursor
|
|
3776
|
+
});
|
|
3777
|
+
sessions.push(...(Array.isArray(result?.sessions)
|
|
3778
|
+
? result.sessions
|
|
3779
|
+
: []));
|
|
3780
|
+
if (sessions.length >= branchLimit) {
|
|
3781
|
+
return sessions.slice(0, branchLimit);
|
|
3782
|
+
}
|
|
3783
|
+
const previousCursor = nextCursor;
|
|
3784
|
+
nextCursor = typeof result?.nextCursor === 'string'
|
|
3785
|
+
? result.nextCursor
|
|
3786
|
+
: '';
|
|
3787
|
+
if (!nextCursor) {
|
|
3788
|
+
break;
|
|
3789
|
+
}
|
|
3790
|
+
if (nextCursor === previousCursor || seenCursors.has(nextCursor)) {
|
|
3791
|
+
break;
|
|
3792
|
+
}
|
|
3793
|
+
seenCursors.add(nextCursor);
|
|
3794
|
+
}
|
|
3795
|
+
return sessions;
|
|
3796
|
+
};
|
|
3797
|
+
|
|
3798
|
+
const cwdPromise = listBranch(false);
|
|
3799
|
+
const allPromise = listBranch(true);
|
|
3800
|
+
|
|
3801
|
+
try {
|
|
3802
|
+
const settled = await Promise.allSettled([
|
|
3803
|
+
cwdPromise,
|
|
3804
|
+
allPromise
|
|
3805
|
+
]);
|
|
3806
|
+
const cwdResult = settled[0];
|
|
3807
|
+
const allResult = settled[1] || null;
|
|
3808
|
+
const cwdSessions = cwdResult?.status === 'fulfilled'
|
|
3809
|
+
? cwdResult.value
|
|
3810
|
+
: [];
|
|
3811
|
+
const allSessions = allResult?.status === 'fulfilled'
|
|
3812
|
+
&& Array.isArray(allResult.value)
|
|
3813
|
+
? allResult.value
|
|
3814
|
+
: [];
|
|
3815
|
+
|
|
3816
|
+
if (cwdSessions.length === 0 && allSessions.length === 0) {
|
|
3817
|
+
throw (
|
|
3818
|
+
cwdResult?.reason
|
|
3819
|
+
|| allResult?.reason
|
|
3820
|
+
|| new Error('Failed to list agent sessions')
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
const merged = [];
|
|
3825
|
+
const seen = new Set();
|
|
3826
|
+
for (const session of [...cwdSessions, ...allSessions]) {
|
|
3827
|
+
const sessionId = String(session?.sessionId || '').trim();
|
|
3828
|
+
if (!sessionId || seen.has(sessionId)) continue;
|
|
3829
|
+
seen.add(sessionId);
|
|
3830
|
+
merged.push(session);
|
|
3831
|
+
if (merged.length >= mergedLimit) {
|
|
3832
|
+
break;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
this.#clearDefinitionAvailabilityOverride(definition.id);
|
|
3837
|
+
if (runtimeEntry.runtime.tabs.size === 0) {
|
|
3838
|
+
runtimeEntry.runtime.scheduleIdleShutdown(async () => {
|
|
3839
|
+
if (runtimeEntry.runtime.tabs.size > 0) return;
|
|
3840
|
+
await this.#disposeRuntimeEntry(
|
|
3841
|
+
runtimeStoreKey,
|
|
3842
|
+
runtimeEntry
|
|
3843
|
+
);
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
return {
|
|
3848
|
+
sessions: merged,
|
|
3849
|
+
scope: allResult?.status === 'fulfilled'
|
|
3850
|
+
? 'merged'
|
|
3851
|
+
: 'cwd'
|
|
3852
|
+
};
|
|
3853
|
+
} catch (error) {
|
|
3854
|
+
if (createdRuntime && runtimeEntry.runtime.tabs.size === 0) {
|
|
3855
|
+
await this.#disposeRuntimeEntry(runtimeStoreKey, runtimeEntry);
|
|
3856
|
+
}
|
|
3857
|
+
throw error;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3699
3861
|
async resumeTab(options) {
|
|
3700
3862
|
await this.ensureConfigsLoaded();
|
|
3701
3863
|
const definition = this.definitions.find(
|
package/src/fs-routes.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { constants as fsConstants } from 'node:fs';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import process from 'node:process';
|
|
5
6
|
|
|
@@ -12,6 +13,11 @@ const IMAGE_MIME_TYPES = {
|
|
|
12
13
|
'.webp': 'image/webp'
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
const RAW_MIME_TYPES = {
|
|
17
|
+
...IMAGE_MIME_TYPES,
|
|
18
|
+
'.pdf': 'application/pdf'
|
|
19
|
+
};
|
|
20
|
+
|
|
15
21
|
export function isSupportedTextBuffer(buffer) {
|
|
16
22
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
17
23
|
return true;
|
|
@@ -45,6 +51,110 @@ export function isSupportedTextBuffer(buffer) {
|
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
function createFsRouteError(message, status) {
|
|
55
|
+
const error = new Error(message);
|
|
56
|
+
error.status = status;
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeFsRouteError(error, fallbackMessage = 'File system error') {
|
|
61
|
+
if (error?.status) {
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
66
|
+
const notFoundError = createFsRouteError('File not found', 404);
|
|
67
|
+
notFoundError.code = 'file-not-found';
|
|
68
|
+
return notFoundError;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (error?.code === 'EISDIR') {
|
|
72
|
+
return createFsRouteError('Not a file', 400);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedError = createFsRouteError(
|
|
76
|
+
error?.message || fallbackMessage,
|
|
77
|
+
500
|
|
78
|
+
);
|
|
79
|
+
if (error?.code) {
|
|
80
|
+
normalizedError.code = error.code;
|
|
81
|
+
}
|
|
82
|
+
return normalizedError;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildTextFileVersion(buffer) {
|
|
86
|
+
return crypto
|
|
87
|
+
.createHash('sha256')
|
|
88
|
+
.update(buffer)
|
|
89
|
+
.digest('hex');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function canWriteExistingFile(targetPath) {
|
|
93
|
+
try {
|
|
94
|
+
const handle = await fs.open(targetPath, 'r+');
|
|
95
|
+
await handle.close();
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readTextFileSnapshot(fullPath) {
|
|
103
|
+
try {
|
|
104
|
+
const stats = await fs.stat(fullPath);
|
|
105
|
+
|
|
106
|
+
if (!stats.isFile()) {
|
|
107
|
+
throw createFsRouteError('Not a file', 400);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (stats.size > 1024 * 1024 * 5) {
|
|
111
|
+
throw createFsRouteError('File too large', 400);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const contentBuffer = await fs.readFile(fullPath);
|
|
115
|
+
if (!isSupportedTextBuffer(contentBuffer)) {
|
|
116
|
+
const error = createFsRouteError('Unsupported file type', 415);
|
|
117
|
+
error.code = 'unsupported-file-type';
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
122
|
+
const content = decoder.decode(contentBuffer);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
content,
|
|
126
|
+
readonly: !(await canWriteExistingFile(fullPath)),
|
|
127
|
+
version: buildTextFileVersion(contentBuffer),
|
|
128
|
+
size: stats.size,
|
|
129
|
+
mtimeMs: stats.mtimeMs
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw normalizeFsRouteError(error, 'Unable to read file');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function writeTextFileSnapshot(
|
|
137
|
+
fullPath,
|
|
138
|
+
content,
|
|
139
|
+
expectedVersion = '',
|
|
140
|
+
force = false
|
|
141
|
+
) {
|
|
142
|
+
const current = await readTextFileSnapshot(fullPath);
|
|
143
|
+
if (
|
|
144
|
+
!force
|
|
145
|
+
&& expectedVersion
|
|
146
|
+
&& expectedVersion !== current.version
|
|
147
|
+
) {
|
|
148
|
+
const error = createFsRouteError('File version conflict', 409);
|
|
149
|
+
error.code = 'file-version-conflict';
|
|
150
|
+
error.snapshot = current;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
155
|
+
return await readTextFileSnapshot(fullPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
48
158
|
function joinRelativePath(basePath, name) {
|
|
49
159
|
if (!basePath || basePath === '.' || basePath === path.sep) {
|
|
50
160
|
return name;
|
|
@@ -325,50 +435,49 @@ export const setupFsRoutes = (router) => {
|
|
|
325
435
|
|
|
326
436
|
try {
|
|
327
437
|
const fullPath = resolvePath(baseDir, filePath);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
ctx.body = { error: 'Not a file' };
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (stats.size > 1024 * 1024 * 5) { // 5MB limit for now
|
|
337
|
-
ctx.status = 400;
|
|
338
|
-
ctx.body = { error: 'File too large' };
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const contentBuffer = await fs.readFile(fullPath);
|
|
343
|
-
if (!isSupportedTextBuffer(contentBuffer)) {
|
|
344
|
-
ctx.status = 415;
|
|
345
|
-
ctx.body = {
|
|
346
|
-
error: 'Unsupported file type',
|
|
347
|
-
code: 'unsupported-file-type'
|
|
348
|
-
};
|
|
349
|
-
return;
|
|
438
|
+
ctx.body = await readTextFileSnapshot(fullPath);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
if ((err?.status || 500) >= 500) {
|
|
441
|
+
console.error('FS Read Error:', err);
|
|
350
442
|
}
|
|
443
|
+
ctx.status = err?.status || 500;
|
|
444
|
+
ctx.body = {
|
|
445
|
+
error: err.message,
|
|
446
|
+
...(err?.code ? { code: err.code } : {})
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
});
|
|
351
450
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
} catch {
|
|
360
|
-
readonly = true;
|
|
361
|
-
}
|
|
451
|
+
router.get('/api/fs/info', async (ctx) => {
|
|
452
|
+
const filePath = ctx.query.path;
|
|
453
|
+
if (!filePath) {
|
|
454
|
+
ctx.status = 400;
|
|
455
|
+
ctx.body = { error: 'Path required' };
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
362
458
|
|
|
363
|
-
|
|
459
|
+
try {
|
|
460
|
+
const fullPath = resolvePath(baseDir, filePath);
|
|
461
|
+
const snapshot = await readTextFileSnapshot(fullPath);
|
|
462
|
+
ctx.body = {
|
|
463
|
+
readonly: snapshot.readonly,
|
|
464
|
+
version: snapshot.version,
|
|
465
|
+
size: snapshot.size,
|
|
466
|
+
mtimeMs: snapshot.mtimeMs
|
|
467
|
+
};
|
|
364
468
|
} catch (err) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
469
|
+
if ((err?.status || 500) >= 500) {
|
|
470
|
+
console.error('FS Info Error:', err);
|
|
471
|
+
}
|
|
472
|
+
ctx.status = err?.status || 500;
|
|
473
|
+
ctx.body = {
|
|
474
|
+
error: err.message,
|
|
475
|
+
...(err?.code ? { code: err.code } : {})
|
|
476
|
+
};
|
|
368
477
|
}
|
|
369
478
|
});
|
|
370
479
|
|
|
371
|
-
// Raw file access (for images)
|
|
480
|
+
// Raw file access (for previews like images and PDFs)
|
|
372
481
|
router.get('/api/fs/raw', async (ctx) => {
|
|
373
482
|
const filePath = ctx.query.path;
|
|
374
483
|
if (!filePath) {
|
|
@@ -380,8 +489,8 @@ export const setupFsRoutes = (router) => {
|
|
|
380
489
|
const fullPath = resolvePath(baseDir, filePath);
|
|
381
490
|
const ext = path.extname(fullPath).toLowerCase();
|
|
382
491
|
|
|
383
|
-
if (
|
|
384
|
-
ctx.type =
|
|
492
|
+
if (RAW_MIME_TYPES[ext]) {
|
|
493
|
+
ctx.type = RAW_MIME_TYPES[ext];
|
|
385
494
|
ctx.body = await fs.readFile(fullPath);
|
|
386
495
|
} else {
|
|
387
496
|
ctx.status = 400;
|
package/src/server.mjs
CHANGED
|
@@ -19,7 +19,10 @@ import { AcpManager } from './acp-manager.mjs';
|
|
|
19
19
|
import { SystemMonitor } from './system-monitor.mjs';
|
|
20
20
|
import { config } from './config.mjs';
|
|
21
21
|
import { authMiddleware, verifyClient } from './auth.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
setupFsRoutes,
|
|
24
|
+
writeTextFileSnapshot
|
|
25
|
+
} from './fs-routes.mjs';
|
|
23
26
|
import * as persistence from './persistence.mjs';
|
|
24
27
|
import { alan, network, web } from 'utilitas';
|
|
25
28
|
|
|
@@ -171,6 +174,22 @@ router.get('/healthz', (ctx) => {
|
|
|
171
174
|
ctx.body = { status: 'ok' };
|
|
172
175
|
});
|
|
173
176
|
|
|
177
|
+
app.use(async (ctx, next) => {
|
|
178
|
+
if (ctx.method === 'GET' && ctx.path === '/api/version') {
|
|
179
|
+
ctx.set(
|
|
180
|
+
'Cache-Control',
|
|
181
|
+
'no-store, no-cache, must-revalidate, proxy-revalidate'
|
|
182
|
+
);
|
|
183
|
+
ctx.set('Pragma', 'no-cache');
|
|
184
|
+
ctx.set('Expires', '0');
|
|
185
|
+
ctx.body = {
|
|
186
|
+
bootId: SERVER_BOOT_ID
|
|
187
|
+
};
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await next();
|
|
191
|
+
});
|
|
192
|
+
|
|
174
193
|
// Serve static files (public) BEFORE auth middleware
|
|
175
194
|
app.use(serve(publicDir));
|
|
176
195
|
|
|
@@ -206,6 +225,7 @@ setupFsRoutes(router);
|
|
|
206
225
|
|
|
207
226
|
// API routes for session management
|
|
208
227
|
router.all('/api/heartbeat', async (ctx) => {
|
|
228
|
+
const fileWriteResults = [];
|
|
209
229
|
if (ctx.method === 'POST') {
|
|
210
230
|
const { updates } = ctx.request.body;
|
|
211
231
|
if (updates && updates.sessions) {
|
|
@@ -223,13 +243,50 @@ router.all('/api/heartbeat', async (ctx) => {
|
|
|
223
243
|
});
|
|
224
244
|
}
|
|
225
245
|
if (update.fileWrites) {
|
|
246
|
+
const sessionResults = [];
|
|
226
247
|
for (const file of update.fileWrites) {
|
|
227
248
|
try {
|
|
228
|
-
await
|
|
249
|
+
const snapshot = await writeTextFileSnapshot(
|
|
250
|
+
file.path,
|
|
251
|
+
file.content,
|
|
252
|
+
file.expectedVersion,
|
|
253
|
+
file.force === true
|
|
254
|
+
);
|
|
255
|
+
sessionResults.push({
|
|
256
|
+
path: file.path,
|
|
257
|
+
status: 'ok',
|
|
258
|
+
version: snapshot.version,
|
|
259
|
+
readonly: snapshot.readonly
|
|
260
|
+
});
|
|
229
261
|
} catch (e) {
|
|
230
|
-
|
|
262
|
+
if (e?.status === 409) {
|
|
263
|
+
sessionResults.push({
|
|
264
|
+
path: file.path,
|
|
265
|
+
status: 'conflict',
|
|
266
|
+
version: e.snapshot?.version || '',
|
|
267
|
+
content: e.snapshot?.content || '',
|
|
268
|
+
readonly: !!e.snapshot?.readonly,
|
|
269
|
+
error: e.message
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
console.error(
|
|
274
|
+
`[Heartbeat] Write failed: ${file.path}`,
|
|
275
|
+
e
|
|
276
|
+
);
|
|
277
|
+
sessionResults.push({
|
|
278
|
+
path: file.path,
|
|
279
|
+
status: 'error',
|
|
280
|
+
error: e?.message || 'Write failed'
|
|
281
|
+
});
|
|
231
282
|
}
|
|
232
283
|
}
|
|
284
|
+
if (sessionResults.length > 0) {
|
|
285
|
+
fileWriteResults.push({
|
|
286
|
+
id: update.id,
|
|
287
|
+
fileWrites: sessionResults
|
|
288
|
+
});
|
|
289
|
+
}
|
|
233
290
|
}
|
|
234
291
|
}
|
|
235
292
|
}
|
|
@@ -239,6 +296,7 @@ router.all('/api/heartbeat', async (ctx) => {
|
|
|
239
296
|
ctx.body = {
|
|
240
297
|
sessions: terminalManager.listSessions(),
|
|
241
298
|
agents: await acpManager.listInventory(),
|
|
299
|
+
fileWriteResults,
|
|
242
300
|
system: systemMonitor.getStats(),
|
|
243
301
|
runtime: {
|
|
244
302
|
bootId: SERVER_BOOT_ID
|
|
@@ -344,7 +402,7 @@ router.get('/api/agents', async (ctx) => {
|
|
|
344
402
|
});
|
|
345
403
|
|
|
346
404
|
router.get('/api/agents/sessions', async (ctx) => {
|
|
347
|
-
const { agentId = '', cwd = ''
|
|
405
|
+
const { agentId = '', cwd = '' } = ctx.query || {};
|
|
348
406
|
if (!agentId || typeof agentId !== 'string') {
|
|
349
407
|
ctx.status = 400;
|
|
350
408
|
ctx.body = { error: 'agentId is required' };
|
|
@@ -357,28 +415,14 @@ router.get('/api/agents/sessions', async (ctx) => {
|
|
|
357
415
|
}
|
|
358
416
|
|
|
359
417
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const result = await acpManager.listSessions({
|
|
365
|
-
agentId,
|
|
366
|
-
cwd,
|
|
367
|
-
cursor: nextCursor
|
|
368
|
-
});
|
|
369
|
-
sessions.push(...(Array.isArray(result?.sessions)
|
|
370
|
-
? result.sessions
|
|
371
|
-
: []));
|
|
372
|
-
nextCursor = typeof result?.nextCursor === 'string'
|
|
373
|
-
? result.nextCursor
|
|
374
|
-
: '';
|
|
375
|
-
if (paginate || !nextCursor || sessions.length >= 50) {
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
418
|
+
const result = await acpManager.listResumeSessions({
|
|
419
|
+
agentId,
|
|
420
|
+
cwd
|
|
421
|
+
});
|
|
379
422
|
ctx.body = {
|
|
380
|
-
sessions:
|
|
381
|
-
nextCursor
|
|
423
|
+
sessions: Array.isArray(result?.sessions) ? result.sessions : [],
|
|
424
|
+
nextCursor: '',
|
|
425
|
+
scope: typeof result?.scope === 'string' ? result.scope : 'cwd'
|
|
382
426
|
};
|
|
383
427
|
} catch (error) {
|
|
384
428
|
const message = error?.message || 'Failed to list agent sessions';
|