@termfleet/core 0.1.3 → 0.2.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 +10 -2
- package/dist/agent-session-tail.js +58 -3
- package/dist/lib/exec.js +18 -1
- package/dist/local-tunnel.js +1 -1
- package/dist/provider-client.d.ts +4 -4
- package/dist/provider-client.js +3 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ this package does that standalone.
|
|
|
22
22
|
npm install @termfleet/core
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Requires **Node
|
|
25
|
+
Requires **Node 20+** and ESM (`"type": "module"`, or `import()`).
|
|
26
26
|
|
|
27
27
|
## Imports are explicit, per module
|
|
28
28
|
|
|
@@ -102,10 +102,18 @@ const off = client.onSnapshot((snapshot) => { /* … */ });
|
|
|
102
102
|
|
|
103
103
|
// create a fresh agent window (boot a claude/codex/gemini session)
|
|
104
104
|
await client.createAgentWindow({ /* AgentWindowCreateOptions */ });
|
|
105
|
+
|
|
106
|
+
// terminals + files: capture/send, and read/write through the provider filesystem
|
|
107
|
+
const { content } = await client.captureTerminal("term-1", 40);
|
|
108
|
+
await client.sendTerminalInput("term-1", "echo hi\n", { breakGlass: true });
|
|
109
|
+
const bytes = await client.readFile("term-1", "/path/file"); // Uint8Array (browser-safe)
|
|
110
|
+
const text = new TextDecoder().decode(bytes); // node: or Buffer.from(bytes)
|
|
105
111
|
```
|
|
106
112
|
|
|
107
113
|
Pass `{ authToken }` for an authenticated/shared provider, or a `urlResolver`
|
|
108
|
-
when proxying through a console; omit both for a direct connection.
|
|
114
|
+
when proxying through a console; omit both for a direct connection. `readFile`
|
|
115
|
+
returns a `Uint8Array` (not a node `Buffer`, since this client also runs in the
|
|
116
|
+
browser) — decode with `new TextDecoder().decode(bytes)` or `Buffer.from(bytes)`.
|
|
109
117
|
|
|
110
118
|
## What's inside
|
|
111
119
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { open, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
|
3
|
-
import { codexTranscriptPath, createClaudeSessionParser, emptyClaudeSession, listLocalAgentSubagents, parseCodexSessionJsonl, parseSubagentSessionId, resolveClaudeSubagentTranscriptPath, resolveClaudeTranscriptPath } from "./agent-session.js";
|
|
3
|
+
import { codexTranscriptPath, createClaudeSessionParser, emptyClaudeSession, listLocalAgentSubagents, parseCodexSessionJsonl, parseGeminiSessionJsonl, parseSubagentSessionId, resolveClaudeSubagentTranscriptPath, resolveClaudeTranscriptPath, resolveGeminiTranscriptPath } from "./agent-session.js";
|
|
4
4
|
// How many transcript lines are parsed between yields back to the event
|
|
5
5
|
// loop. Keeps a multi-megabyte initial catch-up from starving sockets.
|
|
6
6
|
const linesPerSlice = 1_000;
|
|
@@ -145,8 +145,34 @@ function cachedTail(options) {
|
|
|
145
145
|
}
|
|
146
146
|
return tail;
|
|
147
147
|
}
|
|
148
|
+
const maxCachedFullParses = 64;
|
|
149
|
+
const fullParseCache = new Map();
|
|
150
|
+
async function readStatCachedFullParse(options) {
|
|
151
|
+
const fileStat = await stat(options.transcriptPath);
|
|
152
|
+
const signature = `${options.kind}:${fileStat.size}:${fileStat.mtimeMs}`;
|
|
153
|
+
const key = `${options.sessionId}\0${options.transcriptPath}`;
|
|
154
|
+
const cached = fullParseCache.get(key);
|
|
155
|
+
if (cached && cached.signature === signature) {
|
|
156
|
+
// Unchanged file: no re-read, no re-parse. Refresh LRU order.
|
|
157
|
+
fullParseCache.delete(key);
|
|
158
|
+
fullParseCache.set(key, cached);
|
|
159
|
+
return { details: cached.details, signature };
|
|
160
|
+
}
|
|
161
|
+
const jsonl = await readFile(options.transcriptPath, "utf8");
|
|
162
|
+
const entry = { details: options.parse(jsonl), signature };
|
|
163
|
+
fullParseCache.delete(key);
|
|
164
|
+
fullParseCache.set(key, entry);
|
|
165
|
+
if (fullParseCache.size > maxCachedFullParses) {
|
|
166
|
+
const oldest = fullParseCache.keys().next().value;
|
|
167
|
+
if (oldest !== undefined) {
|
|
168
|
+
fullParseCache.delete(oldest);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { details: entry.details, signature };
|
|
172
|
+
}
|
|
148
173
|
export function clearAgentSessionTailCache() {
|
|
149
174
|
tailCache.clear();
|
|
175
|
+
fullParseCache.clear();
|
|
150
176
|
}
|
|
151
177
|
export async function readLocalAgentSessionTailed(options) {
|
|
152
178
|
if (options.sessionId.startsWith("codex:")) {
|
|
@@ -154,8 +180,37 @@ export async function readLocalAgentSessionTailed(options) {
|
|
|
154
180
|
if (!transcriptPath) {
|
|
155
181
|
throw new Error(`Codex session transcript was not found for ${options.sessionId} in ${options.cwd}`);
|
|
156
182
|
}
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
return readStatCachedFullParse({
|
|
184
|
+
kind: "codex",
|
|
185
|
+
parse: (jsonl) => parseCodexSessionJsonl({ jsonl, sessionId: options.sessionId, transcriptPath }),
|
|
186
|
+
sessionId: options.sessionId,
|
|
187
|
+
transcriptPath
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (options.sessionId.startsWith("gemini:")) {
|
|
191
|
+
const transcriptPath = resolveGeminiTranscriptPath(options);
|
|
192
|
+
if (!transcriptPath) {
|
|
193
|
+
// A just-launched Gemini chat with no transcript yet is live but empty —
|
|
194
|
+
// not a failure (mirrors readLocalGeminiSession's empty shape). The next
|
|
195
|
+
// tailed read resolves and parses once it writes messages.
|
|
196
|
+
return {
|
|
197
|
+
details: {
|
|
198
|
+
agentSessionId: options.sessionId,
|
|
199
|
+
endOfTurn: false,
|
|
200
|
+
lastAssistantText: "",
|
|
201
|
+
messages: [],
|
|
202
|
+
provider: "gemini",
|
|
203
|
+
sessionId: options.sessionId,
|
|
204
|
+
timeline: []
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return readStatCachedFullParse({
|
|
209
|
+
kind: "gemini",
|
|
210
|
+
parse: (jsonl) => parseGeminiSessionJsonl({ jsonl, sessionId: options.sessionId, transcriptPath }),
|
|
211
|
+
sessionId: options.sessionId,
|
|
212
|
+
transcriptPath
|
|
213
|
+
});
|
|
159
214
|
}
|
|
160
215
|
// A subagent sidechain, addressed by its composite id: tail its file directly
|
|
161
216
|
// (no nested subagents to list).
|
package/dist/lib/exec.js
CHANGED
|
@@ -105,10 +105,27 @@ async function spawnAsync(command, args, options, input) {
|
|
|
105
105
|
});
|
|
106
106
|
const stdout = [];
|
|
107
107
|
const stderr = [];
|
|
108
|
+
// Honor timeoutMs without blocking the loop (the sync `run` relied on
|
|
109
|
+
// spawnSync's timeout): SIGKILL the child and reject if it overruns.
|
|
110
|
+
const timer = options.timeoutMs === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: setTimeout(() => {
|
|
113
|
+
child.kill("SIGKILL");
|
|
114
|
+
reject(new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms.`));
|
|
115
|
+
}, options.timeoutMs);
|
|
116
|
+
timer?.unref?.();
|
|
117
|
+
const clear = () => {
|
|
118
|
+
if (timer)
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
};
|
|
108
121
|
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
109
122
|
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
110
|
-
child.on("error",
|
|
123
|
+
child.on("error", (error) => {
|
|
124
|
+
clear();
|
|
125
|
+
reject(error);
|
|
126
|
+
});
|
|
111
127
|
child.on("close", (code) => {
|
|
128
|
+
clear();
|
|
112
129
|
if (code !== 0) {
|
|
113
130
|
const detail = Buffer.concat(stderr).toString("utf8").trim();
|
|
114
131
|
reject(new Error(`${command} ${args.join(" ")} failed with exit ${code}.${detail ? `\n${detail}` : ""}`));
|
package/dist/local-tunnel.js
CHANGED
|
@@ -100,7 +100,7 @@ function startLocalTunnel(src, options) {
|
|
|
100
100
|
"--host", tunnelServerUrl,
|
|
101
101
|
// The tunnel layer stays in passthrough (no tunnel-level auth); the console's
|
|
102
102
|
// own remote-access gate is what requires a session for non-loopback requests.
|
|
103
|
-
//
|
|
103
|
+
// The reference tunnel client exposes this as --auth-not-required.
|
|
104
104
|
"--auth-not-required"
|
|
105
105
|
];
|
|
106
106
|
if (options.tunnelId) {
|
|
@@ -120,15 +120,15 @@ export declare class ProviderClient {
|
|
|
120
120
|
}): Promise<CommandAck<T>>;
|
|
121
121
|
listDirectory(terminalId: string, path: string): Promise<ProviderDirectoryList>;
|
|
122
122
|
listDirectoryByFilesystem(filesystemId: string, path: string): Promise<ProviderDirectoryList>;
|
|
123
|
-
readFile(terminalId: string, path: string): Promise<
|
|
124
|
-
readFileByFilesystem(filesystemId: string, path: string): Promise<
|
|
123
|
+
readFile(terminalId: string, path: string): Promise<Uint8Array>;
|
|
124
|
+
readFileByFilesystem(filesystemId: string, path: string): Promise<Uint8Array>;
|
|
125
125
|
statFile(terminalId: string, path: string): Promise<ProviderFileStat>;
|
|
126
126
|
statFileByFilesystem(filesystemId: string, path: string): Promise<ProviderFileStat>;
|
|
127
|
-
writeFile(terminalId: string, path: string, data:
|
|
127
|
+
writeFile(terminalId: string, path: string, data: Uint8Array, options?: {
|
|
128
128
|
mkdirs?: boolean;
|
|
129
129
|
mode?: string;
|
|
130
130
|
}): Promise<ProviderFileWriteResult>;
|
|
131
|
-
writeFileByFilesystem(filesystemId: string, path: string, data:
|
|
131
|
+
writeFileByFilesystem(filesystemId: string, path: string, data: Uint8Array, options?: {
|
|
132
132
|
mkdirs?: boolean;
|
|
133
133
|
mode?: string;
|
|
134
134
|
}): Promise<ProviderFileWriteResult>;
|
package/dist/provider-client.js
CHANGED
|
@@ -385,6 +385,9 @@ export class ProviderClient {
|
|
|
385
385
|
}
|
|
386
386
|
return await response.json();
|
|
387
387
|
}
|
|
388
|
+
// Returns raw bytes as a Uint8Array (browser-safe — this client also runs in the
|
|
389
|
+
// browser, where node's Buffer is absent). Node consumers decode with
|
|
390
|
+
// `Buffer.from(bytes)` or `new TextDecoder().decode(bytes)`.
|
|
388
391
|
async readFile(terminalId, path) {
|
|
389
392
|
const url = new URL(`${this.apiBase}/api/files`);
|
|
390
393
|
url.searchParams.set("terminalId", terminalId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@termfleet/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Termfleet core: contracts, the provider SDK, and the agent-transcript/session library (Claude Code / Codex / Gemini). The reusable layer, usable beyond the console.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -37,7 +37,10 @@
|
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsc -p tsconfig.json",
|
|
40
|
-
"
|
|
40
|
+
"prepack": "tsc -p tsconfig.json"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
41
44
|
},
|
|
42
45
|
"dependencies": {
|
|
43
46
|
"socket.io-client": "^4.8.3",
|