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,423 @@
1
+ /**
2
+ * pi local session helpers.
3
+ *
4
+ * pi exposes a JSONL RPC mode over stdin/stdout via `pi --mode rpc`. We spawn
5
+ * one RPC process per turn, stream text deltas while it runs, then read the
6
+ * final assistant message and session state before terminating the process.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
10
+ import { readFile, stat, writeFile } from 'node:fs/promises';
11
+ import { spawn, spawnSync } from 'node:child_process';
12
+ import { homedir } from 'node:os';
13
+ import { extname, join } from 'node:path';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { buildRuntimeEnv } from '../../core/runtime-env.mjs';
16
+ import { getRuntimeExecutableConfig } from '../../core/config.mjs';
17
+ import { isExecutablePath, resolveExecutable } from '../../core/executables.mjs';
18
+
19
+ export const DEFAULT_PI_COMMAND = 'pi';
20
+ export const PI_AGENT_DIR = process.env.PI_CODING_AGENT_DIR || `${homedir()}/.pi/agent`;
21
+ export const PI_SESSIONS_DIR = process.env.PI_SESSIONS_DIR || `${PI_AGENT_DIR}/sessions`;
22
+ export const PI_MAX_AGE_MS = 24 * 60 * 60 * 1000;
23
+ export const DEFAULT_PI_RUN_TIMEOUT_MS = 7200 * 1000;
24
+
25
+ const EXT_TO_MIME = {
26
+ '.jpg': 'image/jpeg',
27
+ '.jpeg': 'image/jpeg',
28
+ '.png': 'image/png',
29
+ '.gif': 'image/gif',
30
+ '.webp': 'image/webp',
31
+ '.bmp': 'image/bmp',
32
+ '.tiff': 'image/tiff',
33
+ '.tif': 'image/tiff',
34
+ '.heic': 'image/heic',
35
+ '.heif': 'image/heif',
36
+ };
37
+
38
+ function buildPiError(message, info) {
39
+ const wrapped = new Error(message);
40
+ wrapped.info = info;
41
+ return wrapped;
42
+ }
43
+
44
+ export function resolvePiPath(preferredPath = null) {
45
+ return resolveExecutable({
46
+ command: DEFAULT_PI_COMMAND,
47
+ preferredPath,
48
+ configuredPath: getRuntimeExecutableConfig('pi'),
49
+ envKey: 'PI_BIN',
50
+ });
51
+ }
52
+
53
+ export function requirePiPath(preferredPath = null) {
54
+ const requested = String(preferredPath || '').trim();
55
+ if (requested && requested.includes('/') && !isExecutablePath(requested)) {
56
+ throw new Error(`pi CLI is no longer available at: ${requested}. Reconnect this pi agent or set PI_BIN / \`ticlawk config set runtimes.pi.path <path>\`.`);
57
+ }
58
+ const piPath = resolvePiPath(preferredPath);
59
+ if (piPath) return piPath;
60
+ throw new Error('pi CLI not found. Install pi, ensure it is on PATH, or set PI_BIN / `ticlawk config set runtimes.pi.path <path>`.');
61
+ }
62
+
63
+ export function getPiRuntimeHealth(preferredPath = null) {
64
+ const piPath = resolvePiPath(preferredPath);
65
+ return {
66
+ available: Boolean(piPath),
67
+ path: piPath,
68
+ version: getPiVersion(piPath),
69
+ };
70
+ }
71
+
72
+ function getPiVersion(piPath) {
73
+ if (!piPath) return null;
74
+ const result = spawnSync(piPath, ['--version'], {
75
+ env: buildRuntimeEnv(),
76
+ encoding: 'utf8',
77
+ timeout: 15000,
78
+ });
79
+ if (result.error || result.status !== 0) return null;
80
+ const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
81
+ return output.split('\n').map(line => line.trim()).find(Boolean) || null;
82
+ }
83
+
84
+ function encodeSessionDirName(cwd) {
85
+ return `--${String(cwd || '').replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
86
+ }
87
+
88
+ function walkJsonlFiles(rootDir) {
89
+ if (!existsSync(rootDir)) return [];
90
+ const files = [];
91
+ const stack = [rootDir];
92
+ while (stack.length > 0) {
93
+ const current = stack.pop();
94
+ let entries = [];
95
+ try {
96
+ entries = readdirSync(current, { withFileTypes: true });
97
+ } catch {
98
+ continue;
99
+ }
100
+ for (const entry of entries) {
101
+ const fullPath = join(current, entry.name);
102
+ if (entry.isDirectory()) {
103
+ stack.push(fullPath);
104
+ } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
105
+ files.push(fullPath);
106
+ }
107
+ }
108
+ }
109
+ return files;
110
+ }
111
+
112
+ function readSessionHeader(filePath) {
113
+ try {
114
+ const firstLine = readFileSync(filePath, 'utf8').split('\n').find(line => line.trim());
115
+ const header = firstLine ? JSON.parse(firstLine) : null;
116
+ if (header?.type !== 'session' || !header.id) return null;
117
+ return {
118
+ sessionId: String(header.id),
119
+ cwd: header.cwd || '',
120
+ path: filePath,
121
+ };
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ export function discoverPiSessions(cwd = null, rootDir = PI_SESSIONS_DIR) {
128
+ const searchRoot = cwd ? join(rootDir, encodeSessionDirName(cwd)) : rootDir;
129
+ return walkJsonlFiles(searchRoot)
130
+ .map(readSessionHeader)
131
+ .filter(Boolean);
132
+ }
133
+
134
+ export function findPiSessionById(sessionId, cwd = null) {
135
+ if (!sessionId) return null;
136
+ return discoverPiSessions(cwd).find((session) => {
137
+ return session.sessionId === sessionId || session.sessionId.startsWith(sessionId);
138
+ }) || null;
139
+ }
140
+
141
+ function buildPiRpcArgs({ sessionId }) {
142
+ const args = ['--mode', 'rpc'];
143
+ if (sessionId) args.push('--session', sessionId);
144
+ return args;
145
+ }
146
+
147
+ function serializeJsonLine(value) {
148
+ return `${JSON.stringify(value)}\n`;
149
+ }
150
+
151
+ function extractDeltaFromEvent(event) {
152
+ const messageEvent = event?.assistantMessageEvent || event?.messageEvent || event?.event || event;
153
+ if (!messageEvent || typeof messageEvent !== 'object') return '';
154
+ if (messageEvent.type === 'text_delta') {
155
+ return typeof messageEvent.delta === 'string' ? messageEvent.delta : '';
156
+ }
157
+ for (const key of ['delta', 'textDelta', 'text', 'content']) {
158
+ if (typeof messageEvent[key] === 'string') return messageEvent[key];
159
+ }
160
+ return '';
161
+ }
162
+
163
+ function extractFinalTextFromEvent(event) {
164
+ if (!event || typeof event !== 'object') return '';
165
+ if (Array.isArray(event.messages)) {
166
+ const assistant = [...event.messages].reverse().find((message) => message?.role === 'assistant');
167
+ const content = assistant?.content;
168
+ if (typeof content === 'string') return content;
169
+ if (Array.isArray(content)) {
170
+ return content
171
+ .map((part) => typeof part === 'string' ? part : (part?.text || part?.content || ''))
172
+ .filter(Boolean)
173
+ .join('');
174
+ }
175
+ }
176
+ return '';
177
+ }
178
+
179
+ function shouldAnswerExtensionRequest(event) {
180
+ return event?.type === 'extension_ui_request'
181
+ && event.id
182
+ && ['select', 'confirm', 'input', 'editor'].includes(event.method);
183
+ }
184
+
185
+ function extensionCancelResponse(event) {
186
+ if (event.method === 'confirm') {
187
+ return { type: 'extension_ui_response', id: event.id, confirmed: false };
188
+ }
189
+ return { type: 'extension_ui_response', id: event.id, cancelled: true };
190
+ }
191
+
192
+ export function runPiPrompt({
193
+ sessionId,
194
+ cwd,
195
+ message,
196
+ images = [],
197
+ piPath = null,
198
+ timeoutMs = Number(process.env.PI_RUN_TIMEOUT_MS || DEFAULT_PI_RUN_TIMEOUT_MS),
199
+ onEvent,
200
+ }) {
201
+ return new Promise((resolve, reject) => {
202
+ const startedAt = Date.now();
203
+ const piCommand = requirePiPath(piPath);
204
+ const child = spawn(piCommand, buildPiRpcArgs({ sessionId }), {
205
+ cwd,
206
+ env: buildRuntimeEnv(),
207
+ stdio: ['pipe', 'pipe', 'pipe'],
208
+ });
209
+
210
+ let nextId = 1;
211
+ let lineBuffer = '';
212
+ let stderr = '';
213
+ let activeSessionId = sessionId || null;
214
+ let activeSessionFile = null;
215
+ let finalText = '';
216
+ let settled = false;
217
+ let eventChain = Promise.resolve();
218
+ const pending = new Map();
219
+
220
+ const emit = (event) => {
221
+ if (typeof onEvent !== 'function') return;
222
+ eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
223
+ };
224
+
225
+ const settle = (fn, value) => {
226
+ if (settled) return;
227
+ settled = true;
228
+ if (timeout) clearTimeout(timeout);
229
+ for (const { reject: rejectPending } of pending.values()) {
230
+ try { rejectPending(new Error('pi RPC process closed')); } catch {}
231
+ }
232
+ pending.clear();
233
+ try { child.kill('SIGTERM'); } catch {}
234
+ eventChain.catch(() => {}).finally(() => fn(value));
235
+ };
236
+
237
+ const sendRaw = (payload) => {
238
+ child.stdin.write(serializeJsonLine(payload));
239
+ };
240
+
241
+ const send = (command, responseTimeoutMs = 30000) => {
242
+ const id = `req_${nextId++}`;
243
+ const payload = { ...command, id };
244
+ return new Promise((resolvePending, rejectPending) => {
245
+ const responseTimeout = setTimeout(() => {
246
+ pending.delete(id);
247
+ rejectPending(new Error(`timeout waiting for pi response to ${command.type}`));
248
+ }, responseTimeoutMs);
249
+ responseTimeout.unref?.();
250
+ pending.set(id, {
251
+ resolve: (response) => {
252
+ clearTimeout(responseTimeout);
253
+ if (response?.success === false) {
254
+ rejectPending(new Error(response.error || `pi ${command.type} failed`));
255
+ return;
256
+ }
257
+ resolvePending(response?.data);
258
+ },
259
+ reject: rejectPending,
260
+ });
261
+ sendRaw(payload);
262
+ });
263
+ };
264
+
265
+ const completion = new Promise((resolveCompletion, rejectCompletion) => {
266
+ child.on('error', (err) => {
267
+ rejectCompletion(buildPiError(err.message || 'pi spawn failed', {
268
+ ok: false,
269
+ code: null,
270
+ signal: null,
271
+ durationMs: Date.now() - startedAt,
272
+ kind: 'spawn-failed',
273
+ error: err.message,
274
+ }));
275
+ });
276
+
277
+ child.on('exit', (code, signal) => {
278
+ if (settled) return;
279
+ rejectCompletion(buildPiError(`pi exited before turn completion${code !== null ? ` (code ${code})` : ''}${signal ? ` (${signal})` : ''}`, {
280
+ ok: false,
281
+ code,
282
+ signal,
283
+ durationMs: Date.now() - startedAt,
284
+ kind: signal ? 'killed' : 'exit-error',
285
+ errorMessage: stderr.trim() || 'pi exited before turn completion',
286
+ }));
287
+ });
288
+
289
+ const handleEvent = (event) => {
290
+ if (shouldAnswerExtensionRequest(event)) {
291
+ sendRaw(extensionCancelResponse(event));
292
+ return;
293
+ }
294
+ if (event?.type === 'agent_start') {
295
+ emit({ type: 'turn.started', sessionId: activeSessionId });
296
+ return;
297
+ }
298
+ if (event?.type === 'message_update') {
299
+ const delta = extractDeltaFromEvent(event);
300
+ if (delta) {
301
+ finalText += delta;
302
+ emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
303
+ }
304
+ return;
305
+ }
306
+ if (event?.type === 'agent_end') {
307
+ finalText = extractFinalTextFromEvent(event) || finalText;
308
+ resolveCompletion();
309
+ }
310
+ };
311
+
312
+ child.stdout.on('data', (chunk) => {
313
+ lineBuffer += chunk.toString('utf8');
314
+ const lines = lineBuffer.split('\n');
315
+ lineBuffer = lines.pop() || '';
316
+ for (const line of lines) {
317
+ const trimmed = line.trim();
318
+ if (!trimmed) continue;
319
+ let msg;
320
+ try { msg = JSON.parse(trimmed); } catch { continue; }
321
+ if (msg?.type === 'response' && msg.id && pending.has(msg.id)) {
322
+ pending.get(msg.id).resolve(msg);
323
+ pending.delete(msg.id);
324
+ continue;
325
+ }
326
+ handleEvent(msg);
327
+ }
328
+ });
329
+ });
330
+
331
+ child.stderr.on('data', (chunk) => {
332
+ stderr = (stderr + chunk.toString('utf8')).slice(-4000);
333
+ });
334
+
335
+ let timeout = null;
336
+ if (timeoutMs > 0) {
337
+ timeout = setTimeout(() => {
338
+ settle(reject, buildPiError('pi turn timed out', {
339
+ ok: false,
340
+ code: null,
341
+ signal: 'SIGTERM',
342
+ durationMs: Date.now() - startedAt,
343
+ kind: 'killed',
344
+ errorMessage: 'pi turn timed out',
345
+ }));
346
+ }, timeoutMs);
347
+ timeout.unref();
348
+ }
349
+
350
+ (async () => {
351
+ try {
352
+ await send({ type: 'prompt', message, images });
353
+ await completion;
354
+ const state = await send({ type: 'get_state' }).catch(() => null);
355
+ const lastAssistant = await send({ type: 'get_last_assistant_text' }).catch(() => null);
356
+ activeSessionId = state?.sessionId || activeSessionId;
357
+ activeSessionFile = state?.sessionFile || activeSessionFile;
358
+ const text = lastAssistant?.text || finalText;
359
+ if (!activeSessionId) {
360
+ throw buildPiError('pi session id missing from output', {
361
+ ok: false,
362
+ code: 0,
363
+ signal: null,
364
+ durationMs: Date.now() - startedAt,
365
+ kind: 'invalid-output',
366
+ errorMessage: 'pi session id missing from output',
367
+ });
368
+ }
369
+ settle(resolve, {
370
+ sessionId: activeSessionId,
371
+ cwd,
372
+ path: activeSessionFile,
373
+ text,
374
+ durationMs: Date.now() - startedAt,
375
+ });
376
+ } catch (err) {
377
+ settle(reject, buildPiError(err?.message || 'pi failed', {
378
+ ok: false,
379
+ code: null,
380
+ signal: null,
381
+ durationMs: Date.now() - startedAt,
382
+ kind: err?.info?.kind || 'gateway-error',
383
+ errorMessage: err?.info?.errorMessage || err?.message || stderr.trim() || 'pi failed',
384
+ }));
385
+ }
386
+ })();
387
+ });
388
+ }
389
+
390
+ async function readLocalMedia(path) {
391
+ const stats = await stat(path).catch(() => null);
392
+ if (!stats?.isFile() || stats.size === 0) return null;
393
+ const data = await readFile(path);
394
+ const mimeType = EXT_TO_MIME[extname(path).toLowerCase()] || 'application/octet-stream';
395
+ return { type: 'image', mimeType, data: data.toString('base64') };
396
+ }
397
+
398
+ async function downloadRemoteMedia(url, mime = null) {
399
+ const mediaDir = '/tmp/ticlawk/pi-media';
400
+ mkdirSync(mediaDir, { recursive: true });
401
+ const guessedExt = extname(String(url).split('?')[0] || '').toLowerCase() || '.jpg';
402
+ const localPath = join(mediaDir, `${randomUUID()}${guessedExt}`);
403
+ const res = await fetch(url);
404
+ const buf = Buffer.from(await res.arrayBuffer());
405
+ await writeFile(localPath, buf);
406
+ const mimeType = mime || EXT_TO_MIME[guessedExt] || res.headers.get('content-type') || 'application/octet-stream';
407
+ return { type: 'image', mimeType, data: buf.toString('base64') };
408
+ }
409
+
410
+ export async function buildPiImagesFromInbound(inbound) {
411
+ const images = [];
412
+ for (const item of inbound?.media || []) {
413
+ try {
414
+ if (item.kind === 'local_path' && item.value && existsSync(item.value)) {
415
+ const image = await readLocalMedia(item.value);
416
+ if (image) images.push(image);
417
+ } else if (item.kind === 'remote_url' && item.value) {
418
+ images.push(await downloadRemoteMedia(item.value, item.mime));
419
+ }
420
+ } catch {}
421
+ }
422
+ return images;
423
+ }
package/ticlawk.mjs ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ticlawk.mjs
5
+ *
6
+ * Connects local runtimes to a selected adapter.
7
+ *
8
+ * The daemon owns runtime lifecycle, local config, and the local control
9
+ * HTTP surface. Each adapter owns its own setup/auth semantics,
10
+ * inbound loop, outbound write path, and any backend-specific metadata.
11
+ */
12
+
13
+ // IMPORTANT: import ./src/core/config.mjs FIRST so dotenv runs before any
14
+ // other import reads process.env at module-eval time.
15
+ import {
16
+ AF_HOME,
17
+ AF_CONFIG_PATH,
18
+ AF_LOG_PATH,
19
+ AF_CRASH_LOG_PATH,
20
+ getConfiguredAdapter,
21
+ loadPersistentConfig,
22
+ persistConfig,
23
+ } from './src/core/config.mjs';
24
+ import { startLocalHttpServer } from './src/core/http.mjs';
25
+ import { installProcessDiagnostics } from './src/core/diagnostics.mjs';
26
+ import * as logger from './src/core/logger.mjs';
27
+ import { Bus } from './src/core/bus.mjs';
28
+ import { createAdapter } from './src/core/adapter-registry.mjs';
29
+ import { getBinding, listBindings, upsertBinding, deleteBinding, findBindingByTarget } from './src/core/bindings/store.mjs';
30
+ import { buildRuntimeContext, normalizeServiceType } from './src/core/runtime-registry.mjs';
31
+ import { belongsToRuntimeHost, getBindingRuntimeHostId, getHostId } from './src/core/host-id.mjs';
32
+ import { readPkgVersion } from './src/core/update.mjs';
33
+ import { buildImageMessageFromInbound } from './src/core/media/inbound.mjs';
34
+
35
+ // Re-export the config-owned paths so local tooling can inspect the
36
+ // ticlawk home/config/log locations without reaching into src/.
37
+ export {
38
+ AF_HOME,
39
+ AF_CONFIG_PATH,
40
+ AF_LOG_PATH,
41
+ AF_CRASH_LOG_PATH,
42
+ loadPersistentConfig,
43
+ persistConfig,
44
+ };
45
+
46
+ // ── Daemon ──────────────────────────────────────────────────────────────
47
+
48
+ const HTTP_PORT = process.env.FEED_RELAY_PORT || 8741;
49
+
50
+ // Single Bus instance for the daemon. Adapters and the inbound poller
51
+ // dispatch through it; the Bus enforces per-(runtime, channelId)
52
+ // serialization.
53
+ const bus = new Bus();
54
+
55
+ // ── Main ────────────────────────────────────────────────────────────────
56
+
57
+ let started = false;
58
+
59
+ function createResolveRuntimeBinding(runtimes) {
60
+ return async (payload) => {
61
+ const requested = normalizeServiceType(payload?.serviceType);
62
+ const runtime = runtimes[requested];
63
+ if (!runtime?.resolveBinding) {
64
+ throw new Error(`runtime does not support binding resolution: ${requested}`);
65
+ }
66
+ return runtime.resolveBinding(payload);
67
+ };
68
+ }
69
+
70
+ function createUpsertBindingWithSync(runtimes, adapter) {
71
+ return async (binding) => {
72
+ const nextBinding = await upsertBinding(binding);
73
+ const runtime = nextBinding?.runtime ? runtimes[nextBinding.runtime] : null;
74
+ if (typeof runtime?.onBindingUpdated === 'function') {
75
+ await runtime.onBindingUpdated(nextBinding, { adapter, logger });
76
+ }
77
+ if (typeof adapter.syncBinding === 'function') {
78
+ await adapter.syncBinding(nextBinding);
79
+ }
80
+ return nextBinding;
81
+ };
82
+ }
83
+
84
+ function createCacheBinding(runtimes, getAdapter) {
85
+ return async (binding) => {
86
+ const nextBinding = await upsertBinding(binding);
87
+ if (!belongsToRuntimeHost(nextBinding)) {
88
+ logger.debugError('core', 'binding.cache-host-mismatch', {
89
+ bindingId: nextBinding.id,
90
+ adapter: nextBinding.adapter,
91
+ hostId: getHostId(),
92
+ runtime_host_id: getBindingRuntimeHostId(nextBinding),
93
+ });
94
+ return nextBinding;
95
+ }
96
+ const runtime = nextBinding?.runtime ? runtimes[nextBinding.runtime] : null;
97
+ const adapter = getAdapter();
98
+ if (typeof runtime?.onBindingUpdated === 'function') {
99
+ await runtime.onBindingUpdated(nextBinding, { adapter, logger });
100
+ }
101
+ return nextBinding;
102
+ };
103
+ }
104
+
105
+ function createBaseRuntimeCtx(runtimes, cacheBinding, upsertBindingWithSync) {
106
+ return {
107
+ runtimes,
108
+ getBinding,
109
+ listBindings,
110
+ deleteBinding,
111
+ cacheBinding,
112
+ upsertBinding: upsertBindingWithSync,
113
+ buildImageMessageFromInbound,
114
+ logger,
115
+ };
116
+ }
117
+
118
+ function createAdapterContext(baseRuntimeCtx, resolveRuntimeBinding) {
119
+ return {
120
+ bus,
121
+ ...baseRuntimeCtx,
122
+ findBindingByTarget,
123
+ resolveRuntimeBinding,
124
+ };
125
+ }
126
+
127
+ function printBanner(adapter) {
128
+ const version = readPkgVersion() || 'unknown';
129
+ console.log('╔══════════════════════════════════════╗');
130
+ console.log(`║ ticlawk v${version.padEnd(15).slice(0, 15)}║`);
131
+ console.log('╚══════════════════════════════════════╝');
132
+ console.log(`[ticlawk] adapter: ${adapter.id}`);
133
+ if (adapter.id === 'ticlawk') {
134
+ console.log(`[ticlawk] API: ${process.env.TICLAWK_API_URL || 'https://ticlawk.com'}`);
135
+ }
136
+ }
137
+
138
+ function registerRuntimeHandlers(runtimeList, baseRuntimeCtx, adapter) {
139
+ for (const runtime of runtimeList) {
140
+ bus.registerRuntime(runtime.name, async (inbound) => {
141
+ return runtime.deliverTurn(inbound, {
142
+ adapter,
143
+ ...baseRuntimeCtx,
144
+ });
145
+ });
146
+ }
147
+ }
148
+
149
+ async function replayBindings(runtimes, adapter) {
150
+ const hostId = getHostId();
151
+ for (const binding of listBindings()) {
152
+ if (!belongsToRuntimeHost(binding, hostId)) {
153
+ logger.debugError('core', 'binding.replay-host-mismatch', {
154
+ bindingId: binding.id,
155
+ adapter: binding.adapter,
156
+ hostId,
157
+ runtime_host_id: getBindingRuntimeHostId(binding),
158
+ });
159
+ continue;
160
+ }
161
+ const runtime = binding.runtime ? runtimes[binding.runtime] : null;
162
+ if (typeof runtime?.onBindingUpdated !== 'function') continue;
163
+ try {
164
+ await runtime.onBindingUpdated(binding, { adapter, logger });
165
+ } catch (err) {
166
+ logger.debugError('startup', 'replayBindings.failed', {
167
+ runtime: binding.runtime,
168
+ bindingId: binding.id,
169
+ error: err?.message || String(err),
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ async function recoverAllRuntimes(runtimeList, adapter) {
176
+ for (const runtime of runtimeList) {
177
+ if (typeof runtime.recoverInFlight !== 'function') continue;
178
+ try {
179
+ await runtime.recoverInFlight({
180
+ adapter,
181
+ getBinding,
182
+ });
183
+ } catch (err) {
184
+ logger.debugError('startup', 'recoverInFlight.failed', {
185
+ runtime: runtime.name,
186
+ error: err?.message || String(err),
187
+ });
188
+ }
189
+ }
190
+ }
191
+
192
+ async function reconcileBindingsAfterRestart(runtimes, adapter) {
193
+ const hostId = getHostId();
194
+ for (const binding of listBindings({ adapter: adapter.id })) {
195
+ if (!belongsToRuntimeHost(binding, hostId)) {
196
+ logger.debugError('core', 'binding.reconcile-host-mismatch', {
197
+ bindingId: binding.id,
198
+ adapter: binding.adapter,
199
+ hostId,
200
+ runtime_host_id: getBindingRuntimeHostId(binding),
201
+ });
202
+ continue;
203
+ }
204
+ const runtime = binding.runtime ? runtimes[binding.runtime] : null;
205
+ if (typeof runtime?.reconcileAfterRestart !== 'function') continue;
206
+ try {
207
+ await runtime.reconcileAfterRestart(binding, {
208
+ adapter,
209
+ logger,
210
+ });
211
+ } catch (err) {
212
+ logger.debugError('startup', 'reconcileAfterRestart.failed', {
213
+ runtime: binding.runtime,
214
+ bindingId: binding.id,
215
+ error: err?.message || String(err),
216
+ });
217
+ }
218
+ }
219
+ }
220
+
221
+ export async function startTiclawk() {
222
+ if (started) return;
223
+ started = true;
224
+ installProcessDiagnostics();
225
+
226
+ const { runtimeList, runtimes } = await buildRuntimeContext();
227
+ const resolveRuntimeBinding = createResolveRuntimeBinding(runtimes);
228
+ const configuredAdapter = getConfiguredAdapter();
229
+ let adapter;
230
+ const cacheBinding = createCacheBinding(runtimes, () => adapter);
231
+ let baseRuntimeCtx;
232
+ let syncBinding = async (binding) => {
233
+ if (!adapter) {
234
+ throw new Error('adapter not initialized');
235
+ }
236
+ return upsertBinding(binding);
237
+ };
238
+ baseRuntimeCtx = createBaseRuntimeCtx(runtimes, cacheBinding, (binding) => syncBinding(binding));
239
+ adapter = createAdapter(
240
+ configuredAdapter,
241
+ createAdapterContext(baseRuntimeCtx, resolveRuntimeBinding)
242
+ );
243
+ syncBinding = createUpsertBindingWithSync(runtimes, adapter);
244
+ baseRuntimeCtx = createBaseRuntimeCtx(runtimes, cacheBinding, (binding) => syncBinding(binding));
245
+
246
+ printBanner(adapter);
247
+ if (typeof adapter.refreshBindings === 'function') {
248
+ await adapter.refreshBindings();
249
+ }
250
+ registerRuntimeHandlers(runtimeList, baseRuntimeCtx, adapter);
251
+ await replayBindings(runtimes, adapter);
252
+ startLocalHttpServer({ port: HTTP_PORT, adapter });
253
+ await recoverAllRuntimes(runtimeList, adapter);
254
+ await reconcileBindingsAfterRestart(runtimes, adapter);
255
+ await adapter.start();
256
+ }
257
+
258
+ if (import.meta.url === `file://${process.argv[1]}`) {
259
+ startTiclawk();
260
+ }