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.
Files changed (55) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +426 -0
  3. package/agent-freeway.mjs +2 -0
  4. package/assets/ticlawk-concept.svg +137 -0
  5. package/bin/agent-freeway.mjs +4 -0
  6. package/bin/ticlawk.mjs +594 -0
  7. package/cc-watcher.mjs +3 -0
  8. package/package.json +72 -0
  9. package/scripts/postinstall.mjs +61 -0
  10. package/src/adapters/telegram/index.mjs +359 -0
  11. package/src/adapters/ticlawk/api.mjs +360 -0
  12. package/src/adapters/ticlawk/cards.mjs +149 -0
  13. package/src/adapters/ticlawk/credentials.mjs +25 -0
  14. package/src/adapters/ticlawk/index.mjs +1229 -0
  15. package/src/adapters/ticlawk/wake-client.mjs +204 -0
  16. package/src/core/adapter-registry.mjs +50 -0
  17. package/src/core/argv.mjs +38 -0
  18. package/src/core/bindings/store.mjs +81 -0
  19. package/src/core/bus.mjs +91 -0
  20. package/src/core/config.mjs +203 -0
  21. package/src/core/daemon-install.mjs +246 -0
  22. package/src/core/diagnostics.mjs +79 -0
  23. package/src/core/events/worker-events.mjs +80 -0
  24. package/src/core/executables.mjs +106 -0
  25. package/src/core/host-id.mjs +48 -0
  26. package/src/core/http.mjs +65 -0
  27. package/src/core/logger.mjs +34 -0
  28. package/src/core/media/inbound.mjs +127 -0
  29. package/src/core/media/outbound.mjs +163 -0
  30. package/src/core/profiles.mjs +173 -0
  31. package/src/core/runtime-contract.mjs +68 -0
  32. package/src/core/runtime-env.mjs +9 -0
  33. package/src/core/runtime-registry.mjs +93 -0
  34. package/src/core/runtime-support.mjs +197 -0
  35. package/src/core/setup-readiness.mjs +86 -0
  36. package/src/core/store/json-file-store.mjs +47 -0
  37. package/src/core/ticlawk-control.mjs +92 -0
  38. package/src/core/uninstall.mjs +142 -0
  39. package/src/core/update-state.mjs +62 -0
  40. package/src/core/update.mjs +178 -0
  41. package/src/runtimes/claude-code/index.mjs +363 -0
  42. package/src/runtimes/claude-code/session.mjs +388 -0
  43. package/src/runtimes/claude-code/transcripts.mjs +206 -0
  44. package/src/runtimes/codex/index.mjs +306 -0
  45. package/src/runtimes/codex/session.mjs +750 -0
  46. package/src/runtimes/openclaw/gateway.mjs +269 -0
  47. package/src/runtimes/openclaw/identity.mjs +34 -0
  48. package/src/runtimes/openclaw/index.mjs +228 -0
  49. package/src/runtimes/openclaw/inflight.mjs +46 -0
  50. package/src/runtimes/openclaw/target.mjs +57 -0
  51. package/src/runtimes/opencode/index.mjs +318 -0
  52. package/src/runtimes/opencode/session.mjs +413 -0
  53. package/src/runtimes/pi/index.mjs +287 -0
  54. package/src/runtimes/pi/session.mjs +423 -0
  55. package/ticlawk.mjs +260 -0
@@ -0,0 +1,1229 @@
1
+ import { parseOptionArgs } from '../../core/argv.mjs';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
4
+ import { basename } from 'node:path';
5
+ import { AF_ADAPTER_KEY, LEGACY_TICLAWK_API_KEY, loadPersistentConfig, persistConfig, TICLAWK_CONNECTOR_API_KEY, TICLAWK_CONNECTOR_WS_URL } from '../../core/config.mjs';
6
+ import { belongsToRuntimeHost, getBindingRuntimeHostId, getHostId, getHostLabel } from '../../core/host-id.mjs';
7
+ import { debugError, debugLog } from '../../core/logger.mjs';
8
+ import { getActiveProfile, ensureLegacyProfile, readProfileMeta, saveAndActivateProfile } from '../../core/profiles.mjs';
9
+ import { isTerminalRuntimeFailure } from '../../core/runtime-support.mjs';
10
+ import { clearUpdateRequiredState, readUpdateState, setUpdateRequiredState } from '../../core/update-state.mjs';
11
+ import { isManagedInstall, startDetachedSelfUpdate } from '../../core/update.mjs';
12
+ import { resolveOpenClawWorkspace } from '../../runtimes/openclaw/target.mjs';
13
+ import * as api from './api.mjs';
14
+ import { processAndSaveResult } from './cards.mjs';
15
+ import { persistApiCredential } from './credentials.mjs';
16
+ import { TiclawkWakeClient } from './wake-client.mjs';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const qrcode = require('qrcode-terminal');
20
+ const JOBS_WAKE_DEBOUNCE_MS = 100;
21
+ const BINDINGS_WAKE_DEBOUNCE_MS = 500;
22
+ const RECOVERY_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
23
+ const BINDING_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
24
+ const UPDATE_RETRY_COOLDOWN_MS = 15 * 60 * 1000;
25
+ const UPDATE_REQUIRED_LOG_INTERVAL_MS = 60 * 1000;
26
+
27
+ function connectError(statusCode, error) {
28
+ return { statusCode, body: { ok: false, error } };
29
+ }
30
+
31
+ function normalizeInboundMediaAssets(msg) {
32
+ if (!Array.isArray(msg?.media_assets)) return [];
33
+ return msg.media_assets
34
+ .map((asset) => {
35
+ const url = String(asset?.url || '').trim();
36
+ if (!url) return null;
37
+ return {
38
+ kind: 'remote_url',
39
+ value: url,
40
+ mime: asset.content_type || null,
41
+ assetId: asset.asset_id || null,
42
+ expiresAt: asset.expires_at || null,
43
+ };
44
+ })
45
+ .filter(Boolean);
46
+ }
47
+
48
+ function normalizeInboundMessage(msg) {
49
+ const messageId = msg.id || msg.message_id || null;
50
+ const media = normalizeInboundMediaAssets(msg);
51
+ return {
52
+ bindingId: msg.agent_id || '',
53
+ messageId,
54
+ text: msg.text || '',
55
+ action: msg.action || (media.length > 0 ? 'image' : 'task'),
56
+ media,
57
+ raw: {
58
+ ...msg,
59
+ id: messageId,
60
+ },
61
+ };
62
+ }
63
+
64
+ function getBindingSessionId(binding) {
65
+ return binding?.runtimeMeta?.sessionId || binding?.runtimeMeta?.sessionKey || binding?.runtimeMeta?.agentId || binding?.id || null;
66
+ }
67
+
68
+ function getWorkdir(meta = {}) {
69
+ return String(meta.workdir || meta.projectDir || meta.cwd || '').trim();
70
+ }
71
+
72
+ function getRuntimeVersion(bindingOrMeta) {
73
+ const value = bindingOrMeta?.runtimeMeta?.runtimeVersion ?? bindingOrMeta?.runtimeVersion ?? bindingOrMeta?.agent_runtime_version ?? null;
74
+ return Number.isInteger(value) ? Number(value) : null;
75
+ }
76
+
77
+ function buildSyncableRuntimeMeta(binding) {
78
+ const meta = (binding?.runtimeMeta && typeof binding.runtimeMeta === 'object')
79
+ ? { ...binding.runtimeMeta }
80
+ : {};
81
+ delete meta.runtimeVersion;
82
+ return Object.keys(meta).length > 0 ? meta : null;
83
+ }
84
+
85
+ function getRuntimeHostIdFromPayload(payload) {
86
+ return String(payload?.runtime_host_id || '').trim();
87
+ }
88
+
89
+ function getRuntimeHostLabelFromPayload(payload) {
90
+ return String(payload?.runtime_host_label || '').trim();
91
+ }
92
+
93
+ function getAgentIdFromPayload(payload) {
94
+ return String(payload?.agent_id || payload?.agentId || '').trim();
95
+ }
96
+
97
+ function buildBindingFromClaimedMessage(msg) {
98
+ const agentId = getAgentIdFromPayload(msg);
99
+ const runtime = msg.agent_service_type || msg.service_type;
100
+ const rawMeta = msg.agent_meta || msg.meta;
101
+ const sourceMeta = (rawMeta && typeof rawMeta === 'object') ? { ...rawMeta } : {};
102
+ const fallbackWorkdir = runtime === 'openclaw' && sourceMeta.agentId
103
+ ? resolveOpenClawWorkspace(sourceMeta.agentId)
104
+ : '';
105
+ const workdir = getWorkdir(sourceMeta) || fallbackWorkdir;
106
+ if (workdir) {
107
+ sourceMeta.workdir = workdir;
108
+ sourceMeta.projectDir = sourceMeta.projectDir || workdir;
109
+ sourceMeta.cwd = sourceMeta.cwd || workdir;
110
+ }
111
+ const rawRuntimeVersion = msg.agent_runtime_version ?? msg.runtime_version;
112
+ const runtimeVersion = Number.isInteger(rawRuntimeVersion) ? Number(rawRuntimeVersion) : 0;
113
+
114
+ return {
115
+ id: agentId,
116
+ adapter: 'ticlawk',
117
+ targetKey: agentId,
118
+ targetMeta: {
119
+ agentId,
120
+ runtime_host_id: getRuntimeHostIdFromPayload(msg) || undefined,
121
+ },
122
+ runtime_host_id: getRuntimeHostIdFromPayload(msg) || undefined,
123
+ runtime_host_label: getRuntimeHostLabelFromPayload(msg) || undefined,
124
+ runtime,
125
+ runtimeMeta: {
126
+ ...sourceMeta,
127
+ runtimeVersion,
128
+ },
129
+ displayName: msg.agent_display_name || msg.agent_name || agentId,
130
+ status: msg.agent_status || msg.status || 'connected',
131
+ };
132
+ }
133
+
134
+ function buildBindingFromChannelSnapshot(agent) {
135
+ return buildBindingFromClaimedMessage({
136
+ agent_id: agent.id || agent.agent_id,
137
+ agent_name: agent.name,
138
+ agent_display_name: agent.display_name,
139
+ agent_status: agent.status,
140
+ agent_service_type: agent.service_type,
141
+ agent_meta: agent.meta,
142
+ agent_runtime_version: agent.runtime_version,
143
+ runtime_host_id: agent.runtime_host_id,
144
+ runtime_host_label: agent.runtime_host_label,
145
+ });
146
+ }
147
+
148
+ function maskIdentity(identity = {}) {
149
+ return {
150
+ userId: identity.userId || identity.user_id || identity.id || null,
151
+ emailMasked: identity.emailMasked || identity.email_masked || null,
152
+ phoneMasked: identity.phoneMasked || identity.phone_masked || null,
153
+ };
154
+ }
155
+
156
+ function formatIdentityLines(identity = {}) {
157
+ const normalized = maskIdentity(identity);
158
+ return [
159
+ ` user: ${normalized.userId || 'unknown'}`,
160
+ normalized.emailMasked ? ` email: ${normalized.emailMasked}` : null,
161
+ normalized.phoneMasked ? ` phone: ${normalized.phoneMasked}` : null,
162
+ ].filter(Boolean);
163
+ }
164
+
165
+ function formatAffectedAgents(agents = []) {
166
+ if (!agents.length) return [' - none found for this host'];
167
+ return agents.map((agent) => {
168
+ const display = agent.display_name || agent.name || agent.id;
169
+ const runtime = agent.service_type || 'unknown';
170
+ return ` - ${display} (${runtime}) ${agent.id}`;
171
+ });
172
+ }
173
+
174
+ function sha256Hex(value) {
175
+ return createHash('sha256').update(String(value)).digest('hex');
176
+ }
177
+
178
+ function makeClientSecret() {
179
+ return randomBytes(32).toString('base64url');
180
+ }
181
+
182
+ function getRuntimeWorkdir(runtimeMeta = {}) {
183
+ return String(runtimeMeta.workdir || runtimeMeta.cwd || runtimeMeta.projectDir || '').trim();
184
+ }
185
+
186
+ function runtimeLabel(runtime) {
187
+ if (runtime === 'claude_code') return 'Claude Code';
188
+ if (runtime === 'opencode') return 'OpenCode';
189
+ if (runtime === 'openclaw') return 'OpenClaw';
190
+ if (runtime === 'codex') return 'Codex';
191
+ if (runtime === 'pi') return 'Pi';
192
+ return runtime || 'Agent';
193
+ }
194
+
195
+ async function buildAutoRuntimeOptions(ctx, payload = {}) {
196
+ const workdir = getRuntimeWorkdir(payload) || process.cwd();
197
+ const candidates = ['codex', 'claude_code', 'opencode', 'pi'];
198
+ const options = [];
199
+ for (const serviceType of candidates) {
200
+ try {
201
+ const resolved = await ctx.resolveRuntimeBinding({
202
+ ...payload,
203
+ serviceType,
204
+ workdir,
205
+ });
206
+ const runtimeMeta = resolved.runtimeMeta || {};
207
+ const optionWorkdir = getRuntimeWorkdir(runtimeMeta) || workdir;
208
+ options.push({
209
+ runtime: resolved.runtime,
210
+ runtime_label: runtimeLabel(resolved.runtime),
211
+ display_name: resolved.displayName || basename(optionWorkdir) || runtimeLabel(resolved.runtime),
212
+ workdir: optionWorkdir,
213
+ binding_key: optionWorkdir,
214
+ runtime_meta: runtimeMeta,
215
+ });
216
+ } catch (err) {
217
+ debugLog('ticlawk-pairing', 'auto-runtime.skip', {
218
+ runtime: serviceType,
219
+ reason: err?.message || String(err),
220
+ });
221
+ }
222
+ }
223
+ return options;
224
+ }
225
+
226
+ function printPairingChallenge(session) {
227
+ console.log();
228
+ console.log('Open Ticlawk and scan the QR code below, or enter the pairing code.');
229
+ console.log();
230
+ console.log(`Pairing code: ${session.display_code}`);
231
+ if (session.expires_at) {
232
+ console.log(`Expires: ${session.expires_at}`);
233
+ }
234
+ console.log();
235
+ qrcode.generate(session.qr_payload, { small: true });
236
+ console.log();
237
+ }
238
+
239
+ async function waitForPairingApproval({ pairingId, clientSecret, pollAfterMs = 1500, expiresAt }) {
240
+ const deadline = expiresAt ? Date.parse(expiresAt) + 5000 : Date.now() + 10 * 60 * 1000;
241
+ let lastStatus = null;
242
+ while (Date.now() < deadline) {
243
+ await new Promise(resolve => setTimeout(resolve, pollAfterMs));
244
+ const result = await api.pollPairingSession({
245
+ pairing_id: pairingId,
246
+ client_secret: clientSecret,
247
+ });
248
+ if (!result?.ok) {
249
+ throw new Error(result?.error || `ticlawk pairing poll failed (${result?.code || result?.statusCode || 'unknown'})`);
250
+ }
251
+ if (result.status && result.status !== lastStatus) {
252
+ lastStatus = result.status;
253
+ if (result.status === 'scanned') {
254
+ console.log('[connect] scanned; waiting for approval...');
255
+ }
256
+ }
257
+ if (result.status === 'approved') return result;
258
+ if (['rejected', 'expired', 'cancelled', 'claimed'].includes(result.status)) {
259
+ throw new Error(`ticlawk pairing ${result.status}`);
260
+ }
261
+ pollAfterMs = Number(result.poll_after_ms || pollAfterMs || 1500);
262
+ }
263
+ throw new Error('ticlawk pairing expired');
264
+ }
265
+
266
+ export function createTiclawkAdapter(ctx) {
267
+ const processingChannels = new Map();
268
+ const hostId = getHostId();
269
+ const hostLabel = getHostLabel();
270
+ const wakeState = {
271
+ connected: false,
272
+ lastStatus: 'idle',
273
+ lastEventAt: null,
274
+ lastError: null,
275
+ lastConnectedAt: null,
276
+ lastCloseAt: null,
277
+ lastCloseCode: null,
278
+ lastCloseReason: null,
279
+ reconnectCount: 0,
280
+ };
281
+ let connectorSocket = null;
282
+ let recoveryTimer = null;
283
+ let bindingAuditTimer = null;
284
+ let jobsWakeTimer = null;
285
+ let bindingsWakeTimer = null;
286
+ let drainPromise = null;
287
+ let drainRequested = false;
288
+ let lastJobsWakeAt = 0;
289
+ let lastBindingsWakeAt = 0;
290
+ let updateRequired = null;
291
+ let lastUpdateRequiredLogAt = 0;
292
+
293
+ function clearDebounce(timer) {
294
+ if (timer) clearTimeout(timer);
295
+ return null;
296
+ }
297
+
298
+ function getUpdateRequiredHealth() {
299
+ const state = updateRequired || readUpdateState();
300
+ if (!state?.updateRequired) {
301
+ return {
302
+ updateRequired: false,
303
+ agentFreewayVersion: api.getAgentFreewayVersion(),
304
+ managedInstall: isManagedInstall(),
305
+ };
306
+ }
307
+ return {
308
+ updateRequired: true,
309
+ agentFreewayVersion: api.getAgentFreewayVersion(),
310
+ currentAgentFreewayVersion: state.currentAgentFreewayVersion || api.getAgentFreewayVersion(),
311
+ requiredAgentFreewayVersion: state.requiredAgentFreewayVersion || null,
312
+ autoUpdateStatus: state.autoUpdateStatus || null,
313
+ autoUpdateError: state.autoUpdateError || null,
314
+ managedInstall: isManagedInstall(),
315
+ };
316
+ }
317
+
318
+ function shouldAttemptAutoUpdate(previousState) {
319
+ const lastAttemptMs = Date.parse(previousState?.lastAutoUpdateAttemptAt || '');
320
+ if (Number.isFinite(lastAttemptMs) && Date.now() - lastAttemptMs < UPDATE_RETRY_COOLDOWN_MS) {
321
+ return false;
322
+ }
323
+ return true;
324
+ }
325
+
326
+ function recordUpdateRequired(err, reason) {
327
+ const now = Date.now();
328
+ const currentVersion = err?.currentAgentFreewayVersion || api.getAgentFreewayVersion();
329
+ const requiredVersion = err?.requiredAgentFreewayVersion || null;
330
+ const previousState = readUpdateState();
331
+ let autoUpdateStatus = previousState?.autoUpdateStatus || 'not_started';
332
+ let autoUpdateError = null;
333
+ let lastAutoUpdateAttemptAt = previousState?.lastAutoUpdateAttemptAt || null;
334
+
335
+ if (!requiredVersion) {
336
+ autoUpdateStatus = 'missing_required_version';
337
+ autoUpdateError = 'backend did not provide required_agent_freeway_version';
338
+ } else if (shouldAttemptAutoUpdate(previousState)) {
339
+ lastAutoUpdateAttemptAt = new Date().toISOString();
340
+ try {
341
+ const result = startDetachedSelfUpdate({
342
+ reason,
343
+ currentVersion,
344
+ requiredVersion,
345
+ });
346
+ autoUpdateStatus = result.started ? 'started' : result.reason;
347
+ if (result.started) {
348
+ debugLog('ticlawk', 'update.auto-started', {
349
+ reason,
350
+ currentAgentFreewayVersion: currentVersion,
351
+ requiredAgentFreewayVersion: requiredVersion,
352
+ pid: result.pid || null,
353
+ logPath: result.logPath || null,
354
+ });
355
+ }
356
+ } catch (updateErr) {
357
+ autoUpdateStatus = 'failed_to_start';
358
+ autoUpdateError = updateErr?.message || 'unknown error';
359
+ }
360
+ } else {
361
+ autoUpdateStatus = 'cooldown';
362
+ }
363
+
364
+ updateRequired = setUpdateRequiredState({
365
+ adapter: 'ticlawk',
366
+ currentVersion,
367
+ requiredVersion,
368
+ autoUpdateStatus,
369
+ autoUpdateError,
370
+ lastAutoUpdateAttemptAt,
371
+ });
372
+
373
+ if (now - lastUpdateRequiredLogAt > UPDATE_REQUIRED_LOG_INTERVAL_MS) {
374
+ lastUpdateRequiredLogAt = now;
375
+ debugError('ticlawk', 'update.required', {
376
+ reason,
377
+ currentAgentFreewayVersion: currentVersion,
378
+ requiredAgentFreewayVersion: requiredVersion,
379
+ autoUpdateStatus,
380
+ managedInstall: isManagedInstall(),
381
+ message: `Ticlawk requires ticlawk >= ${requiredVersion || 'unknown'}, current version is ${currentVersion || 'unknown'}. Please update ticlawk.`,
382
+ });
383
+ }
384
+ }
385
+
386
+ function clearUpdateRequired(reason) {
387
+ if (!updateRequired?.updateRequired && !readUpdateState()?.updateRequired) return;
388
+ updateRequired = null;
389
+ clearUpdateRequiredState('ticlawk');
390
+ debugLog('ticlawk', 'update.cleared', {
391
+ reason,
392
+ currentAgentFreewayVersion: api.getAgentFreewayVersion(),
393
+ });
394
+ }
395
+
396
+ function isLowFrequencyRecovery(reason) {
397
+ return String(reason || '').startsWith('audit');
398
+ }
399
+
400
+ function getRemoteAgentId(agent) {
401
+ return String(agent?.id || agent?.agent_id || '').trim();
402
+ }
403
+
404
+ function isTiclawkBindingForCurrentHost(binding) {
405
+ if (binding?.adapter !== 'ticlawk') return false;
406
+ return getBindingRuntimeHostId(binding) === hostId;
407
+ }
408
+
409
+ async function pruneDeletedBindings(channels, reason) {
410
+ if (typeof ctx.deleteBinding !== 'function') return 0;
411
+ if (typeof ctx.listBindings !== 'function') return 0;
412
+ const activeProfile = getActiveProfile();
413
+ if (activeProfile?.adapter !== 'ticlawk') return 0;
414
+
415
+ const remoteAgentIds = new Set(channels.map(getRemoteAgentId).filter(Boolean));
416
+ let pruned = 0;
417
+ for (const binding of ctx.listBindings({ adapter: 'ticlawk' })) {
418
+ if (!binding?.id) continue;
419
+ if (!isTiclawkBindingForCurrentHost(binding)) continue;
420
+ const bindingAgentIds = [
421
+ binding.id,
422
+ binding.targetKey,
423
+ binding.targetMeta?.agentId,
424
+ ].map((value) => String(value || '').trim()).filter(Boolean);
425
+ if (bindingAgentIds.some((id) => remoteAgentIds.has(id))) continue;
426
+ try {
427
+ await ctx.deleteBinding(binding.id);
428
+ pruned += 1;
429
+ debugLog('ticlawk', 'binding.pruned', {
430
+ reason,
431
+ bindingId: binding.id,
432
+ runtime: binding.runtime || null,
433
+ runtime_host_id: getBindingRuntimeHostId(binding) || null,
434
+ });
435
+ } catch (err) {
436
+ debugError('ticlawk', 'binding.prune-failed', {
437
+ reason,
438
+ bindingId: binding.id,
439
+ error: err?.message || 'unknown error',
440
+ });
441
+ }
442
+ }
443
+ return pruned;
444
+ }
445
+
446
+ async function processPendingMessagesForAgent(agentId, messages) {
447
+ for (const msg of messages) {
448
+ try {
449
+ const messageHostId = getRuntimeHostIdFromPayload(msg);
450
+ if (messageHostId && messageHostId !== hostId) {
451
+ await api.releaseMessage(msg.message_id, hostId);
452
+ debugError('ticlawk', 'message.host-mismatch', {
453
+ agentId,
454
+ messageId: msg.message_id,
455
+ hostId,
456
+ runtime_host_id: messageHostId,
457
+ });
458
+ continue;
459
+ }
460
+
461
+ const binding = await ctx.cacheBinding(buildBindingFromClaimedMessage(msg));
462
+ if (!binding?.runtime) {
463
+ throw new Error('claimed message missing runtime binding');
464
+ }
465
+ if (!belongsToRuntimeHost(binding, hostId)) {
466
+ await api.releaseMessage(msg.message_id, hostId);
467
+ debugError('ticlawk', 'message.binding-host-mismatch', {
468
+ agentId,
469
+ messageId: msg.message_id,
470
+ hostId,
471
+ runtime_host_id: getBindingRuntimeHostId(binding),
472
+ });
473
+ continue;
474
+ }
475
+
476
+ const completed = await ctx.bus.dispatchToAgent(binding.runtime, binding.id, normalizeInboundMessage(msg));
477
+ if (completed !== true) {
478
+ if (isTerminalRuntimeFailure(completed)) {
479
+ await api.completeMessage(msg.message_id, hostId);
480
+ void requestDrain('message.terminal-completed');
481
+ debugError('ticlawk', 'message.terminal-failed', {
482
+ agentId,
483
+ messageId: msg.message_id,
484
+ runtime: binding.runtime,
485
+ runtimeVersion: getRuntimeVersion(binding),
486
+ reason: completed.reason || 'runtime terminal failure',
487
+ });
488
+ continue;
489
+ }
490
+ throw new Error('runtime did not complete turn');
491
+ }
492
+ await api.completeMessage(msg.message_id, hostId);
493
+ void requestDrain('message.completed');
494
+ debugLog('ticlawk', 'message.completed', {
495
+ agentId,
496
+ messageId: msg.message_id,
497
+ runtime: binding.runtime,
498
+ runtimeVersion: getRuntimeVersion(binding),
499
+ });
500
+ } catch (err) {
501
+ if (api.isUpdateRequiredError(err)) {
502
+ recordUpdateRequired(err, 'message.dispatch');
503
+ }
504
+ try {
505
+ await api.releaseMessage(msg.message_id, hostId);
506
+ } catch (releaseErr) {
507
+ debugError('ticlawk', 'message.release-failed', {
508
+ agentId,
509
+ messageId: msg.message_id,
510
+ hostId,
511
+ runtime: msg.agent_service_type || null,
512
+ runtimeVersion: msg.agent_runtime_version ?? null,
513
+ error: releaseErr?.message || 'unknown error',
514
+ });
515
+ }
516
+ debugError('ticlawk', 'message.dispatch-failed', {
517
+ agentId,
518
+ messageId: msg.message_id,
519
+ hostId,
520
+ runtime: msg.agent_service_type || null,
521
+ runtimeVersion: msg.agent_runtime_version ?? null,
522
+ error: err?.message || 'unknown error',
523
+ });
524
+ }
525
+ }
526
+ }
527
+
528
+ async function refreshBindings(reason = 'manual') {
529
+ let channels = [];
530
+ const startedAt = Date.now();
531
+ try {
532
+ channels = await api.getAgents({ hostId });
533
+ } catch (err) {
534
+ if (api.isUpdateRequiredError(err)) {
535
+ recordUpdateRequired(err, 'binding.refresh');
536
+ }
537
+ debugError('ticlawk', 'binding.refresh-failed', {
538
+ reason,
539
+ durationMs: Date.now() - startedAt,
540
+ error: err?.message || 'unknown error',
541
+ });
542
+ return 0;
543
+ }
544
+
545
+ let hydrated = 0;
546
+ for (const agent of channels) {
547
+ if (!agent?.id || !agent?.service_type || !ctx.runtimes[agent.service_type]) continue;
548
+ const agentHostId = getRuntimeHostIdFromPayload(agent);
549
+ if (agentHostId && agentHostId !== hostId) {
550
+ debugError('ticlawk', 'binding.host-mismatch', {
551
+ agentId: agent.id,
552
+ hostId,
553
+ runtime_host_id: agentHostId,
554
+ });
555
+ continue;
556
+ }
557
+ try {
558
+ await ctx.cacheBinding(buildBindingFromChannelSnapshot(agent));
559
+ hydrated += 1;
560
+ } catch (err) {
561
+ debugError('ticlawk', 'binding.hydrate-failed', {
562
+ agentId: agent.id,
563
+ error: err?.message || 'unknown error',
564
+ });
565
+ }
566
+ }
567
+ const pruned = await pruneDeletedBindings(channels, reason);
568
+ debugLog('ticlawk', 'binding.refresh-ok', {
569
+ reason,
570
+ hydrated,
571
+ pruned,
572
+ durationMs: Date.now() - startedAt,
573
+ wakeToRefreshMs: String(reason || '').startsWith('wake') && lastBindingsWakeAt
574
+ ? Date.now() - lastBindingsWakeAt
575
+ : null,
576
+ });
577
+ return hydrated;
578
+ }
579
+
580
+ async function releaseBlockedRows(agentId, messages, reason) {
581
+ for (const msg of messages) {
582
+ if (!msg?.message_id) continue;
583
+ try {
584
+ await api.releaseMessage(msg.message_id, hostId);
585
+ } catch (err) {
586
+ debugError('ticlawk', 'claim.blocked-release-failed', {
587
+ reason,
588
+ agentId,
589
+ messageId: msg.message_id,
590
+ error: err?.message || 'unknown error',
591
+ });
592
+ }
593
+ }
594
+ }
595
+
596
+ async function drainPendingOnce(reason) {
597
+ const startedAt = Date.now();
598
+ const excludedChannelIds = [...processingChannels.keys()];
599
+ let data = [];
600
+ if (updateRequired?.updateRequired && !isLowFrequencyRecovery(reason)) {
601
+ debugLog('ticlawk', 'claim.paused-update-required', {
602
+ reason,
603
+ hostId,
604
+ currentAgentFreewayVersion: updateRequired.currentAgentFreewayVersion || api.getAgentFreewayVersion(),
605
+ requiredAgentFreewayVersion: updateRequired.requiredAgentFreewayVersion || null,
606
+ });
607
+ return { failed: true, claimed: 0, launched: 0, updateRequired: true };
608
+ }
609
+ try {
610
+ data = await api.claimPendingMessages(hostId, 5, excludedChannelIds);
611
+ clearUpdateRequired('claim');
612
+ } catch (err) {
613
+ if (api.isUpdateRequiredError(err)) {
614
+ recordUpdateRequired(err, 'claim');
615
+ return { failed: true, claimed: 0, launched: 0, updateRequired: true };
616
+ }
617
+ debugError('ticlawk', 'claim.failed', {
618
+ reason,
619
+ hostId,
620
+ excludedChannelCount: excludedChannelIds.length,
621
+ durationMs: Date.now() - startedAt,
622
+ error: err?.message || 'unknown error',
623
+ });
624
+ return { failed: true, claimed: 0, launched: 0 };
625
+ }
626
+
627
+ const claimed = Array.isArray(data) ? data.length : 0;
628
+ debugLog('ticlawk', 'claim.result', {
629
+ reason,
630
+ hostId,
631
+ returnedRows: claimed,
632
+ excludedChannelCount: excludedChannelIds.length,
633
+ durationMs: Date.now() - startedAt,
634
+ wakeToClaimMs: String(reason || '').startsWith('wake') && lastJobsWakeAt
635
+ ? Date.now() - lastJobsWakeAt
636
+ : null,
637
+ });
638
+ if (String(reason || '').startsWith('audit') && claimed > 0) {
639
+ debugError('ticlawk', 'audit.found-jobs', {
640
+ reason,
641
+ returnedRows: claimed,
642
+ excludedChannelCount: excludedChannelIds.length,
643
+ });
644
+ }
645
+
646
+ if (!Array.isArray(data) || data.length === 0) {
647
+ return { failed: false, claimed: 0, launched: 0 };
648
+ }
649
+
650
+ const grouped = new Map();
651
+ for (const msg of data) {
652
+ const agentId = getAgentIdFromPayload(msg) || '__default__';
653
+ const bucket = grouped.get(agentId) || [];
654
+ bucket.push(msg);
655
+ grouped.set(agentId, bucket);
656
+ }
657
+
658
+ let launched = 0;
659
+ for (const [agentId, messages] of grouped.entries()) {
660
+ if (processingChannels.has(agentId)) {
661
+ debugError('ticlawk', 'claim.blocked-claimed-rows', {
662
+ reason,
663
+ agentId,
664
+ blockedRows: messages.length,
665
+ });
666
+ await releaseBlockedRows(agentId, messages, reason);
667
+ continue;
668
+ }
669
+ const run = processPendingMessagesForAgent(agentId, messages)
670
+ .catch(() => {})
671
+ .finally(() => {
672
+ processingChannels.delete(agentId);
673
+ void requestDrain('channel.completed');
674
+ });
675
+ processingChannels.set(agentId, run);
676
+ launched += messages.length;
677
+ }
678
+
679
+ return { failed: false, claimed: data.length, launched };
680
+ }
681
+
682
+ async function runDrain(reason) {
683
+ let totalClaimed = 0;
684
+ let iterations = 0;
685
+ while (true) {
686
+ iterations += 1;
687
+ const result = await drainPendingOnce(reason);
688
+ totalClaimed += result.claimed;
689
+ if (result.failed || result.claimed === 0 || result.launched === 0) {
690
+ break;
691
+ }
692
+ }
693
+ return { totalClaimed, iterations };
694
+ }
695
+
696
+ function requestDrain(reason) {
697
+ if (drainPromise) {
698
+ drainRequested = true;
699
+ return drainPromise;
700
+ }
701
+ drainPromise = (async () => {
702
+ let currentReason = reason;
703
+ do {
704
+ drainRequested = false;
705
+ await runDrain(currentReason);
706
+ currentReason = 'drain.requested-again';
707
+ } while (drainRequested);
708
+ })().finally(() => {
709
+ drainPromise = null;
710
+ });
711
+ return drainPromise;
712
+ }
713
+
714
+ function scheduleDrain(reason) {
715
+ jobsWakeTimer = clearDebounce(jobsWakeTimer);
716
+ jobsWakeTimer = setTimeout(() => {
717
+ jobsWakeTimer = null;
718
+ void requestDrain(reason);
719
+ }, JOBS_WAKE_DEBOUNCE_MS);
720
+ jobsWakeTimer.unref?.();
721
+ }
722
+
723
+ function scheduleRefreshAndDrain(reason) {
724
+ bindingsWakeTimer = clearDebounce(bindingsWakeTimer);
725
+ bindingsWakeTimer = setTimeout(() => {
726
+ bindingsWakeTimer = null;
727
+ void refreshBindings(reason)
728
+ .then(() => requestDrain(reason))
729
+ .catch(() => {});
730
+ }, BINDINGS_WAKE_DEBOUNCE_MS);
731
+ bindingsWakeTimer.unref?.();
732
+ }
733
+
734
+ function handleWakeEvent(event) {
735
+ wakeState.lastEventAt = new Date().toISOString();
736
+ if (event?.type === 'hello') {
737
+ void refreshBindings('wake.hello')
738
+ .then(() => requestDrain('wake.hello'))
739
+ .catch(() => {});
740
+ return;
741
+ }
742
+ if (event?.type === 'jobs.available') {
743
+ lastJobsWakeAt = Date.now();
744
+ debugLog('ticlawk-wake', 'jobs.available', {
745
+ agentId: event.agent_id || null,
746
+ reason: event.reason || null,
747
+ });
748
+ scheduleDrain('wake.jobs.available');
749
+ return;
750
+ }
751
+ if (event?.type === 'bindings.changed') {
752
+ lastBindingsWakeAt = Date.now();
753
+ debugLog('ticlawk-wake', 'bindings.changed', {
754
+ agentId: event.agent_id || null,
755
+ reason: event.reason || null,
756
+ });
757
+ scheduleRefreshAndDrain('wake.bindings.changed');
758
+ return;
759
+ }
760
+ if (event?.type === 'auth.revoked') {
761
+ wakeState.lastError = 'auth revoked';
762
+ debugError('ticlawk-wake', 'auth.revoked', {});
763
+ }
764
+ }
765
+
766
+ function handleWakeStatus(status) {
767
+ wakeState.connected = Boolean(status.connected);
768
+ wakeState.lastStatus = status.state || wakeState.lastStatus;
769
+ if (status.state === 'connected') {
770
+ wakeState.lastConnectedAt = new Date().toISOString();
771
+ wakeState.lastError = null;
772
+ }
773
+ if (status.state === 'closed') {
774
+ wakeState.lastCloseAt = new Date().toISOString();
775
+ wakeState.lastCloseCode = status.code ?? null;
776
+ wakeState.lastCloseReason = status.reason || null;
777
+ }
778
+ if (status.error) wakeState.lastError = status.error;
779
+ if (status.state === 'reconnecting') wakeState.reconnectCount += 1;
780
+ debugLog('ticlawk-wake', 'status', {
781
+ state: status.state,
782
+ connected: status.connected,
783
+ attempt: status.attempt || null,
784
+ delayMs: status.delayMs || null,
785
+ code: status.code || null,
786
+ reason: status.reason || null,
787
+ wasConnected: status.wasConnected ?? null,
788
+ error: status.error || null,
789
+ });
790
+ }
791
+
792
+ function connectWakeSocket() {
793
+ connectorSocket = new TiclawkWakeClient({
794
+ getUrl: api.getConnectorWsUrl,
795
+ getApiKey: api.getApiKey,
796
+ onEvent: handleWakeEvent,
797
+ onStatus: handleWakeStatus,
798
+ logger: { debugLog, debugError },
799
+ });
800
+ connectorSocket.start();
801
+ }
802
+
803
+ function restartWakeSocket(reason) {
804
+ if (connectorSocket) {
805
+ connectorSocket.stop();
806
+ }
807
+ debugLog('ticlawk-wake', 'restart', { reason });
808
+ connectWakeSocket();
809
+ }
810
+
811
+ function startAuditTimers() {
812
+ recoveryTimer = setInterval(() => {
813
+ void requestDrain('audit.recovery');
814
+ }, RECOVERY_AUDIT_INTERVAL_MS);
815
+ recoveryTimer.unref?.();
816
+ bindingAuditTimer = setInterval(() => {
817
+ void refreshBindings('audit.bindings');
818
+ }, BINDING_AUDIT_INTERVAL_MS);
819
+ bindingAuditTimer.unref?.();
820
+ }
821
+
822
+ async function finishPairing(resolved, pairData) {
823
+ const apiKey = pairData.connector_api_key || pairData.apiKey || pairData.api_key;
824
+ persistApiCredential(apiKey);
825
+ if (pairData.connector_ws_url) {
826
+ persistConfig({ [TICLAWK_CONNECTOR_WS_URL]: pairData.connector_ws_url });
827
+ }
828
+ const pairedIdentity = maskIdentity(pairData.user || pairData);
829
+ if (pairedIdentity.userId) {
830
+ saveAndActivateProfile({
831
+ adapter: 'ticlawk',
832
+ userId: pairedIdentity.userId,
833
+ config: loadPersistentConfig(),
834
+ meta: pairedIdentity,
835
+ });
836
+ }
837
+ if (connectorSocket) {
838
+ restartWakeSocket('connect.paired');
839
+ }
840
+
841
+ const bindingId = pairData.agent_id || pairData.agentId;
842
+ if (!bindingId) {
843
+ throw new Error('pairing did not return agent_id');
844
+ }
845
+ const runtimeMeta = pairData.runtime_meta || resolved.runtimeMeta;
846
+ const binding = await ctx.upsertBinding({
847
+ id: bindingId,
848
+ adapter: 'ticlawk',
849
+ targetKey: bindingId,
850
+ targetMeta: {
851
+ agentId: bindingId,
852
+ runtime_host_id: hostId,
853
+ binding_key: pairData.binding_key || null,
854
+ },
855
+ runtime_host_id: hostId,
856
+ runtime_host_label: hostLabel,
857
+ runtime: resolved.runtime,
858
+ runtimeMeta,
859
+ displayName: resolved.displayName,
860
+ status: 'connected',
861
+ });
862
+ return {
863
+ statusCode: 200,
864
+ body: {
865
+ ok: true,
866
+ agentId: binding.id,
867
+ serviceType: resolved.runtime,
868
+ name: binding.displayName,
869
+ bindingKey: pairData.binding_key || null,
870
+ user: pairedIdentity,
871
+ },
872
+ };
873
+ }
874
+
875
+ async function connectWithQrPairing(payload) {
876
+ const autoRuntime = Boolean(payload?.autoRuntime);
877
+ const runtimeOptions = autoRuntime ? await buildAutoRuntimeOptions(ctx, payload) : [];
878
+ if (autoRuntime && runtimeOptions.length === 0) {
879
+ throw new Error('No supported local agent runtime found in this terminal. Install or sign in to Codex, Claude Code, OpenCode, or pi, then try again.');
880
+ }
881
+ const resolved = autoRuntime ? null : await ctx.resolveRuntimeBinding(payload);
882
+ const runtimeMeta = resolved?.runtimeMeta || {};
883
+ const workdir = getRuntimeWorkdir(runtimeMeta) || getRuntimeWorkdir(payload) || process.cwd();
884
+ const clientSecret = makeClientSecret();
885
+ const created = await api.createPairingSession({
886
+ client: 'ticlawk',
887
+ client_version: api.getAgentFreewayVersion(),
888
+ host_id: hostId,
889
+ host_label: hostLabel,
890
+ ...(autoRuntime ? { runtime_options: runtimeOptions } : { runtime: resolved.runtime }),
891
+ workdir,
892
+ display_name: resolved?.displayName || basename(workdir) || 'Agent',
893
+ challenge_hash: sha256Hex(clientSecret),
894
+ });
895
+ if (!created?.ok) {
896
+ return {
897
+ statusCode: created?.statusCode || 400,
898
+ body: created || { ok: false, error: 'ticlawk pairing create failed' },
899
+ };
900
+ }
901
+ if (!created.pairing_id || !created.display_code || !created.qr_payload) {
902
+ return connectError(502, 'ticlawk pairing create response was incomplete');
903
+ }
904
+
905
+ printPairingChallenge(created);
906
+ try {
907
+ const approved = await waitForPairingApproval({
908
+ pairingId: created.pairing_id,
909
+ clientSecret,
910
+ pollAfterMs: Number(created.poll_after_ms || 1500),
911
+ expiresAt: created.expires_at,
912
+ });
913
+ const approvedRuntime = approved.runtime || approved.selected_runtime;
914
+ const approvedOption = runtimeOptions.find((option) => option.runtime === approvedRuntime) || null;
915
+ const finalResolved = resolved || {
916
+ runtime: approvedRuntime || approvedOption?.runtime,
917
+ displayName: approved.display_name || approvedOption?.display_name || runtimeLabel(approvedRuntime),
918
+ runtimeMeta: approved.runtime_meta || approvedOption?.runtime_meta || {},
919
+ };
920
+ if (!finalResolved.runtime) {
921
+ throw new Error('ticlawk pairing did not return a selected runtime');
922
+ }
923
+ return await finishPairing.call(this, finalResolved, {
924
+ ...created,
925
+ ...approved,
926
+ binding_key: approved.binding_key || created.binding_key || null,
927
+ });
928
+ } catch (err) {
929
+ await api.cancelPairingSession({
930
+ pairing_id: created.pairing_id,
931
+ client_secret: clientSecret,
932
+ });
933
+ throw err;
934
+ }
935
+ }
936
+
937
+ return {
938
+ id: 'ticlawk',
939
+
940
+ async start() {
941
+ try {
942
+ const recovered = await api.recoverClaimedMessages(hostId);
943
+ if (recovered?.recoveredCount) {
944
+ debugLog('ticlawk', 'message.recovered', {
945
+ recoveredCount: recovered.recoveredCount,
946
+ hostId,
947
+ });
948
+ }
949
+ } catch (err) {
950
+ if (api.isUpdateRequiredError(err)) {
951
+ recordUpdateRequired(err, 'recover');
952
+ }
953
+ debugError('ticlawk', 'message.recover-failed', {
954
+ hostId,
955
+ error: err?.message || 'unknown error',
956
+ });
957
+ }
958
+
959
+ await refreshBindings('startup');
960
+ await requestDrain('startup');
961
+ connectWakeSocket();
962
+ startAuditTimers();
963
+ },
964
+
965
+ async refreshBindings() {
966
+ return refreshBindings('manual');
967
+ },
968
+
969
+ async health() {
970
+ const config = loadPersistentConfig();
971
+ const runtimeHealthEntries = await Promise.all(Object.entries(ctx.runtimes || {})
972
+ .filter(([, runtime]) => typeof runtime?.health === 'function')
973
+ .map(async ([name, runtime]) => [name, await runtime.health()]));
974
+ const runtimesHealth = Object.fromEntries(runtimeHealthEntries);
975
+ const codexHealth = runtimesHealth.codex || {};
976
+ return {
977
+ apiKey: Boolean(process.env[TICLAWK_CONNECTOR_API_KEY] || config[TICLAWK_CONNECTOR_API_KEY] || config[LEGACY_TICLAWK_API_KEY]),
978
+ wakeUrl: api.getConnectorWsUrl(),
979
+ wakeConnected: Boolean(connectorSocket?.isConnected()),
980
+ wakeLastStatus: wakeState.lastStatus,
981
+ wakeLastEventAt: wakeState.lastEventAt,
982
+ wakeLastError: wakeState.lastError,
983
+ wakeLastConnectedAt: wakeState.lastConnectedAt,
984
+ wakeLastCloseAt: wakeState.lastCloseAt,
985
+ wakeLastCloseCode: wakeState.lastCloseCode,
986
+ wakeLastCloseReason: wakeState.lastCloseReason,
987
+ wakeReconnectCount: wakeState.reconnectCount,
988
+ ...getUpdateRequiredHealth(),
989
+ runtimes: runtimesHealth,
990
+ codexAvailable: codexHealth.available,
991
+ codexPath: codexHealth.path || null,
992
+ codexVersion: codexHealth.version || null,
993
+ gatewayConnected: ctx.runtimes.openclaw?.isReady?.() || undefined,
994
+ };
995
+ },
996
+
997
+ async connect(payload) {
998
+ const config = loadPersistentConfig();
999
+ const connectCode = String(payload?.code || config.TICLAWK_SETUP_CODE || '').trim();
1000
+ if (!connectCode) {
1001
+ try {
1002
+ return await connectWithQrPairing.call(this, payload);
1003
+ } catch (err) {
1004
+ return connectError(err?.status || 500, err.message);
1005
+ }
1006
+ }
1007
+
1008
+ try {
1009
+ const resolved = await ctx.resolveRuntimeBinding(payload);
1010
+ const runtimeMeta = resolved.runtimeMeta;
1011
+ let currentIdentity = null;
1012
+ const activeProfile = getActiveProfile();
1013
+ if (activeProfile?.adapter === 'ticlawk') {
1014
+ currentIdentity = readProfileMeta(activeProfile.adapter, activeProfile.userId) || {
1015
+ userId: activeProfile.userId,
1016
+ };
1017
+ } else if (config[TICLAWK_CONNECTOR_API_KEY] || config[LEGACY_TICLAWK_API_KEY]) {
1018
+ try {
1019
+ const me = await api.getMe();
1020
+ if (me?.userId || me?.user_id) {
1021
+ currentIdentity = maskIdentity(me);
1022
+ ensureLegacyProfile({
1023
+ adapter: 'ticlawk',
1024
+ userId: currentIdentity.userId,
1025
+ meta: currentIdentity,
1026
+ });
1027
+ }
1028
+ } catch {}
1029
+ }
1030
+
1031
+ let previewIdentity = null;
1032
+ try {
1033
+ const preview = await api.pairPreview({ code: connectCode });
1034
+ if (preview?.ok) {
1035
+ previewIdentity = maskIdentity(preview);
1036
+ } else if (preview?.userId || preview?.user_id) {
1037
+ previewIdentity = maskIdentity(preview);
1038
+ }
1039
+ } catch {}
1040
+
1041
+ if (currentIdentity?.userId && !previewIdentity?.userId && !payload.switchUser) {
1042
+ return {
1043
+ statusCode: 409,
1044
+ body: {
1045
+ ok: false,
1046
+ code: 'ticlawk_pairing_user_unverified',
1047
+ currentUser: currentIdentity,
1048
+ error: [
1049
+ 'This ticlawk home is already paired to ticlawk user:',
1050
+ ...formatIdentityLines(currentIdentity),
1051
+ '',
1052
+ 'Could not verify which ticlawk user owns the new pairing code.',
1053
+ 'Refusing to switch users because existing agents may stop processing messages.',
1054
+ '',
1055
+ 'If you want to switch user, please rerun this command with --switch-user.',
1056
+ ].join('\n'),
1057
+ },
1058
+ };
1059
+ }
1060
+
1061
+ if (
1062
+ currentIdentity?.userId
1063
+ && previewIdentity?.userId
1064
+ && currentIdentity.userId !== previewIdentity.userId
1065
+ && !payload.switchUser
1066
+ ) {
1067
+ let affectedAgents = [];
1068
+ try {
1069
+ affectedAgents = await api.getAgents({ hostId });
1070
+ } catch {}
1071
+ const message = [
1072
+ 'This ticlawk home is already paired to ticlawk user:',
1073
+ ...formatIdentityLines(currentIdentity),
1074
+ '',
1075
+ 'The pairing code belongs to a different ticlawk user:',
1076
+ ...formatIdentityLines(previewIdentity),
1077
+ '',
1078
+ 'Refusing to switch users because these agents are currently bound to this host:',
1079
+ ...formatAffectedAgents(affectedAgents),
1080
+ '',
1081
+ 'Switching would stop this daemon from processing messages for those agents.',
1082
+ '',
1083
+ 'If you want to switch user, please rerun this command with --switch-user.',
1084
+ ].join('\n');
1085
+ return {
1086
+ statusCode: 409,
1087
+ body: {
1088
+ ok: false,
1089
+ error: message,
1090
+ code: 'ticlawk_user_mismatch',
1091
+ currentUser: currentIdentity,
1092
+ pairingUser: previewIdentity,
1093
+ affectedAgents,
1094
+ },
1095
+ };
1096
+ }
1097
+
1098
+ const pairData = await api.pair({
1099
+ code: connectCode,
1100
+ name: resolved.displayName,
1101
+ serviceType: resolved.runtime,
1102
+ runtimeMeta,
1103
+ runtime_host_id: hostId,
1104
+ runtime_host_label: hostLabel,
1105
+ });
1106
+ if (!pairData?.ok) {
1107
+ return { statusCode: pairData?.statusCode || 401, body: pairData };
1108
+ }
1109
+ return finishPairing.call(this, resolved, {
1110
+ ...pairData,
1111
+ agent_id: pairData.agentId,
1112
+ connector_api_key: pairData.apiKey,
1113
+ });
1114
+ } catch (err) {
1115
+ return connectError(err?.status || 500, err.message);
1116
+ }
1117
+ },
1118
+
1119
+ async send(binding, outbound) {
1120
+ const localMediaPaths = (outbound.media || [])
1121
+ .filter((item) => item.kind === 'local_path')
1122
+ .map((item) => item.value);
1123
+ await processAndSaveResult({
1124
+ text: outbound.text || '',
1125
+ mediaUrls: localMediaPaths,
1126
+ }, {
1127
+ agentId: binding.id,
1128
+ hostId,
1129
+ agent: binding.runtime,
1130
+ sessionId: getBindingSessionId(binding),
1131
+ runtimeVersion: getRuntimeVersion(binding),
1132
+ turnId: outbound.turnId || outbound.replyToMessageId || null,
1133
+ type: outbound.type || 'agent_message',
1134
+ replyToMessageId: outbound.replyToMessageId || null,
1135
+ });
1136
+ },
1137
+
1138
+ async emitEvent(binding, payload) {
1139
+ if (!payload?.event) return;
1140
+ await api.postEvent({
1141
+ agent: payload.agent,
1142
+ agent_id: binding.id,
1143
+ runtime_host_id: hostId,
1144
+ session_id: payload.sessionId,
1145
+ cwd: payload.cwd || '',
1146
+ runtime_version: getRuntimeVersion(binding),
1147
+ event: payload.event,
1148
+ });
1149
+ },
1150
+
1151
+ async syncBinding(binding) {
1152
+ const updates = {
1153
+ status: binding.status || 'connected',
1154
+ runtime_host_id: hostId,
1155
+ runtime_host_label: hostLabel,
1156
+ };
1157
+ const syncableMeta = buildSyncableRuntimeMeta(binding);
1158
+ if (syncableMeta) {
1159
+ updates.meta = syncableMeta;
1160
+ }
1161
+ try {
1162
+ await api.updateAgent(binding.id, updates);
1163
+ } catch (err) {
1164
+ if (api.isUpdateRequiredError(err)) {
1165
+ recordUpdateRequired(err, 'binding.sync');
1166
+ }
1167
+ debugError('ticlawk', err?.status === 409 ? 'binding.sync-host-mismatch' : 'binding.sync-failed', {
1168
+ agentId: binding.id,
1169
+ hostId,
1170
+ runtime_host_id: getBindingRuntimeHostId(binding) || null,
1171
+ error: err?.message || 'unknown error',
1172
+ });
1173
+ if (err?.status === 409) {
1174
+ throw err;
1175
+ }
1176
+ }
1177
+ },
1178
+ };
1179
+ }
1180
+
1181
+ export function getTiclawkAuthHelp() {
1182
+ return `ticlawk auth ticlawk --code <6-digit-code> [--api-url <url>]
1183
+
1184
+ Options:
1185
+ --code <code> 6-digit setup code from the ticlawk app
1186
+ --api-url <url> optional ticlawk API base URL override
1187
+ `;
1188
+ }
1189
+
1190
+ export async function runTiclawkAuth(rawArgs) {
1191
+ const args = parseOptionArgs(rawArgs);
1192
+ if (args.help || args.h) {
1193
+ return {
1194
+ statusCode: 200,
1195
+ body: {
1196
+ ok: true,
1197
+ help: getTiclawkAuthHelp(),
1198
+ },
1199
+ };
1200
+ }
1201
+ const code = String(args.code || '').trim();
1202
+ if (!code) {
1203
+ return {
1204
+ statusCode: 400,
1205
+ body: {
1206
+ ok: false,
1207
+ error: 'ticlawk auth requires --code',
1208
+ },
1209
+ };
1210
+ }
1211
+ const updates = {
1212
+ [AF_ADAPTER_KEY]: 'ticlawk',
1213
+ TICLAWK_SETUP_CODE: code,
1214
+ };
1215
+ const apiUrl = String(args['api-url'] || '').trim();
1216
+ if (apiUrl) {
1217
+ updates.TICLAWK_API_URL = apiUrl;
1218
+ }
1219
+ persistConfig(updates);
1220
+ return {
1221
+ statusCode: 200,
1222
+ body: {
1223
+ ok: true,
1224
+ adapter: 'ticlawk',
1225
+ setupCode: 'set',
1226
+ apiUrl: apiUrl || undefined,
1227
+ },
1228
+ };
1229
+ }