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,318 @@
1
+ /**
2
+ * opencode runtime entry point.
3
+ *
4
+ * Wraps the opencode CLI (https://opencode.ai). Exposes the runtime
5
+ * surface that adapters drive: resolve a binding, deliver a turn,
6
+ * reconcile after restart. Session lifecycle and the `opencode run`
7
+ * subprocess live in `./session.mjs`.
8
+ */
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { basename } from 'node:path';
12
+ import {
13
+ createOpenCodeSession,
14
+ getOpenCodeRuntimeHealth,
15
+ runOpenCodePrompt,
16
+ streamOpenCodePrompt,
17
+ findOpenCodeSessionById,
18
+ discoverOpenCodeSessions,
19
+ OPENCODE_DATA_DIR,
20
+ OPENCODE_MAX_AGE_MS,
21
+ requireOpenCodePath,
22
+ } from './session.mjs';
23
+ import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
24
+ import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
25
+ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
26
+ import {
27
+ shouldStreamRuntime,
28
+ createDeltaAggregator,
29
+ sendAdapterMessage,
30
+ sendResult,
31
+ reportSubprocessFailure,
32
+ terminalRuntimeFailure,
33
+ updateBindingRuntimeMeta,
34
+ } from '../../core/runtime-support.mjs';
35
+
36
+ export const openCodeRuntime = {
37
+ name: 'opencode',
38
+
39
+ async createSession({ cwd, text, opencodePath }) {
40
+ return createOpenCodeSession({ cwd, message: text, opencodePath });
41
+ },
42
+
43
+ runTurn({ sessionId, cwd, opencodePath }, text, opts = {}) {
44
+ return runOpenCodePrompt({
45
+ sessionId,
46
+ cwd,
47
+ message: text,
48
+ opencodePath,
49
+ files: opts.files,
50
+ timeoutMs: opts.timeoutMs,
51
+ });
52
+ },
53
+
54
+ runTurnStream({ sessionId, cwd, opencodePath }, text, opts = {}) {
55
+ return streamOpenCodePrompt({
56
+ sessionId,
57
+ cwd,
58
+ message: text,
59
+ opencodePath,
60
+ files: opts.files,
61
+ timeoutMs: opts.timeoutMs,
62
+ onEvent: opts.onEvent,
63
+ });
64
+ },
65
+
66
+ async listLocalSessions(cwd, opencodePath = null) {
67
+ return discoverOpenCodeSessions(cwd, { opencodePath });
68
+ },
69
+
70
+ async resolveBinding(payload) {
71
+ const requestedCwd = String(payload?.cwd || payload?.workdir || payload?.projectDir || '').trim();
72
+ const opencodePath = requireOpenCodePath(payload?.opencodePath || payload?.runtimePath);
73
+ const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version;
74
+
75
+ if (payload?.sessionId) {
76
+ // opencode `session list` is project-scoped, so we need a cwd to
77
+ // verify the session even exists. Surface this requirement clearly
78
+ // rather than silently failing during the next turn.
79
+ if (!requestedCwd) {
80
+ throw new Error('opencode requires --workdir along with --session-id');
81
+ }
82
+ if (!existsSync(requestedCwd)) {
83
+ throw new Error(`opencode cwd not found locally: ${requestedCwd}`);
84
+ }
85
+ const session = await findOpenCodeSessionById(payload.sessionId, requestedCwd, { opencodePath });
86
+ if (!session) {
87
+ throw new Error(`opencode session ${payload.sessionId} not found in ${requestedCwd}`);
88
+ }
89
+ return {
90
+ runtime: this.name,
91
+ displayName: payload.name || session.title || basename(requestedCwd) || 'opencode',
92
+ runtimeMeta: {
93
+ sessionId: session.sessionId,
94
+ workdir: requestedCwd,
95
+ cwd: requestedCwd,
96
+ runtimePath: opencodePath,
97
+ opencodePath,
98
+ opencodeVersion,
99
+ rotatePending: false,
100
+ },
101
+ };
102
+ }
103
+
104
+ if (!requestedCwd) {
105
+ throw new Error('cwd or sessionId is required for opencode binding');
106
+ }
107
+ if (!existsSync(requestedCwd)) {
108
+ throw new Error(`opencode cwd not found locally: ${requestedCwd}`);
109
+ }
110
+
111
+ return {
112
+ runtime: this.name,
113
+ displayName: payload.name || basename(requestedCwd) || 'opencode',
114
+ runtimeMeta: {
115
+ sessionId: null,
116
+ workdir: requestedCwd,
117
+ cwd: requestedCwd,
118
+ runtimePath: opencodePath,
119
+ opencodePath,
120
+ opencodeVersion,
121
+ rotatePending: false,
122
+ },
123
+ };
124
+ },
125
+
126
+ async health(meta = {}) {
127
+ return getOpenCodeRuntimeHealth(meta?.opencodePath || meta?.runtimePath);
128
+ },
129
+
130
+ async deliverTurn(inbound, ctx) {
131
+ const binding = ctx.getBinding(inbound.bindingId);
132
+ if (!binding) return false;
133
+ const adapter = ctx.adapter;
134
+ const meta = binding.runtimeMeta || {};
135
+ const runtimeOpenCodePath = meta.opencodePath || meta.runtimePath || null;
136
+
137
+ if (!meta.cwd || !existsSync(meta.cwd)) {
138
+ await sendAdapterMessage(adapter, binding, {
139
+ type: 'assistant',
140
+ text: `⚠️ opencode cwd not found: ${meta.cwd || '(missing)'}`,
141
+ media: [],
142
+ replyToMessageId: inbound.messageId || null,
143
+ });
144
+ return true;
145
+ }
146
+
147
+ // For image inbound, resolve the attached media to local file paths
148
+ // and forward them to opencode via `--file` (mirrors how Codex uses
149
+ // buildCodexInputFromInbound). If downloads fail and there's no
150
+ // caption to fall back on, stop early — sending an empty prompt
151
+ // would burn tokens for no reason.
152
+ let files = [];
153
+ let message = inbound.text || '';
154
+ if (inbound.action === 'image') {
155
+ const ocInput = await buildOpenCodeInputFromInbound(inbound, 'opencode');
156
+ files = ocInput.files;
157
+ const captionText = (inbound.text || '').trim();
158
+
159
+ if (files.length === 0 && !captionText) {
160
+ await sendAdapterMessage(adapter, binding, {
161
+ type: 'assistant',
162
+ text: '⚠️ Image attached, but I could not access the image data and there is no caption to fall back to. Try sending the image again, or include a text caption.',
163
+ media: [],
164
+ replyToMessageId: inbound.messageId || null,
165
+ });
166
+ return true;
167
+ }
168
+
169
+ if (files.length === 0 && captionText) {
170
+ // Downloads all failed; tell the user we're proceeding with the caption alone.
171
+ await sendAdapterMessage(adapter, binding, {
172
+ type: 'assistant',
173
+ text: '⚠️ Could not access the attached image data; acting on the caption text only.',
174
+ media: [],
175
+ replyToMessageId: inbound.messageId || null,
176
+ });
177
+ }
178
+
179
+ // If user sent images with no caption, give the model a minimal
180
+ // instruction so it has something to anchor on.
181
+ message = captionText || 'Please analyze the attached image(s).';
182
+ }
183
+ const shouldRotate = !meta.sessionId || meta.rotatePending;
184
+
185
+ const deltaAggregator = createDeltaAggregator({
186
+ flushDelta: async ({ text, sessionId, cwd }) => {
187
+ await emitWorkerEvent({
188
+ adapter,
189
+ binding,
190
+ agent: this.name,
191
+ sessionId: sessionId || meta.sessionId || binding.id,
192
+ cwd: cwd || meta.cwd,
193
+ replyToMessageId: inbound.messageId || null,
194
+ event: {
195
+ hook_event_name: 'worker.message.delta',
196
+ worker_event_name: 'worker.message.delta',
197
+ delta: text,
198
+ },
199
+ logger: ctx.logger,
200
+ });
201
+ },
202
+ });
203
+
204
+ try {
205
+ const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
206
+ const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
207
+ const result = shouldStreamRuntime(this.name, this)
208
+ ? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: meta.cwd, opencodePath }, message, {
209
+ files,
210
+ onEvent: async (event) => {
211
+ if (event?.type === 'turn.started') {
212
+ await emitWorkerEvent({
213
+ adapter,
214
+ binding,
215
+ agent: this.name,
216
+ sessionId: event.sessionId || meta.sessionId || binding.id,
217
+ cwd: meta.cwd,
218
+ replyToMessageId: inbound.messageId || null,
219
+ event: {
220
+ hook_event_name: 'worker.turn.start',
221
+ worker_event_name: 'worker.turn.start',
222
+ },
223
+ logger: ctx.logger,
224
+ });
225
+ } else if (event?.type === 'message.delta' && event.text) {
226
+ deltaAggregator.push(event.text, {
227
+ sessionId: event.sessionId || meta.sessionId || binding.id,
228
+ cwd: meta.cwd,
229
+ });
230
+ }
231
+ },
232
+ })
233
+ : await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: meta.cwd, opencodePath }, message, { files });
234
+
235
+ await deltaAggregator.flush();
236
+
237
+ const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
238
+ sessionId: result?.sessionId || meta.sessionId,
239
+ cwd: result?.cwd || meta.cwd,
240
+ runtimePath: opencodePath,
241
+ opencodePath,
242
+ opencodeVersion,
243
+ rotatePending: false,
244
+ lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
245
+ }, { status: 'connected' });
246
+ await sendResult(adapter, nextBinding, inbound, {
247
+ ...result,
248
+ media: normalizeOutboundMedia(result),
249
+ });
250
+ await emitWorkerEvent({
251
+ adapter,
252
+ binding: nextBinding,
253
+ agent: this.name,
254
+ sessionId: result?.sessionId || meta.sessionId || binding.id,
255
+ cwd: result?.cwd || meta.cwd,
256
+ replyToMessageId: inbound.messageId || null,
257
+ event: {
258
+ hook_event_name: 'Stop',
259
+ worker_event_name: 'worker.turn.complete',
260
+ },
261
+ logger: ctx.logger,
262
+ });
263
+ return true;
264
+ } catch (err) {
265
+ await deltaAggregator.flush().catch(() => {});
266
+ await emitWorkerEvent({
267
+ adapter,
268
+ binding,
269
+ agent: this.name,
270
+ sessionId: meta.sessionId || binding.id,
271
+ cwd: meta.cwd,
272
+ replyToMessageId: inbound.messageId || null,
273
+ event: {
274
+ hook_event_name: 'worker.turn.error',
275
+ worker_event_name: 'worker.turn.error',
276
+ error: err?.message || 'opencode failed',
277
+ },
278
+ logger: ctx.logger,
279
+ });
280
+ await reportSubprocessFailure({
281
+ adapter,
282
+ binding,
283
+ inbound,
284
+ runtimeName: 'opencode',
285
+ info: err?.info || {
286
+ ok: false,
287
+ kind: 'exit-error',
288
+ errorMessage: err?.message || 'opencode failed',
289
+ durationMs: 0,
290
+ },
291
+ });
292
+ return terminalRuntimeFailure(err?.message || 'opencode failed');
293
+ }
294
+ },
295
+
296
+ async reconcileAfterRestart(binding, ctx) {
297
+ const meta = binding.runtimeMeta || {};
298
+ await emitWorkerEvent({
299
+ adapter: ctx.adapter,
300
+ binding,
301
+ agent: this.name,
302
+ sessionId: meta.sessionId || binding.id,
303
+ cwd: meta.cwd || '',
304
+ event: {
305
+ hook_event_name: 'Stop',
306
+ worker_event_name: 'worker.turn.complete',
307
+ reason: 'connector.restart.reconcile',
308
+ },
309
+ logger: ctx.logger,
310
+ });
311
+ return 1;
312
+ },
313
+
314
+ dataDir: OPENCODE_DATA_DIR,
315
+ maxAgeMs: OPENCODE_MAX_AGE_MS,
316
+ };
317
+
318
+ export default openCodeRuntime;