termify-agent 1.0.40 → 1.0.41
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/auth.d.ts.map +1 -1
- package/dist/auth.js +55 -0
- package/dist/auth.js.map +1 -1
- package/hooks/termify-response.js +151 -124
- package/hooks/termify-sync.js +165 -116
- package/package.json +1 -1
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAsHA;;;GAGG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,OAAO,CAAC,CAgGnB;AAED;;GAEG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAK5C"}
|
package/dist/auth.js
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
import open from 'open';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
4
7
|
import { getConfig, setAccessToken, setServerUrl } from './config.js';
|
|
5
8
|
import { getMachineId, getSystemInfo } from './utils/machine-id.js';
|
|
6
9
|
import { logger } from './utils/logger.js';
|
|
7
10
|
const VERSION = '1.0.5';
|
|
11
|
+
/**
|
|
12
|
+
* Write ~/.termify/config.json with the server endpoint and CLI token.
|
|
13
|
+
* Merges with existing config to avoid losing other endpoints.
|
|
14
|
+
*/
|
|
15
|
+
function writeTermifyCliConfig(serverUrl, cliToken) {
|
|
16
|
+
const termifyDir = join(homedir(), '.termify');
|
|
17
|
+
const configPath = join(termifyDir, 'config.json');
|
|
18
|
+
mkdirSync(termifyDir, { recursive: true });
|
|
19
|
+
// Derive a short name from the URL
|
|
20
|
+
const isLocal = serverUrl.includes('localhost') || serverUrl.includes('127.0.0.1');
|
|
21
|
+
const endpointName = isLocal ? 'local' : 'prod';
|
|
22
|
+
// Read existing config if present
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
let config = { endpoints: [] };
|
|
25
|
+
if (existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch { /* corrupted, overwrite */ }
|
|
30
|
+
}
|
|
31
|
+
// Migrate legacy format
|
|
32
|
+
if (!Array.isArray(config.endpoints)) {
|
|
33
|
+
config.endpoints = [];
|
|
34
|
+
if (config.token && config.apiUrl) {
|
|
35
|
+
config.endpoints.push({ name: 'default', apiUrl: config.apiUrl, token: config.token });
|
|
36
|
+
}
|
|
37
|
+
delete config.token;
|
|
38
|
+
delete config.apiUrl;
|
|
39
|
+
}
|
|
40
|
+
// Upsert endpoint by URL
|
|
41
|
+
const existing = config.endpoints.findIndex(
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
(ep) => ep.apiUrl === serverUrl);
|
|
44
|
+
const entry = { name: endpointName, apiUrl: serverUrl, token: cliToken };
|
|
45
|
+
if (existing >= 0) {
|
|
46
|
+
config.endpoints[existing] = entry;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
config.endpoints.push(entry);
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
52
|
+
logger.debug(`CLI config written to ${configPath}`);
|
|
53
|
+
}
|
|
8
54
|
/**
|
|
9
55
|
* Attempt to verify a pairing code with the server
|
|
10
56
|
*/
|
|
@@ -63,6 +109,9 @@ export async function login(options) {
|
|
|
63
109
|
const result = await verifyPairingCode(serverUrl, options.pairingCode);
|
|
64
110
|
if (result) {
|
|
65
111
|
setAccessToken(result.accessToken, result.agentId);
|
|
112
|
+
if (result.cliToken) {
|
|
113
|
+
writeTermifyCliConfig(serverUrl, result.cliToken);
|
|
114
|
+
}
|
|
66
115
|
spinner.succeed(chalk.green('Login successful!'));
|
|
67
116
|
console.log(chalk.gray(` Agent ID: ${result.agentId}`));
|
|
68
117
|
return true;
|
|
@@ -110,9 +159,15 @@ export async function login(options) {
|
|
|
110
159
|
const result = await verifyPairingCode(serverUrl, cleanCode);
|
|
111
160
|
if (result) {
|
|
112
161
|
setAccessToken(result.accessToken, result.agentId);
|
|
162
|
+
if (result.cliToken) {
|
|
163
|
+
writeTermifyCliConfig(serverUrl, result.cliToken);
|
|
164
|
+
}
|
|
113
165
|
spinner.succeed(chalk.green('Login successful!'));
|
|
114
166
|
console.log(chalk.gray(` Agent ID: ${result.agentId}`));
|
|
115
167
|
console.log(chalk.gray(` Config saved to: ${(await import('./config.js')).getConfigPath()}`));
|
|
168
|
+
if (result.cliToken) {
|
|
169
|
+
console.log(chalk.gray(` CLI config: ${join(homedir(), '.termify', 'config.json')}`));
|
|
170
|
+
}
|
|
116
171
|
console.log(chalk.cyan('\nRun `termify-agent start` to connect.\n'));
|
|
117
172
|
resolve(true);
|
|
118
173
|
}
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,OAAO,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Termify Response Sync Hook (Stop / AfterAgent)
|
|
4
4
|
*
|
|
5
|
-
* Syncs the assistant's last response to
|
|
5
|
+
* Syncs the assistant's last response to all Termify endpoints
|
|
6
6
|
* when Claude Code / Gemini finishes its turn.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* so the full reasoning process is visible in the Termify UI.
|
|
10
|
-
*
|
|
11
|
-
* Uses transcript_path hash to find the session file created by termify-sync.js.
|
|
8
|
+
* Supports multiple endpoints (local + prod) — sends to all in parallel.
|
|
12
9
|
*
|
|
13
10
|
* Token: ~/.termify/config.json
|
|
14
11
|
* Session: /tmp/termify-session-{hash}.json
|
|
@@ -18,79 +15,101 @@ const fs = require('fs');
|
|
|
18
15
|
const path = require('path');
|
|
19
16
|
const crypto = require('crypto');
|
|
20
17
|
|
|
21
|
-
/**
|
|
22
|
-
* Fix Gemini CLI streaming bug where prompt_response contains the response
|
|
23
|
-
* text duplicated (full_response + partial_repeat_from_midpoint).
|
|
24
|
-
* Detects and removes the duplicated tail.
|
|
25
|
-
*/
|
|
26
18
|
function deduplicateGeminiResponse(text) {
|
|
27
19
|
if (!text || text.length < 200) return text;
|
|
28
|
-
|
|
29
20
|
const len = text.length;
|
|
30
|
-
|
|
31
|
-
// Scan for a split point where the tail matches earlier content
|
|
32
21
|
for (let splitAt = Math.floor(len * 0.35); splitAt <= Math.floor(len * 0.65); splitAt++) {
|
|
33
22
|
const tail = text.slice(splitAt);
|
|
34
|
-
|
|
35
|
-
// The duplicated portion must be significant (>25% of total)
|
|
36
23
|
if (tail.length < len * 0.25) continue;
|
|
37
|
-
|
|
38
|
-
// Use first 80 chars of tail as search needle
|
|
39
24
|
const needleLen = Math.min(80, tail.length);
|
|
40
25
|
const needle = tail.slice(0, needleLen);
|
|
41
|
-
|
|
42
|
-
// Search for this needle BEFORE the split point
|
|
43
26
|
const idx = text.indexOf(needle);
|
|
44
27
|
if (idx >= 0 && idx < splitAt) {
|
|
45
|
-
// Verify longer match (at least 100 chars must match)
|
|
46
28
|
const matchLen = Math.min(tail.length, len - idx, 300);
|
|
47
29
|
if (matchLen > 100 && tail.slice(0, matchLen) === text.slice(idx, idx + matchLen)) {
|
|
48
30
|
return text.slice(0, splitAt).trimEnd();
|
|
49
31
|
}
|
|
50
32
|
}
|
|
51
33
|
}
|
|
52
|
-
|
|
53
34
|
return text;
|
|
54
35
|
}
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
function extractAllThinking(transcriptPath) {
|
|
37
|
+
function extractTranscriptEvents(transcriptPath) {
|
|
38
|
+
const result = { thinkings: [], toolEvents: [] };
|
|
60
39
|
try {
|
|
61
40
|
const data = fs.readFileSync(transcriptPath, 'utf-8');
|
|
62
41
|
const lines = data.trim().split('\n');
|
|
63
|
-
|
|
64
|
-
const thinkings = [];
|
|
42
|
+
const entries = [];
|
|
65
43
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
66
44
|
try {
|
|
67
45
|
const entry = JSON.parse(lines[i]);
|
|
68
|
-
// Stop at user message (end of current assistant turn)
|
|
69
46
|
if (entry.type === 'user' && !entry.toolUseResult && !entry.sourceToolAssistantUUID) break;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
47
|
+
entries.unshift(entry);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.type !== 'assistant' || !entry.message) continue;
|
|
52
|
+
let msg;
|
|
53
|
+
try {
|
|
54
|
+
msg = typeof entry.message === 'string' ? JSON.parse(entry.message) : entry.message;
|
|
55
|
+
} catch {
|
|
56
|
+
try {
|
|
57
|
+
msg = typeof entry.message === 'string' ? JSON.parse(entry.message.replace(/'/g, '"')) : entry.message;
|
|
58
|
+
} catch { continue; }
|
|
59
|
+
}
|
|
60
|
+
const content = msg?.content;
|
|
61
|
+
if (!Array.isArray(content)) continue;
|
|
62
|
+
for (const block of content) {
|
|
63
|
+
if (block?.type === 'thinking' && block.thinking) {
|
|
64
|
+
result.thinkings.push(block.thinking);
|
|
65
|
+
}
|
|
66
|
+
if (block?.type === 'tool_use' && block.name) {
|
|
67
|
+
const toolEvent = { type: 'tool_use', tool: block.name };
|
|
68
|
+
if (block.input) {
|
|
69
|
+
const displayKeys = ['file_path', 'command', 'pattern', 'query', 'description', 'url', 'prompt', 'skill', 'subagent_type', 'old_string', 'notebook_path'];
|
|
70
|
+
const slim = {};
|
|
71
|
+
for (const k of displayKeys) {
|
|
72
|
+
if (block.input[k] != null) {
|
|
73
|
+
const v = String(block.input[k]);
|
|
74
|
+
slim[k] = v.length > 200 ? v.substring(0, 197) + '...' : v;
|
|
75
|
+
}
|
|
84
76
|
}
|
|
77
|
+
if (Object.keys(slim).length > 0) toolEvent.args = slim;
|
|
78
|
+
}
|
|
79
|
+
result.toolEvents.push(toolEvent);
|
|
80
|
+
if (block.name === 'AskUserQuestion' && block.input?.questions) {
|
|
81
|
+
result.toolEvents.push({
|
|
82
|
+
type: 'question',
|
|
83
|
+
questionId: block.id || `q-${Date.now()}`,
|
|
84
|
+
questions: block.input.questions,
|
|
85
|
+
});
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
}
|
|
88
|
+
}
|
|
88
89
|
}
|
|
90
|
+
} catch {}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Read endpoints from config. Supports both old and new format.
|
|
96
|
+
*/
|
|
97
|
+
function readEndpoints() {
|
|
98
|
+
try {
|
|
99
|
+
const configPath = path.join(process.env.HOME || '', '.termify', 'config.json');
|
|
100
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
101
|
+
if (Array.isArray(config.endpoints) && config.endpoints.length > 0) {
|
|
102
|
+
return config.endpoints.map(ep => ({
|
|
103
|
+
name: ep.name || 'default',
|
|
104
|
+
apiUrl: ep.apiUrl,
|
|
105
|
+
token: ep.token,
|
|
106
|
+
})).filter(ep => ep.apiUrl && ep.token);
|
|
107
|
+
}
|
|
108
|
+
if (config.token) {
|
|
109
|
+
return [{ name: 'default', apiUrl: config.apiUrl || 'http://localhost:3001', token: config.token }];
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
return [];
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
// Skip sync when running inside a Termify-managed chat terminal
|
|
@@ -109,7 +128,6 @@ process.stdin.on('end', async () => {
|
|
|
109
128
|
return;
|
|
110
129
|
}
|
|
111
130
|
|
|
112
|
-
// Prevent infinite loops
|
|
113
131
|
if (hookData?.stop_hook_active) {
|
|
114
132
|
process.exit(0);
|
|
115
133
|
return;
|
|
@@ -117,8 +135,6 @@ process.stdin.on('end', async () => {
|
|
|
117
135
|
|
|
118
136
|
const transcriptPath = hookData?.transcript_path;
|
|
119
137
|
const hookEvent = hookData?.hook_event_name || '';
|
|
120
|
-
|
|
121
|
-
// Claude Code: last_assistant_message, Gemini CLI: prompt_response
|
|
122
138
|
let lastMessage = hookData?.last_assistant_message || hookData?.prompt_response;
|
|
123
139
|
|
|
124
140
|
if (!transcriptPath || !lastMessage) {
|
|
@@ -126,15 +142,11 @@ process.stdin.on('end', async () => {
|
|
|
126
142
|
return;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
// Trim leading whitespace from Gemini responses (they often start with spaces)
|
|
130
145
|
lastMessage = lastMessage.trimStart();
|
|
131
|
-
|
|
132
|
-
// Fix Gemini CLI streaming duplication bug
|
|
133
146
|
if (hookEvent === 'AfterAgent') {
|
|
134
147
|
lastMessage = deduplicateGeminiResponse(lastMessage);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
// Find session file
|
|
138
150
|
const sessionHash = crypto.createHash('md5').update(transcriptPath).digest('hex').substring(0, 12);
|
|
139
151
|
const sessionFile = path.join('/tmp', `termify-session-${sessionHash}.json`);
|
|
140
152
|
|
|
@@ -146,99 +158,114 @@ process.stdin.on('end', async () => {
|
|
|
146
158
|
return;
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
// Legacy single-endpoint session: convert to multi-endpoint format
|
|
162
|
+
if (session.conversationId && !session.endpoints) {
|
|
163
|
+
const endpoints = readEndpoints();
|
|
164
|
+
const defaultName = endpoints[0]?.name || 'default';
|
|
165
|
+
session.endpoints = {
|
|
166
|
+
[defaultName]: {
|
|
167
|
+
conversationId: session.conversationId,
|
|
168
|
+
assistantMessageId: session.assistantMessageId,
|
|
169
|
+
}
|
|
170
|
+
};
|
|
152
171
|
}
|
|
153
172
|
|
|
154
|
-
|
|
155
|
-
let config;
|
|
156
|
-
try {
|
|
157
|
-
const configPath = path.join(process.env.HOME || '', '.termify', 'config.json');
|
|
158
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
159
|
-
} catch {
|
|
173
|
+
if (!session.endpoints || Object.keys(session.endpoints).length === 0) {
|
|
160
174
|
process.exit(0);
|
|
161
175
|
return;
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
|
|
178
|
+
const endpoints = readEndpoints();
|
|
179
|
+
if (endpoints.length === 0) {
|
|
165
180
|
process.exit(0);
|
|
166
181
|
return;
|
|
167
182
|
}
|
|
168
183
|
|
|
169
|
-
const
|
|
184
|
+
const { thinkings, toolEvents } = extractTranscriptEvents(transcriptPath);
|
|
185
|
+
const alreadySent = session.thinkingsSent || 0;
|
|
170
186
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Sync response to a single endpoint.
|
|
189
|
+
*/
|
|
190
|
+
async function syncResponseToEndpoint(ep) {
|
|
191
|
+
const epState = session.endpoints[ep.name];
|
|
192
|
+
if (!epState?.conversationId) return;
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
const { conversationId, assistantMessageId } = epState;
|
|
195
|
+
const headers = {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
'Authorization': `Bearer ${ep.token}`,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// CRITICAL: complete message + set idle
|
|
201
|
+
try {
|
|
202
|
+
if (assistantMessageId) {
|
|
203
|
+
const res = await fetch(
|
|
204
|
+
`${ep.apiUrl}/api/chat/conversations/${conversationId}/messages/${assistantMessageId}/complete`,
|
|
205
|
+
{
|
|
206
|
+
method: 'POST', headers,
|
|
207
|
+
body: JSON.stringify({ content: lastMessage }),
|
|
208
|
+
signal: AbortSignal.timeout(5000),
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
if (!res.ok) throw new Error('complete failed');
|
|
212
|
+
} else {
|
|
213
|
+
await Promise.allSettled([
|
|
214
|
+
fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}/messages`, {
|
|
215
|
+
method: 'POST', headers,
|
|
216
|
+
body: JSON.stringify({ role: 'assistant', content: lastMessage }),
|
|
217
|
+
signal: AbortSignal.timeout(5000),
|
|
218
|
+
}),
|
|
219
|
+
fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}`, {
|
|
220
|
+
method: 'PATCH', headers,
|
|
221
|
+
body: JSON.stringify({ status: 'idle' }),
|
|
222
|
+
signal: AbortSignal.timeout(3000),
|
|
223
|
+
}),
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Fallback
|
|
193
228
|
await Promise.allSettled([
|
|
194
|
-
fetch(`${apiUrl}/api/chat/conversations/${
|
|
195
|
-
method: 'POST',
|
|
196
|
-
headers,
|
|
229
|
+
fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}/messages`, {
|
|
230
|
+
method: 'POST', headers,
|
|
197
231
|
body: JSON.stringify({ role: 'assistant', content: lastMessage }),
|
|
198
232
|
signal: AbortSignal.timeout(5000),
|
|
199
233
|
}),
|
|
200
|
-
fetch(`${apiUrl}/api/chat/conversations/${
|
|
201
|
-
method: 'PATCH',
|
|
202
|
-
headers,
|
|
234
|
+
fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}`, {
|
|
235
|
+
method: 'PATCH', headers,
|
|
203
236
|
body: JSON.stringify({ status: 'idle' }),
|
|
204
237
|
signal: AbortSignal.timeout(3000),
|
|
205
238
|
}),
|
|
206
239
|
]);
|
|
207
240
|
}
|
|
208
|
-
} catch {
|
|
209
|
-
// /complete failed — fallback: create message + PATCH idle in parallel
|
|
210
|
-
await Promise.allSettled([
|
|
211
|
-
fetch(`${apiUrl}/api/chat/conversations/${session.conversationId}/messages`, {
|
|
212
|
-
method: 'POST',
|
|
213
|
-
headers,
|
|
214
|
-
body: JSON.stringify({ role: 'assistant', content: lastMessage }),
|
|
215
|
-
signal: AbortSignal.timeout(5000),
|
|
216
|
-
}),
|
|
217
|
-
fetch(`${apiUrl}/api/chat/conversations/${session.conversationId}`, {
|
|
218
|
-
method: 'PATCH',
|
|
219
|
-
headers,
|
|
220
|
-
body: JSON.stringify({ status: 'idle' }),
|
|
221
|
-
signal: AbortSignal.timeout(3000),
|
|
222
|
-
}),
|
|
223
|
-
]);
|
|
224
|
-
}
|
|
225
241
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
events
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
// NON-CRITICAL: stream events
|
|
243
|
+
if (assistantMessageId) {
|
|
244
|
+
const eventsUrl = `${ep.apiUrl}/api/chat/conversations/${conversationId}/messages/${assistantMessageId}/events`;
|
|
245
|
+
const events = [];
|
|
246
|
+
for (let i = alreadySent; i < thinkings.length; i++) {
|
|
247
|
+
events.push({ type: 'thinking', content: thinkings[i] });
|
|
248
|
+
}
|
|
249
|
+
for (const te of toolEvents) {
|
|
250
|
+
events.push(te);
|
|
251
|
+
}
|
|
252
|
+
events.push({ type: 'text', content: lastMessage });
|
|
253
|
+
events.push({ type: 'status', status: 'done' });
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await fetch(eventsUrl, {
|
|
257
|
+
method: 'POST', headers,
|
|
258
|
+
body: JSON.stringify({ events }),
|
|
259
|
+
signal: AbortSignal.timeout(5000),
|
|
260
|
+
});
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
241
263
|
}
|
|
242
264
|
|
|
265
|
+
// Sync all endpoints in parallel
|
|
266
|
+
await Promise.allSettled(
|
|
267
|
+
endpoints.map(ep => syncResponseToEndpoint(ep))
|
|
268
|
+
);
|
|
269
|
+
|
|
243
270
|
process.exit(0);
|
|
244
271
|
});
|
package/hooks/termify-sync.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Syncs Claude Code & Gemini conversations to Termify chat.
|
|
6
6
|
* On each prompt: creates/reuses a conversation and adds the user message.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* transcript as "prompt". We extract just the user's actual message.
|
|
8
|
+
* Supports multiple endpoints (local + prod) — sends to all in parallel.
|
|
10
9
|
*
|
|
11
10
|
* Token: ~/.termify/config.json
|
|
12
11
|
* Session: /tmp/termify-session-{hash}.json
|
|
@@ -16,66 +15,63 @@ const fs = require('fs');
|
|
|
16
15
|
const path = require('path');
|
|
17
16
|
const crypto = require('crypto');
|
|
18
17
|
|
|
19
|
-
/**
|
|
20
|
-
* Extract the actual user message from a prompt that might contain
|
|
21
|
-
* compressed transcript or system-reminder tags.
|
|
22
|
-
*/
|
|
23
|
-
/**
|
|
24
|
-
* Detect if the prompt is a compacted/compressed context rather than a real user message.
|
|
25
|
-
* Claude Code sends the full compressed transcript as prompt when context is compacted.
|
|
26
|
-
*/
|
|
27
18
|
function isCompactedContext(raw) {
|
|
28
19
|
if (!raw) return false;
|
|
29
|
-
// Compacted prompts are huge and contain summary markers
|
|
30
20
|
if (raw.length > 5000 && raw.includes('<system-reminder>')) return true;
|
|
31
|
-
// Starts with the transcript compaction header
|
|
32
21
|
if (raw.startsWith('This session is being continued from a previous conversation')) return true;
|
|
33
|
-
// Contains compaction-specific markers
|
|
34
22
|
if (raw.includes('conversation that ran out of context') && raw.includes('summary below')) return true;
|
|
35
23
|
return false;
|
|
36
24
|
}
|
|
37
25
|
|
|
38
26
|
function extractUserMessage(raw) {
|
|
39
27
|
if (!raw) return null;
|
|
40
|
-
|
|
41
|
-
// Skip compacted context entirely — not a real user message
|
|
42
28
|
if (isCompactedContext(raw)) return null;
|
|
43
|
-
|
|
44
|
-
// Clean prompt (no system markers, reasonable size) — use as-is
|
|
45
29
|
if (raw.length < 2000 && !raw.includes('<system-reminder>') && !raw.startsWith('===')) {
|
|
46
30
|
return raw.trim();
|
|
47
31
|
}
|
|
48
|
-
|
|
49
|
-
// Compressed context: extract the last user message after all system tags
|
|
50
|
-
// Pattern: ...system stuff...\n</system-reminder>\nactual user message
|
|
51
32
|
const parts = raw.split('</system-reminder>');
|
|
52
33
|
if (parts.length > 1) {
|
|
53
34
|
const lastPart = parts[parts.length - 1].trim();
|
|
54
|
-
// Must look like a real message, not continuation instructions
|
|
55
35
|
if (lastPart && lastPart.length > 0 && lastPart.length < 5000 && !isCompactedContext(lastPart)) {
|
|
56
36
|
return lastPart;
|
|
57
37
|
}
|
|
58
38
|
}
|
|
59
|
-
|
|
60
|
-
// Fallback: if starts with === Transcript, skip entirely
|
|
61
|
-
if (raw.startsWith('===')) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Last resort: take first 500 chars
|
|
39
|
+
if (raw.startsWith('===')) return null;
|
|
66
40
|
return raw.substring(0, 500).trim() || null;
|
|
67
41
|
}
|
|
68
42
|
|
|
69
|
-
/**
|
|
70
|
-
* Detect which CLI is running from hook event name.
|
|
71
|
-
*/
|
|
72
43
|
function detectProvider(hookData) {
|
|
73
44
|
const event = hookData?.hook_event_name;
|
|
74
45
|
if (event === 'BeforeAgent') return 'gemini';
|
|
75
|
-
// Default to claude for Claude Code
|
|
76
46
|
return 'claude';
|
|
77
47
|
}
|
|
78
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Read endpoints from config. Supports both old format (single token/apiUrl)
|
|
51
|
+
* and new format (endpoints array with per-endpoint tokens).
|
|
52
|
+
*/
|
|
53
|
+
function readEndpoints() {
|
|
54
|
+
try {
|
|
55
|
+
const configPath = path.join(process.env.HOME || '', '.termify', 'config.json');
|
|
56
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
57
|
+
|
|
58
|
+
// New format: endpoints array
|
|
59
|
+
if (Array.isArray(config.endpoints) && config.endpoints.length > 0) {
|
|
60
|
+
return config.endpoints.map(ep => ({
|
|
61
|
+
name: ep.name || 'default',
|
|
62
|
+
apiUrl: ep.apiUrl,
|
|
63
|
+
token: ep.token,
|
|
64
|
+
})).filter(ep => ep.apiUrl && ep.token);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Legacy format: single token + apiUrl
|
|
68
|
+
if (config.token) {
|
|
69
|
+
return [{ name: 'default', apiUrl: config.apiUrl || 'http://localhost:3001', token: config.token }];
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
79
75
|
// Flag: running inside a Termify-managed chat terminal
|
|
80
76
|
const isTermifyManaged = !!process.env.TERMIFY_CONVERSATION_ID;
|
|
81
77
|
|
|
@@ -96,7 +92,6 @@ process.stdin.on('end', async () => {
|
|
|
96
92
|
return;
|
|
97
93
|
}
|
|
98
94
|
|
|
99
|
-
// Session key from transcript_path
|
|
100
95
|
const sessionHash = crypto.createHash('md5').update(transcriptPath).digest('hex').substring(0, 12);
|
|
101
96
|
const sessionFile = path.join('/tmp', `termify-session-${sessionHash}.json`);
|
|
102
97
|
|
|
@@ -116,132 +111,186 @@ process.stdin.on('end', async () => {
|
|
|
116
111
|
return;
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
// ── External CLI:
|
|
114
|
+
// ── External CLI: multi-endpoint sync ──
|
|
120
115
|
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
116
|
+
const endpoints = readEndpoints();
|
|
117
|
+
if (endpoints.length === 0) {
|
|
123
118
|
process.exit(0);
|
|
124
119
|
return;
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Compacted context or unparseable prompt — don't create messages.
|
|
130
|
-
// But update the session file to clear the old assistantMessageId
|
|
131
|
-
// so the Stop hook creates a fresh message for the next response.
|
|
132
|
-
if (session?.conversationId) {
|
|
133
|
-
fs.writeFileSync(sessionFile, JSON.stringify({
|
|
134
|
-
...session,
|
|
135
|
-
assistantMessageId: null,
|
|
136
|
-
updatedAt: new Date().toISOString(),
|
|
137
|
-
}));
|
|
138
|
-
}
|
|
139
|
-
process.exit(0);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Read Termify config
|
|
144
|
-
let config;
|
|
122
|
+
// Read existing session
|
|
123
|
+
let session = null;
|
|
145
124
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} catch {
|
|
149
|
-
process.exit(0);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
125
|
+
session = JSON.parse(fs.readFileSync(sessionFile, 'utf-8'));
|
|
126
|
+
} catch {}
|
|
152
127
|
|
|
153
|
-
|
|
128
|
+
const provider = detectProvider(hookData);
|
|
129
|
+
const rawPrompt = hookData?.prompt;
|
|
130
|
+
if (!rawPrompt) {
|
|
154
131
|
process.exit(0);
|
|
155
132
|
return;
|
|
156
133
|
}
|
|
157
134
|
|
|
158
|
-
const
|
|
159
|
-
const provider = detectProvider(hookData);
|
|
135
|
+
const prompt = extractUserMessage(rawPrompt);
|
|
160
136
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Sync a single endpoint. Creates conversation if needed,
|
|
139
|
+
* adds user message, creates streaming assistant placeholder.
|
|
140
|
+
*/
|
|
141
|
+
async function syncEndpoint(ep, existingConvoId) {
|
|
142
|
+
const headers = {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'Authorization': `Bearer ${ep.token}`,
|
|
145
|
+
};
|
|
165
146
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
147
|
+
if (!prompt) {
|
|
148
|
+
// Compacted context — just create new assistant message for streaming
|
|
149
|
+
if (existingConvoId) {
|
|
150
|
+
const prevMsgId = session?.endpoints?.[ep.name]?.assistantMessageId;
|
|
151
|
+
if (prevMsgId) {
|
|
152
|
+
try {
|
|
153
|
+
await fetch(`${ep.apiUrl}/api/chat/conversations/${existingConvoId}/messages/${prevMsgId}/complete`, {
|
|
154
|
+
method: 'POST', headers,
|
|
155
|
+
body: JSON.stringify({ content: '...' }),
|
|
156
|
+
signal: AbortSignal.timeout(3000),
|
|
157
|
+
});
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
170
160
|
|
|
171
|
-
|
|
172
|
-
|
|
161
|
+
let assistantMessageId = null;
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(`${ep.apiUrl}/api/chat/conversations/${existingConvoId}/messages`, {
|
|
164
|
+
method: 'POST', headers,
|
|
165
|
+
body: JSON.stringify({ role: 'assistant', content: '', status: 'streaming' }),
|
|
166
|
+
signal: AbortSignal.timeout(5000),
|
|
167
|
+
});
|
|
168
|
+
if (res.ok) {
|
|
169
|
+
const json = await res.json();
|
|
170
|
+
assistantMessageId = json.data?.id || null;
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await fetch(`${ep.apiUrl}/api/chat/conversations/${existingConvoId}`, {
|
|
176
|
+
method: 'PATCH', headers,
|
|
177
|
+
body: JSON.stringify({ status: 'working' }),
|
|
178
|
+
signal: AbortSignal.timeout(3000),
|
|
179
|
+
});
|
|
180
|
+
} catch {}
|
|
181
|
+
|
|
182
|
+
if (assistantMessageId) {
|
|
183
|
+
try {
|
|
184
|
+
await fetch(`${ep.apiUrl}/api/chat/conversations/${existingConvoId}/messages/${assistantMessageId}/events`, {
|
|
185
|
+
method: 'POST', headers,
|
|
186
|
+
body: JSON.stringify({ events: [{ type: 'status', status: 'thinking' }] }),
|
|
187
|
+
signal: AbortSignal.timeout(3000),
|
|
188
|
+
});
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { conversationId: existingConvoId, assistantMessageId };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Normal flow: create conversation if needed, add user message
|
|
198
|
+
let conversationId = existingConvoId;
|
|
173
199
|
|
|
174
200
|
if (!conversationId) {
|
|
175
201
|
const firstLine = prompt.split('\n')[0].trim();
|
|
176
202
|
const title = firstLine.length > 80 ? firstLine.substring(0, 77) + '...' : firstLine;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
conversationId = json.data?.id;
|
|
203
|
+
try {
|
|
204
|
+
const res = await fetch(`${ep.apiUrl}/api/chat/conversations`, {
|
|
205
|
+
method: 'POST', headers,
|
|
206
|
+
body: JSON.stringify({ title, provider }),
|
|
207
|
+
signal: AbortSignal.timeout(5000),
|
|
208
|
+
});
|
|
209
|
+
if (res.ok) {
|
|
210
|
+
const json = await res.json();
|
|
211
|
+
conversationId = json.data?.id;
|
|
212
|
+
}
|
|
213
|
+
} catch {}
|
|
189
214
|
}
|
|
190
215
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
216
|
+
if (!conversationId) return null;
|
|
217
|
+
|
|
218
|
+
// Add user message
|
|
219
|
+
try {
|
|
220
|
+
await fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}/messages`, {
|
|
221
|
+
method: 'POST', headers,
|
|
222
|
+
body: JSON.stringify({ role: 'user', content: prompt }),
|
|
223
|
+
signal: AbortSignal.timeout(5000),
|
|
224
|
+
});
|
|
225
|
+
} catch {}
|
|
198
226
|
|
|
199
|
-
// Create streaming assistant
|
|
227
|
+
// Create streaming assistant placeholder
|
|
200
228
|
let assistantMessageId = null;
|
|
201
229
|
try {
|
|
202
|
-
const
|
|
203
|
-
method: 'POST',
|
|
204
|
-
headers,
|
|
230
|
+
const res = await fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}/messages`, {
|
|
231
|
+
method: 'POST', headers,
|
|
205
232
|
body: JSON.stringify({ role: 'assistant', content: '', status: 'streaming' }),
|
|
206
233
|
signal: AbortSignal.timeout(5000),
|
|
207
234
|
});
|
|
208
|
-
if (
|
|
209
|
-
const
|
|
210
|
-
assistantMessageId =
|
|
235
|
+
if (res.ok) {
|
|
236
|
+
const json = await res.json();
|
|
237
|
+
assistantMessageId = json.data?.id || null;
|
|
211
238
|
}
|
|
212
239
|
} catch {}
|
|
213
240
|
|
|
214
|
-
// Set
|
|
241
|
+
// Set working status
|
|
215
242
|
try {
|
|
216
|
-
await fetch(`${apiUrl}/api/chat/conversations/${conversationId}`, {
|
|
217
|
-
method: 'PATCH',
|
|
218
|
-
headers,
|
|
243
|
+
await fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}`, {
|
|
244
|
+
method: 'PATCH', headers,
|
|
219
245
|
body: JSON.stringify({ status: 'working' }),
|
|
220
246
|
signal: AbortSignal.timeout(3000),
|
|
221
247
|
});
|
|
222
248
|
} catch {}
|
|
223
249
|
|
|
224
|
-
//
|
|
250
|
+
// Initial thinking event
|
|
225
251
|
if (assistantMessageId) {
|
|
226
252
|
try {
|
|
227
|
-
await fetch(`${apiUrl}/api/chat/conversations/${conversationId}/messages/${assistantMessageId}/events`, {
|
|
228
|
-
method: 'POST',
|
|
229
|
-
headers,
|
|
253
|
+
await fetch(`${ep.apiUrl}/api/chat/conversations/${conversationId}/messages/${assistantMessageId}/events`, {
|
|
254
|
+
method: 'POST', headers,
|
|
230
255
|
body: JSON.stringify({ events: [{ type: 'status', status: 'thinking' }] }),
|
|
231
256
|
signal: AbortSignal.timeout(3000),
|
|
232
257
|
});
|
|
233
258
|
} catch {}
|
|
234
259
|
}
|
|
235
260
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
261
|
+
return { conversationId, assistantMessageId };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Sync all endpoints in parallel
|
|
265
|
+
const results = await Promise.allSettled(
|
|
266
|
+
endpoints.map(ep => {
|
|
267
|
+
const existingId = session?.endpoints?.[ep.name]?.conversationId || null;
|
|
268
|
+
return syncEndpoint(ep, existingId).then(r => ({ name: ep.name, ...r }));
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Build session with per-endpoint state
|
|
273
|
+
const endpointState = {};
|
|
274
|
+
for (const r of results) {
|
|
275
|
+
if (r.status === 'fulfilled' && r.value) {
|
|
276
|
+
const { name, conversationId, assistantMessageId } = r.value;
|
|
277
|
+
endpointState[name] = { conversationId, assistantMessageId };
|
|
278
|
+
} else if (r.status === 'fulfilled' && !r.value) {
|
|
279
|
+
// Keep previous state for this endpoint if sync returned null
|
|
280
|
+
const epName = endpoints[results.indexOf(r)]?.name;
|
|
281
|
+
if (epName && session?.endpoints?.[epName]) {
|
|
282
|
+
endpointState[epName] = session.endpoints[epName];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fs.writeFileSync(sessionFile, JSON.stringify({
|
|
288
|
+
endpoints: endpointState,
|
|
289
|
+
transcriptPath,
|
|
290
|
+
provider,
|
|
291
|
+
thinkingsSent: 0,
|
|
292
|
+
updatedAt: new Date().toISOString(),
|
|
293
|
+
}));
|
|
245
294
|
|
|
246
295
|
process.exit(0);
|
|
247
296
|
});
|