ticlawk 0.1.12-dev.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/LICENSE +15 -0
- package/README.md +426 -0
- package/agent-freeway.mjs +2 -0
- package/assets/ticlawk-concept.svg +137 -0
- package/bin/agent-freeway.mjs +4 -0
- package/bin/ticlawk.mjs +594 -0
- package/cc-watcher.mjs +3 -0
- package/package.json +72 -0
- package/scripts/postinstall.mjs +61 -0
- package/src/adapters/telegram/index.mjs +359 -0
- package/src/adapters/ticlawk/api.mjs +360 -0
- package/src/adapters/ticlawk/cards.mjs +149 -0
- package/src/adapters/ticlawk/credentials.mjs +25 -0
- package/src/adapters/ticlawk/index.mjs +1229 -0
- package/src/adapters/ticlawk/wake-client.mjs +204 -0
- package/src/core/adapter-registry.mjs +50 -0
- package/src/core/argv.mjs +38 -0
- package/src/core/bindings/store.mjs +81 -0
- package/src/core/bus.mjs +91 -0
- package/src/core/config.mjs +203 -0
- package/src/core/daemon-install.mjs +246 -0
- package/src/core/diagnostics.mjs +79 -0
- package/src/core/events/worker-events.mjs +80 -0
- package/src/core/executables.mjs +106 -0
- package/src/core/host-id.mjs +48 -0
- package/src/core/http.mjs +65 -0
- package/src/core/logger.mjs +34 -0
- package/src/core/media/inbound.mjs +127 -0
- package/src/core/media/outbound.mjs +163 -0
- package/src/core/profiles.mjs +173 -0
- package/src/core/runtime-contract.mjs +68 -0
- package/src/core/runtime-env.mjs +9 -0
- package/src/core/runtime-registry.mjs +93 -0
- package/src/core/runtime-support.mjs +197 -0
- package/src/core/setup-readiness.mjs +86 -0
- package/src/core/store/json-file-store.mjs +47 -0
- package/src/core/ticlawk-control.mjs +92 -0
- package/src/core/uninstall.mjs +142 -0
- package/src/core/update-state.mjs +62 -0
- package/src/core/update.mjs +178 -0
- package/src/runtimes/claude-code/index.mjs +363 -0
- package/src/runtimes/claude-code/session.mjs +388 -0
- package/src/runtimes/claude-code/transcripts.mjs +206 -0
- package/src/runtimes/codex/index.mjs +306 -0
- package/src/runtimes/codex/session.mjs +750 -0
- package/src/runtimes/openclaw/gateway.mjs +269 -0
- package/src/runtimes/openclaw/identity.mjs +34 -0
- package/src/runtimes/openclaw/index.mjs +228 -0
- package/src/runtimes/openclaw/inflight.mjs +46 -0
- package/src/runtimes/openclaw/target.mjs +57 -0
- package/src/runtimes/opencode/index.mjs +318 -0
- package/src/runtimes/opencode/session.mjs +413 -0
- package/src/runtimes/pi/index.mjs +287 -0
- package/src/runtimes/pi/session.mjs +423 -0
- package/ticlawk.mjs +260 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ticlawk adapter — HTTP API client.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around `ticlawk.com/api/*` (override with `TICLAWK_API_URL`).
|
|
5
|
+
* Only depends on a `tk_` connector bearer API key from
|
|
6
|
+
* `process.env.TICLAWK_CONNECTOR_API_KEY`.
|
|
7
|
+
* No direct Supabase access.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { loadPersistentConfig, TICLAWK_CONNECTOR_WS_URL } from '../../core/config.mjs';
|
|
11
|
+
import { debugError, debugLog } from '../../core/logger.mjs';
|
|
12
|
+
import { readPkgVersion } from '../../core/update.mjs';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONNECTOR_WS_URL = 'wss://edge.ticlawk.com/functions/v1/connector-wake';
|
|
15
|
+
|
|
16
|
+
export function getApiUrl() {
|
|
17
|
+
const config = loadPersistentConfig();
|
|
18
|
+
return process.env.TICLAWK_API_URL || config.TICLAWK_API_URL || 'https://ticlawk.com';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getApiKey() {
|
|
22
|
+
const config = loadPersistentConfig();
|
|
23
|
+
return process.env.TICLAWK_CONNECTOR_API_KEY || config.TICLAWK_CONNECTOR_API_KEY || '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getConnectorWsUrl() {
|
|
27
|
+
const config = loadPersistentConfig();
|
|
28
|
+
const configured = process.env[TICLAWK_CONNECTOR_WS_URL] || config[TICLAWK_CONNECTOR_WS_URL];
|
|
29
|
+
if (configured) return configured;
|
|
30
|
+
return DEFAULT_CONNECTOR_WS_URL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Agent event writes must preserve per-agent order while still allowing
|
|
34
|
+
// different agents to proceed concurrently.
|
|
35
|
+
const agentEventQueues = new Map(); // agentId -> Promise
|
|
36
|
+
|
|
37
|
+
export class AgentFreewayUpdateRequiredError extends Error {
|
|
38
|
+
constructor(payload = {}) {
|
|
39
|
+
const currentVersion = payload.current_agent_freeway_version || payload.currentAgentFreewayVersion || getAgentFreewayVersion();
|
|
40
|
+
const requiredVersion = payload.required_agent_freeway_version || payload.requiredAgentFreewayVersion || '';
|
|
41
|
+
super(`Ticlawk requires ticlawk >= ${requiredVersion || 'unknown'}, current version is ${currentVersion || 'unknown'}. Please update ticlawk.`);
|
|
42
|
+
this.name = 'AgentFreewayUpdateRequiredError';
|
|
43
|
+
this.code = 'agent_freeway_update_required';
|
|
44
|
+
this.updateRequired = true;
|
|
45
|
+
this.status = 409;
|
|
46
|
+
this.statusCode = 409;
|
|
47
|
+
this.currentAgentFreewayVersion = currentVersion || null;
|
|
48
|
+
this.requiredAgentFreewayVersion = requiredVersion || null;
|
|
49
|
+
this.payload = payload;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isUpdateRequiredError(err) {
|
|
54
|
+
return Boolean(err?.updateRequired || err?.code === 'agent_freeway_update_required');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getAgentFreewayVersion() {
|
|
58
|
+
return readPkgVersion() || '0.0.0';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withAgentFreewayVersion(body = {}) {
|
|
62
|
+
return {
|
|
63
|
+
...body,
|
|
64
|
+
agent_freeway_version: getAgentFreewayVersion(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isUpdateRequiredPayload(payload) {
|
|
69
|
+
return Boolean(
|
|
70
|
+
payload?.error === 'agent_freeway_update_required'
|
|
71
|
+
|| payload?.code === 'agent_freeway_update_required'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shortId(value) {
|
|
76
|
+
return value ? String(value).slice(0, 8) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function apiFetch(path, opts = {}) {
|
|
80
|
+
const url = `${getApiUrl()}${path}`;
|
|
81
|
+
const res = await fetch(url, {
|
|
82
|
+
...opts,
|
|
83
|
+
headers: {
|
|
84
|
+
'Authorization': `Bearer ${getApiKey()}`,
|
|
85
|
+
...(opts.body && !(opts.body instanceof FormData) ? {'Content-Type': 'application/json'} : {}),
|
|
86
|
+
...opts.headers,
|
|
87
|
+
},
|
|
88
|
+
signal: AbortSignal.timeout(opts.timeout || 15000),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const text = await res.text().catch(() => '');
|
|
92
|
+
let parsed = {};
|
|
93
|
+
if (text) {
|
|
94
|
+
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isUpdateRequiredPayload(parsed)) {
|
|
98
|
+
throw new AgentFreewayUpdateRequiredError(parsed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const msg = parsed?.error || parsed?.message || text;
|
|
103
|
+
const err = new Error(`API ${res.status}: ${msg || res.statusText}`);
|
|
104
|
+
err.status = res.status;
|
|
105
|
+
err.statusCode = res.status;
|
|
106
|
+
if (parsed?.error) err.code = parsed.error;
|
|
107
|
+
if (parsed?.code) err.code = parsed.code;
|
|
108
|
+
err.payload = parsed || {};
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return parsed || {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Agents ──
|
|
116
|
+
|
|
117
|
+
export async function getAgents({ hostId } = {}) {
|
|
118
|
+
const query = hostId ? `?${new URLSearchParams({ runtime_host_id: hostId })}` : '';
|
|
119
|
+
const { data } = await apiFetch(`/api/agents${query}`);
|
|
120
|
+
if (!Array.isArray(data)) {
|
|
121
|
+
const err = new Error('API /api/agents response missing data array');
|
|
122
|
+
err.code = 'invalid_agents_response';
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function getChannels(hostId = null) {
|
|
129
|
+
return getAgents({ hostId });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function getAgent(id) {
|
|
133
|
+
const { data } = await apiFetch(`/api/agents/${id}`);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function getChannel(id) {
|
|
138
|
+
return getAgent(id);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function updateAgent(id, updates) {
|
|
142
|
+
return apiFetch(`/api/agents/${id}`, {
|
|
143
|
+
method: 'PATCH',
|
|
144
|
+
body: JSON.stringify(updates),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function updateChannel(id, updates) {
|
|
149
|
+
return updateAgent(id, updates);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function postRuntimeResult(body) {
|
|
153
|
+
const result = await apiFetch('/api/runtime-results', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: JSON.stringify(body),
|
|
156
|
+
timeout: 30000,
|
|
157
|
+
});
|
|
158
|
+
if (result?.matched === false) {
|
|
159
|
+
throw new Error('runtime result was not matched');
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Messages ──
|
|
165
|
+
|
|
166
|
+
export async function getPendingMessages() {
|
|
167
|
+
const { data } = await apiFetch('/api/messages/pending');
|
|
168
|
+
return data || [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function claimPendingMessages(hostId, limit = 5, excludedAgentIds = []) {
|
|
172
|
+
const { data } = await apiFetch('/api/messages/claim-pending', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify(withAgentFreewayVersion({
|
|
175
|
+
runtime_host_id: hostId,
|
|
176
|
+
limit,
|
|
177
|
+
excluded_agent_ids: excludedAgentIds,
|
|
178
|
+
})),
|
|
179
|
+
});
|
|
180
|
+
return data || [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function syncMessages(rows) {
|
|
184
|
+
return apiFetch('/api/messages/sync', {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
body: JSON.stringify({ rows }),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function claimMessage(id, hostId) {
|
|
191
|
+
return apiFetch(`/api/messages/${id}/claim`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
body: JSON.stringify(withAgentFreewayVersion({ runtime_host_id: hostId })),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function releaseMessage(id, hostId) {
|
|
198
|
+
return apiFetch(`/api/messages/${id}/release`, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
body: JSON.stringify({ runtime_host_id: hostId }),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function completeMessage(id, hostId) {
|
|
205
|
+
return apiFetch(`/api/messages/${id}/complete`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
body: JSON.stringify({ runtime_host_id: hostId }),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function recoverClaimedMessages(hostId) {
|
|
212
|
+
return apiFetch('/api/messages/recover', {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: JSON.stringify(withAgentFreewayVersion({ runtime_host_id: hostId })),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Upload ──
|
|
219
|
+
|
|
220
|
+
export async function uploadAsset(fileName, fileData, contentType, kind = 'chat_media') {
|
|
221
|
+
const formData = new FormData();
|
|
222
|
+
formData.append('file', new Blob([fileData], { type: contentType }), fileName);
|
|
223
|
+
formData.append('kind', kind);
|
|
224
|
+
if (contentType) formData.append('content_type', contentType);
|
|
225
|
+
|
|
226
|
+
const { data } = await apiFetch('/api/assets/upload', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
body: formData,
|
|
229
|
+
timeout: 30000,
|
|
230
|
+
});
|
|
231
|
+
return data;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Channel event pipe ──
|
|
235
|
+
|
|
236
|
+
export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
|
|
237
|
+
const agentId = agent_id || null;
|
|
238
|
+
const queueKey = agentId || `${agent}:${session_id || ''}`;
|
|
239
|
+
const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
|
|
240
|
+
const eventName = event?.worker_event_name || event?.hook_event_name || event?.event_name || 'unknown';
|
|
241
|
+
const turnId = event?.turn_id || event?.reply_to_message_id || null;
|
|
242
|
+
const seq = event?.event_seq ?? null;
|
|
243
|
+
const deltaChars = typeof event?.delta === 'string' ? event.delta.length : null;
|
|
244
|
+
|
|
245
|
+
const queued = previous
|
|
246
|
+
.catch(() => null)
|
|
247
|
+
.then(async () => {
|
|
248
|
+
const startedAt = Date.now();
|
|
249
|
+
try {
|
|
250
|
+
const result = await apiFetch('/api/events', {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
|
|
253
|
+
timeout: 5000,
|
|
254
|
+
});
|
|
255
|
+
if (required && result?.matched === false) {
|
|
256
|
+
throw new Error(`event was not matched (${eventName})`);
|
|
257
|
+
}
|
|
258
|
+
debugLog('events', 'post.ok', {
|
|
259
|
+
agent,
|
|
260
|
+
agentId,
|
|
261
|
+
sessionId: shortId(session_id),
|
|
262
|
+
runtimeVersion: runtime_version ?? null,
|
|
263
|
+
turnId: shortId(turnId),
|
|
264
|
+
eventName,
|
|
265
|
+
seq,
|
|
266
|
+
deltaChars,
|
|
267
|
+
durationMs: Date.now() - startedAt,
|
|
268
|
+
});
|
|
269
|
+
return result;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
debugError('events', 'post.failed', {
|
|
272
|
+
agent,
|
|
273
|
+
agentId,
|
|
274
|
+
sessionId: shortId(session_id),
|
|
275
|
+
runtimeVersion: runtime_version ?? null,
|
|
276
|
+
turnId: shortId(turnId),
|
|
277
|
+
eventName,
|
|
278
|
+
seq,
|
|
279
|
+
deltaChars,
|
|
280
|
+
durationMs: Date.now() - startedAt,
|
|
281
|
+
error: err?.message || 'unknown error',
|
|
282
|
+
});
|
|
283
|
+
if (required) {
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
// Best-effort by default. Callers that need a hard failure set
|
|
287
|
+
// `required: true` (used for the final reply path).
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
agentEventQueues.set(queueKey, queued);
|
|
293
|
+
queued.finally(() => {
|
|
294
|
+
if (agentEventQueues.get(queueKey) === queued) {
|
|
295
|
+
agentEventQueues.delete(queueKey);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return queued;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Pair (no auth needed) ──
|
|
303
|
+
|
|
304
|
+
export async function pair(payload) {
|
|
305
|
+
const url = `${getApiUrl()}/dispatch`;
|
|
306
|
+
const res = await fetch(url, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify({ action: 'pair', ...payload }),
|
|
310
|
+
signal: AbortSignal.timeout(15000),
|
|
311
|
+
});
|
|
312
|
+
const body = await res.json().catch(() => ({}));
|
|
313
|
+
return {
|
|
314
|
+
statusCode: res.status,
|
|
315
|
+
...body,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function pairPreview(payload) {
|
|
320
|
+
const url = `${getApiUrl()}/dispatch`;
|
|
321
|
+
const res = await fetch(url, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: { 'Content-Type': 'application/json' },
|
|
324
|
+
body: JSON.stringify({ action: 'pair-preview', ...payload }),
|
|
325
|
+
signal: AbortSignal.timeout(15000),
|
|
326
|
+
});
|
|
327
|
+
return res.json();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function pairingRequest(payload, timeout = 15000) {
|
|
331
|
+
const url = `${getApiUrl()}/api/agent-pairings`;
|
|
332
|
+
const res = await fetch(url, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: { 'Content-Type': 'application/json' },
|
|
335
|
+
body: JSON.stringify(payload),
|
|
336
|
+
signal: AbortSignal.timeout(timeout),
|
|
337
|
+
});
|
|
338
|
+
const body = await res.json().catch(() => ({}));
|
|
339
|
+
return {
|
|
340
|
+
statusCode: res.status,
|
|
341
|
+
...body,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function createPairingSession(payload) {
|
|
346
|
+
return pairingRequest({ action: 'create', ...payload });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function pollPairingSession(payload) {
|
|
350
|
+
return pairingRequest({ action: 'poll', ...payload }, 30000);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function cancelPairingSession(payload) {
|
|
354
|
+
return pairingRequest({ action: 'cancel', ...payload }).catch(() => null);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function getMe() {
|
|
358
|
+
const { data } = await apiFetch('/api/me');
|
|
359
|
+
return data || null;
|
|
360
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ticlawk adapter — final agent message / media write path.
|
|
3
|
+
*
|
|
4
|
+
* Everything here turns an agent result (text + optional media local paths)
|
|
5
|
+
* into a terminal runtime result written back to ticlawk.
|
|
6
|
+
*
|
|
7
|
+
* This module imports from `./api.mjs` (HTTP client) and from
|
|
8
|
+
* `../../core/logger.mjs` (structured logging). No runtime dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { extname } from 'node:path';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import * as api from './api.mjs';
|
|
15
|
+
import { extractMediaPaths } from '../../core/media/outbound.mjs';
|
|
16
|
+
import { debugLog, debugError } from '../../core/logger.mjs';
|
|
17
|
+
import { TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
18
|
+
|
|
19
|
+
const MIME_MAP = {
|
|
20
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
21
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
22
|
+
'.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
23
|
+
'.opus': 'audio/opus', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
24
|
+
'.pdf': 'application/pdf',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function uploadLocalAsset(localPath) {
|
|
28
|
+
if (!process.env[TICLAWK_CONNECTOR_API_KEY]) {
|
|
29
|
+
debugError('relay', 'upload.skipped', {
|
|
30
|
+
localPath,
|
|
31
|
+
reason: 'missing connector api key',
|
|
32
|
+
});
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!existsSync(localPath)) {
|
|
36
|
+
debugError('relay', 'upload.skipped', {
|
|
37
|
+
localPath,
|
|
38
|
+
reason: 'file not found',
|
|
39
|
+
});
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ext = extname(localPath).toLowerCase();
|
|
44
|
+
const contentType = MIME_MAP[ext] || 'application/octet-stream';
|
|
45
|
+
const fileName = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
|
|
46
|
+
const fileData = readFileSync(localPath);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const asset = await api.uploadAsset(fileName, fileData, contentType);
|
|
50
|
+
if (!asset?.asset_id) {
|
|
51
|
+
debugError('relay', 'upload.failed', {
|
|
52
|
+
localPath,
|
|
53
|
+
fileName,
|
|
54
|
+
error: 'missing asset_id in upload response',
|
|
55
|
+
});
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
debugLog('relay', 'upload.ok', {
|
|
59
|
+
localPath,
|
|
60
|
+
fileName,
|
|
61
|
+
assetId: asset.asset_id,
|
|
62
|
+
contentType: asset.content_type || contentType,
|
|
63
|
+
sizeBytes: asset.size_bytes ?? null,
|
|
64
|
+
});
|
|
65
|
+
return asset;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
debugError('relay', 'upload.failed', {
|
|
68
|
+
localPath,
|
|
69
|
+
fileName,
|
|
70
|
+
error: err.message,
|
|
71
|
+
});
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function uploadMediaAssets(localPaths) {
|
|
77
|
+
const assets = [];
|
|
78
|
+
for (const p of localPaths) {
|
|
79
|
+
const asset = await uploadLocalAsset(p);
|
|
80
|
+
if (asset) assets.push(asset);
|
|
81
|
+
}
|
|
82
|
+
return assets;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function processAndSaveResult(result, opts) {
|
|
86
|
+
const { agentId: explicitAgentId, sessionKey, hostId, type, replyToMessageId, agent, sessionId, turnId, runtimeVersion } = opts;
|
|
87
|
+
const agentId = explicitAgentId || sessionKey;
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
|
|
90
|
+
// Collect media: from agent mediaUrls + parsed from text
|
|
91
|
+
const allLocalPaths = [...new Set([
|
|
92
|
+
...(result.mediaUrls || []),
|
|
93
|
+
...extractMediaPaths(result.text || ''),
|
|
94
|
+
])];
|
|
95
|
+
|
|
96
|
+
debugLog('relay', 'process-result.begin', {
|
|
97
|
+
agentId,
|
|
98
|
+
type,
|
|
99
|
+
parentMessageId: replyToMessageId || null,
|
|
100
|
+
textLength: result.text?.length || 0,
|
|
101
|
+
localMediaCount: allLocalPaths.length,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Upload local media to Ticlawk private chat assets.
|
|
105
|
+
const uploadedAssets = await uploadMediaAssets(allLocalPaths);
|
|
106
|
+
const uploadedAssetIds = uploadedAssets
|
|
107
|
+
.map((asset) => asset?.asset_id)
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
|
|
110
|
+
const updateId = randomUUID();
|
|
111
|
+
debugLog('relay', 'post-final.begin', {
|
|
112
|
+
agentId,
|
|
113
|
+
updateId,
|
|
114
|
+
durationMs: Date.now() - startedAt,
|
|
115
|
+
uploadedMediaCount: uploadedAssetIds.length,
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
await api.postRuntimeResult({
|
|
119
|
+
agent,
|
|
120
|
+
agent_id: agentId,
|
|
121
|
+
runtime_host_id: hostId,
|
|
122
|
+
session_id: sessionId || null,
|
|
123
|
+
cwd: '',
|
|
124
|
+
runtime_version: runtimeVersion ?? null,
|
|
125
|
+
result_id: updateId,
|
|
126
|
+
turn_id: turnId || replyToMessageId || null,
|
|
127
|
+
reply_to_message_id: replyToMessageId || null,
|
|
128
|
+
origin_ts: new Date().toISOString(),
|
|
129
|
+
text: result.text || '',
|
|
130
|
+
media_asset_ids: uploadedAssetIds,
|
|
131
|
+
output_type: type || 'agent_message',
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
debugError('relay', 'post-final.failed', {
|
|
135
|
+
agentId,
|
|
136
|
+
updateId,
|
|
137
|
+
durationMs: Date.now() - startedAt,
|
|
138
|
+
error: err.message,
|
|
139
|
+
});
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
debugLog('relay', 'process-result.ok', {
|
|
143
|
+
agentId,
|
|
144
|
+
updateId,
|
|
145
|
+
durationMs: Date.now() - startedAt,
|
|
146
|
+
uploadedMediaCount: uploadedAssetIds.length,
|
|
147
|
+
});
|
|
148
|
+
return { id: updateId, agentId };
|
|
149
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ticlawk adapter — API key persistence.
|
|
3
|
+
*
|
|
4
|
+
* The ticlawk connect flow returns a `tk_...` bearer key in response to
|
|
5
|
+
* a local adapter connect call. We persist it to `~/.ticlawk/.config`
|
|
6
|
+
* using the connector-specific env name so publisher scripts can keep using
|
|
7
|
+
* `TICLAWK_API_KEY` without being polluted by ticlawk.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AF_ADAPTER_KEY, AF_CONFIG_PATH, LEGACY_TICLAWK_API_KEY, persistConfig, TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
11
|
+
|
|
12
|
+
export function persistApiCredential(apiKey) {
|
|
13
|
+
if (!apiKey || !apiKey.startsWith('tk_')) return;
|
|
14
|
+
|
|
15
|
+
persistConfig({
|
|
16
|
+
[AF_ADAPTER_KEY]: 'ticlawk',
|
|
17
|
+
[TICLAWK_CONNECTOR_API_KEY]: apiKey,
|
|
18
|
+
[LEGACY_TICLAWK_API_KEY]: '',
|
|
19
|
+
TICLAWK_SETUP_CODE: '',
|
|
20
|
+
});
|
|
21
|
+
process.env[TICLAWK_CONNECTOR_API_KEY] = apiKey;
|
|
22
|
+
delete process.env[LEGACY_TICLAWK_API_KEY];
|
|
23
|
+
delete process.env.TICLAWK_SETUP_CODE;
|
|
24
|
+
console.log(`[connect] saved ${TICLAWK_CONNECTOR_API_KEY} to ${AF_CONFIG_PATH}`);
|
|
25
|
+
}
|