termify-agent 1.0.39 → 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/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +55 -0
- package/dist/auth.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +26 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/pty-manager.d.ts +19 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +111 -0
- package/dist/pty-manager.js.map +1 -1
- package/dist/setup.d.ts +15 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +603 -0
- package/dist/setup.js.map +1 -0
- package/hooks/termify-response.js +151 -124
- package/hooks/termify-sync.js +165 -116
- package/mcp/memsearch-mcp-server.mjs +149 -0
- package/package.json +3 -2
- package/plugins/context7/.claude-plugin/plugin.json +7 -0
- package/plugins/context7/.mcp.json +6 -0
- package/plugins/memsearch/.claude-plugin/plugin.json +5 -0
- package/plugins/memsearch/README.md +762 -0
- package/plugins/memsearch/hooks/common.sh +151 -0
- package/plugins/memsearch/hooks/hooks.json +50 -0
- package/plugins/memsearch/hooks/parse-transcript.sh +117 -0
- package/plugins/memsearch/hooks/session-end.sh +9 -0
- package/plugins/memsearch/hooks/session-start.sh +119 -0
- package/plugins/memsearch/hooks/stop.sh +117 -0
- package/plugins/memsearch/hooks/user-prompt-submit.sh +21 -0
- package/plugins/memsearch/scripts/derive-collection.sh +50 -0
- package/plugins/memsearch/skills/memory-recall/SKILL.md +42 -0
- package/scripts/postinstall.js +21 -483
|
@@ -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
|
});
|