@termfleet/core 0.1.4 → 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 +1 -1
- package/dist/agent-session-tail.js +58 -3
- package/dist/lib/exec.js +18 -1
- package/dist/local-tunnel.js +1 -1
- package/package.json +4 -1
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
|
|
|
@@ -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) {
|
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",
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
"build": "tsc -p tsconfig.json",
|
|
40
40
|
"prepack": "tsc -p tsconfig.json"
|
|
41
41
|
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
42
45
|
"dependencies": {
|
|
43
46
|
"socket.io-client": "^4.8.3",
|
|
44
47
|
"yjs": "^13.6.31"
|