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,178 @@
1
+ /**
2
+ * Self-update.
3
+ *
4
+ * Updates the npm package, refreshes the user service, and prints the new
5
+ * version. Default flow is interactive; `-y` skips the prompt;
6
+ * non-interactive without `-y` exits 1.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
10
+ import { spawn, spawnSync } from 'node:child_process';
11
+ import { dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { AF_HOME } from './config.mjs';
14
+
15
+ const PACKAGE_NAME = 'ticlawk';
16
+
17
+ export function readPkgVersion() {
18
+ try {
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8'));
21
+ return pkg.version || '';
22
+ } catch {
23
+ return '';
24
+ }
25
+ }
26
+
27
+ function commandExists(command) {
28
+ return spawnSync('command', ['-v', command], { shell: true, stdio: 'ignore' }).status === 0;
29
+ }
30
+
31
+ export function isManagedInstall() {
32
+ if (process.platform === 'linux' && commandExists('systemctl')) {
33
+ return ['ticlawk', 'agent-freeway'].some((service) => {
34
+ const enabled = spawnSync('systemctl', ['--user', 'is-enabled', '--quiet', service], { stdio: 'ignore' });
35
+ const active = spawnSync('systemctl', ['--user', 'is-active', '--quiet', service], { stdio: 'ignore' });
36
+ return enabled.status === 0 || active.status === 0;
37
+ });
38
+ }
39
+
40
+ if (process.platform === 'darwin' && commandExists('launchctl')) {
41
+ const uid = String(process.getuid?.() || '');
42
+ return ['ticlawk', 'agent-freeway', 'com.ticlawk.agent-freeway'].some((label) => {
43
+ const service = `gui/${uid}/${label}`;
44
+ return spawnSync('launchctl', ['print', service], { stdio: 'ignore' }).status === 0;
45
+ });
46
+ }
47
+
48
+ return false;
49
+ }
50
+
51
+ function shellQuote(value) {
52
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
53
+ }
54
+
55
+ export function startDetachedSelfUpdate({ requiredVersion = '', currentVersion = '', reason = 'update_required' } = {}) {
56
+ if (!isManagedInstall()) {
57
+ return { started: false, reason: 'unmanaged_install' };
58
+ }
59
+ if (!requiredVersion) {
60
+ return { started: false, reason: 'missing_required_version' };
61
+ }
62
+
63
+ mkdirSync(AF_HOME, { recursive: true });
64
+ const updateLogPath = join(AF_HOME, 'ticlawk-update.log');
65
+ const currentCli = getCurrentCliCommand();
66
+ const installerVersion = requiredVersion;
67
+ const script = [
68
+ 'sleep 1',
69
+ `printf '%s\\n' ${shellQuote(`[${new Date().toISOString()}] update required: current=${currentVersion || 'unknown'} required=${requiredVersion || 'unknown'} installer_version=${installerVersion || 'default'} reason=${reason}`)} >> ${shellQuote(updateLogPath)}`,
70
+ `${currentCli} update -y >> ${shellQuote(updateLogPath)} 2>&1`,
71
+ ].join('; ');
72
+
73
+ const child = spawn('sh', ['-lc', script], {
74
+ detached: true,
75
+ stdio: 'ignore',
76
+ env: {
77
+ ...process.env,
78
+ TICLAWK_UPDATE_REASON: reason,
79
+ TICLAWK_REQUIRED_VERSION: requiredVersion,
80
+ TICLAWK_CURRENT_VERSION: currentVersion,
81
+ ...(installerVersion ? { TICLAWK_VERSION: installerVersion } : {}),
82
+ AGENT_FREEWAY_UPDATE_REASON: reason,
83
+ AGENT_FREEWAY_REQUIRED_VERSION: requiredVersion,
84
+ AGENT_FREEWAY_CURRENT_VERSION: currentVersion,
85
+ ...(installerVersion ? { AGENT_FREEWAY_VERSION: installerVersion } : {}),
86
+ },
87
+ });
88
+ child.unref();
89
+ return { started: true, pid: child.pid, logPath: updateLogPath };
90
+ }
91
+
92
+ function getCurrentCliCommand() {
93
+ const scriptPath = process.argv[1] || '';
94
+ if (scriptPath && existsSync(scriptPath)) {
95
+ return `${shellQuote(process.execPath)} ${shellQuote(realpathSync(scriptPath))}`;
96
+ }
97
+ return shellQuote('ticlawk');
98
+ }
99
+
100
+ function getNpmGlobalTiclawkBin() {
101
+ const prefixResult = spawnSync('npm', ['prefix', '-g'], { encoding: 'utf8' });
102
+ const prefix = String(prefixResult.stdout || '').trim();
103
+ if (!prefix) return '';
104
+ if (process.platform === 'win32') return join(prefix, 'ticlawk.cmd');
105
+ return join(prefix, 'bin', 'ticlawk');
106
+ }
107
+
108
+ function runInstalledTiclawk(args, options = {}) {
109
+ const npmBin = getNpmGlobalTiclawkBin();
110
+ if (npmBin && existsSync(npmBin)) {
111
+ return spawnSync(npmBin, args, options);
112
+ }
113
+ return spawnSync('ticlawk', args, options);
114
+ }
115
+
116
+ function promptYesNo(question) {
117
+ if (!process.stdin.isTTY) {
118
+ return Promise.resolve(false);
119
+ }
120
+ process.stdout.write(`${question} `);
121
+ return new Promise((resolveAnswer) => {
122
+ process.stdin.setEncoding('utf8');
123
+ const onData = (chunk) => {
124
+ process.stdin.removeListener('data', onData);
125
+ process.stdin.pause();
126
+ const answer = String(chunk).trim().toLowerCase();
127
+ resolveAnswer(answer === '' || answer === 'y' || answer === 'yes');
128
+ };
129
+ process.stdin.resume();
130
+ process.stdin.on('data', onData);
131
+ });
132
+ }
133
+
134
+ export async function runSelfUpdate(skipPrompt) {
135
+ const currentVersion = readPkgVersion();
136
+ const targetVersion = process.env.TICLAWK_VERSION
137
+ || process.env.TICLAWK_REQUIRED_VERSION
138
+ || process.env.AGENT_FREEWAY_VERSION
139
+ || process.env.AGENT_FREEWAY_REQUIRED_VERSION
140
+ || 'latest';
141
+ const packageSpec = `${PACKAGE_NAME}@${targetVersion}`;
142
+ console.log(`current version: ${currentVersion || '(unknown)'}`);
143
+ console.log(`package: ${packageSpec}`);
144
+
145
+ if (!skipPrompt) {
146
+ if (!process.stdin.isTTY) {
147
+ console.error('refusing to update non-interactively without -y');
148
+ process.exit(1);
149
+ }
150
+ const ok = await promptYesNo('Update ticlawk with npm? [Y/n]');
151
+ if (!ok) {
152
+ console.log('aborted');
153
+ return;
154
+ }
155
+ }
156
+
157
+ const installResult = spawnSync('npm', ['install', '--global', '--no-audit', '--no-fund', packageSpec], {
158
+ stdio: 'inherit',
159
+ });
160
+ if (installResult.status !== 0) {
161
+ console.error('npm install failed');
162
+ process.exit(installResult.status || 1);
163
+ }
164
+
165
+ const readiness = runInstalledTiclawk(['setup-readiness', '--ensure-daemon'], { stdio: 'inherit' });
166
+ if (readiness.status !== 0) {
167
+ console.error('setup readiness failed');
168
+ process.exit(readiness.status || 1);
169
+ }
170
+
171
+ const newVer = runInstalledTiclawk(['version'], { encoding: 'utf8' });
172
+ const newVersion = (newVer.stdout || '').trim();
173
+ if (newVersion) {
174
+ console.log(`installed version: ${newVersion}`);
175
+ } else {
176
+ console.log('update complete');
177
+ }
178
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Claude Code runtime entry point.
3
+ *
4
+ * Exposes a narrow surface an adapter can use to drive a Claude Code
5
+ * session without knowing anything about the `claude` binary's calling
6
+ * convention. The adapter decides rotation and message routing; the
7
+ * runtime owns the binary-level details.
8
+ */
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { basename } from 'node:path';
12
+ import {
13
+ createCCSession,
14
+ getClaudeCodeRuntimeHealth,
15
+ runCCPrompt,
16
+ streamCCPrompt,
17
+ requireClaudePath,
18
+ prettyProjectName,
19
+ getClaudeTranscriptPath,
20
+ CC_PROJECTS_DIR,
21
+ CC_MAX_AGE_MS,
22
+ } from './session.mjs';
23
+ import { discoverSessions } from './transcripts.mjs';
24
+ import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
25
+ import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
26
+ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
27
+ import {
28
+ shouldStreamRuntime,
29
+ sendAdapterMessage,
30
+ sendResult,
31
+ reportSubprocessFailure,
32
+ terminalRuntimeFailure,
33
+ updateBindingRuntimeMeta,
34
+ } from '../../core/runtime-support.mjs';
35
+
36
+ export const claudeCodeRuntime = {
37
+ name: 'claude_code',
38
+
39
+ // Start a new Claude session without blocking on local transcript
40
+ // discovery. The live turn path trusts stdout/session_id; transcript
41
+ // indexing is a separate concern.
42
+ async createSession({ projectDir, text, claudePath }) {
43
+ return createCCSession({ projectDir, message: text, claudePath });
44
+ },
45
+
46
+ // Run a Claude turn and wait for the final result on stdout. This is
47
+ // the worker-first path used by the adapter for direct reply delivery.
48
+ runTurn({ sessionId, projectDir, claudePath }, text, opts = {}) {
49
+ return runCCPrompt({ sessionId, projectDir, message: text, claudePath, timeoutMs: opts.timeoutMs });
50
+ },
51
+
52
+ runTurnStream({ sessionId, projectDir, claudePath }, text, opts = {}) {
53
+ return streamCCPrompt({
54
+ sessionId,
55
+ projectDir,
56
+ message: text,
57
+ claudePath,
58
+ timeoutMs: opts.timeoutMs,
59
+ onEvent: opts.onEvent,
60
+ });
61
+ },
62
+
63
+ // Enumerate local CC sessions from disk. Used by adapter setup / bind
64
+ // flows to verify a sessionId exists locally before creating or
65
+ // updating a binding. Each session carries enough metadata for the
66
+ // adapter to round-trip it back into backend-specific channel meta.
67
+ async listLocalSessions() {
68
+ return discoverSessions(CC_PROJECTS_DIR);
69
+ },
70
+
71
+ async resolveBinding(payload) {
72
+ const requestedProjectDir = String(payload?.projectDir || payload?.workdir || payload?.cwd || '').trim();
73
+ const claudePath = requireClaudePath(payload?.claudePath || payload?.runtimePath);
74
+ const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version;
75
+
76
+ if (payload?.sessionId) {
77
+ const sessions = await this.listLocalSessions();
78
+ const session = sessions.find((entry) => entry.sessionId === payload.sessionId);
79
+ if (!session) {
80
+ throw new Error('session not found locally');
81
+ }
82
+ return {
83
+ runtime: this.name,
84
+ displayName: payload?.name || this.formatDisplayName(session) || basename(session.projectDir || '') || 'Claude Code',
85
+ runtimeMeta: {
86
+ sessionId: session.sessionId,
87
+ project: session.project,
88
+ workdir: session.projectDir,
89
+ projectDir: session.projectDir,
90
+ path: session.path,
91
+ runtimePath: claudePath,
92
+ claudePath,
93
+ claudeVersion,
94
+ rotatePending: false,
95
+ },
96
+ };
97
+ }
98
+
99
+ if (!requestedProjectDir) {
100
+ throw new Error('projectDir or sessionId is required for claude_code binding');
101
+ }
102
+ if (!existsSync(requestedProjectDir)) {
103
+ throw new Error(`project dir not found locally: ${requestedProjectDir}`);
104
+ }
105
+
106
+ return {
107
+ runtime: this.name,
108
+ displayName: payload?.name || basename(requestedProjectDir) || 'Claude Code',
109
+ runtimeMeta: {
110
+ sessionId: null,
111
+ project: basename(requestedProjectDir) || '',
112
+ workdir: requestedProjectDir,
113
+ projectDir: requestedProjectDir,
114
+ path: null,
115
+ runtimePath: claudePath,
116
+ claudePath,
117
+ claudeVersion,
118
+ rotatePending: false,
119
+ },
120
+ };
121
+ },
122
+
123
+ async health(meta = {}) {
124
+ return getClaudeCodeRuntimeHealth(meta?.claudePath || meta?.runtimePath);
125
+ },
126
+
127
+ async deliverTurn(inbound, ctx) {
128
+ const binding = ctx.getBinding(inbound.bindingId);
129
+ if (!binding) return false;
130
+ const adapter = ctx.adapter;
131
+ const meta = binding.runtimeMeta || {};
132
+ const projectDir = meta.projectDir;
133
+ const sessionId = meta.sessionId || binding.id;
134
+ const runtimeClaudePath = meta.claudePath || meta.runtimePath || null;
135
+
136
+ if (!projectDir || !existsSync(projectDir)) {
137
+ await sendAdapterMessage(adapter, binding, {
138
+ type: 'assistant',
139
+ text: `⚠️ Claude Code project dir not found: ${projectDir || '(missing)'}`,
140
+ media: [],
141
+ replyToMessageId: inbound.messageId || null,
142
+ });
143
+ return true;
144
+ }
145
+
146
+ const message = inbound.action === 'image'
147
+ ? await buildImageMessageFromInbound(inbound, 'claude-code')
148
+ : inbound.text;
149
+
150
+ const shouldRotate = !meta.sessionId || meta.rotatePending;
151
+ if (shouldRotate) {
152
+ await emitWorkerEvent({
153
+ adapter,
154
+ binding,
155
+ agent: this.name,
156
+ sessionId: sessionId || binding.id,
157
+ cwd: projectDir,
158
+ replyToMessageId: inbound.messageId || null,
159
+ event: {
160
+ hook_event_name: 'worker.turn.start',
161
+ worker_event_name: 'worker.turn.start',
162
+ },
163
+ logger: ctx.logger,
164
+ });
165
+ try {
166
+ const claudePath = requireClaudePath(runtimeClaudePath);
167
+ const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
168
+ const created = await this.createSession({ projectDir, text: message, claudePath });
169
+ const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
170
+ sessionId: created.sessionId,
171
+ projectDir,
172
+ path: null,
173
+ runtimePath: claudePath,
174
+ claudePath,
175
+ claudeVersion,
176
+ rotatePending: false,
177
+ lastRotatedAt: new Date().toISOString(),
178
+ }, { status: 'connected' });
179
+ if (created.resultText && created.resultText.trim()) {
180
+ await sendAdapterMessage(adapter, nextBinding, {
181
+ type: 'assistant',
182
+ text: created.resultText,
183
+ media: [],
184
+ replyToMessageId: inbound.messageId || null,
185
+ });
186
+ }
187
+ await emitWorkerEvent({
188
+ adapter,
189
+ binding: nextBinding,
190
+ agent: this.name,
191
+ sessionId: created.sessionId,
192
+ cwd: projectDir,
193
+ replyToMessageId: inbound.messageId || null,
194
+ event: {
195
+ hook_event_name: 'Stop',
196
+ worker_event_name: 'worker.turn.complete',
197
+ },
198
+ logger: ctx.logger,
199
+ });
200
+ return true;
201
+ } catch (err) {
202
+ await emitWorkerEvent({
203
+ adapter,
204
+ binding,
205
+ agent: this.name,
206
+ sessionId: sessionId || binding.id,
207
+ cwd: projectDir,
208
+ replyToMessageId: inbound.messageId || null,
209
+ event: {
210
+ hook_event_name: 'worker.turn.error',
211
+ worker_event_name: 'worker.turn.error',
212
+ error: err?.message || 'Claude Code failed',
213
+ },
214
+ logger: ctx.logger,
215
+ });
216
+ await reportSubprocessFailure({
217
+ adapter,
218
+ binding,
219
+ inbound,
220
+ runtimeName: 'Claude Code',
221
+ info: err?.info || {
222
+ ok: false,
223
+ kind: 'exit-error',
224
+ errorMessage: err?.message || 'Claude Code failed',
225
+ durationMs: 0,
226
+ },
227
+ });
228
+ return terminalRuntimeFailure(err?.message || 'Claude Code failed');
229
+ }
230
+ }
231
+
232
+ try {
233
+ const claudePath = requireClaudePath(runtimeClaudePath);
234
+ const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
235
+ const result = shouldStreamRuntime(this.name, this)
236
+ ? await this.runTurnStream({ sessionId, projectDir, claudePath }, message, {
237
+ onEvent: async (event) => {
238
+ if (event?.type === 'turn.started') {
239
+ await emitWorkerEvent({
240
+ adapter,
241
+ binding,
242
+ agent: this.name,
243
+ sessionId: event.sessionId || sessionId,
244
+ cwd: projectDir,
245
+ replyToMessageId: inbound.messageId || null,
246
+ event: {
247
+ hook_event_name: 'worker.turn.start',
248
+ worker_event_name: 'worker.turn.start',
249
+ },
250
+ logger: ctx.logger,
251
+ });
252
+ } else if (event?.type === 'message.delta' && event.text) {
253
+ await emitWorkerEvent({
254
+ adapter,
255
+ binding,
256
+ agent: this.name,
257
+ sessionId: event.sessionId || sessionId,
258
+ cwd: projectDir,
259
+ replyToMessageId: inbound.messageId || null,
260
+ event: {
261
+ hook_event_name: 'worker.message.delta',
262
+ worker_event_name: 'worker.message.delta',
263
+ delta: event.text,
264
+ },
265
+ logger: ctx.logger,
266
+ });
267
+ }
268
+ },
269
+ })
270
+ : await this.runTurn({ sessionId, projectDir, claudePath }, message);
271
+ const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
272
+ sessionId: result?.sessionId || meta.sessionId,
273
+ runtimePath: claudePath,
274
+ claudePath,
275
+ claudeVersion,
276
+ }, { status: 'connected' });
277
+ await sendResult(adapter, nextBinding, inbound, {
278
+ ...result,
279
+ media: normalizeOutboundMedia(result),
280
+ });
281
+ await emitWorkerEvent({
282
+ adapter,
283
+ binding: nextBinding,
284
+ agent: this.name,
285
+ sessionId: result?.sessionId || sessionId,
286
+ cwd: projectDir,
287
+ replyToMessageId: inbound.messageId || null,
288
+ event: {
289
+ hook_event_name: 'Stop',
290
+ worker_event_name: 'worker.turn.complete',
291
+ },
292
+ logger: ctx.logger,
293
+ });
294
+ return true;
295
+ } catch (err) {
296
+ await emitWorkerEvent({
297
+ adapter,
298
+ binding,
299
+ agent: this.name,
300
+ sessionId,
301
+ cwd: projectDir,
302
+ replyToMessageId: inbound.messageId || null,
303
+ event: {
304
+ hook_event_name: 'worker.turn.error',
305
+ worker_event_name: 'worker.turn.error',
306
+ error: err?.message || 'Claude Code failed',
307
+ },
308
+ logger: ctx.logger,
309
+ });
310
+ await reportSubprocessFailure({
311
+ adapter,
312
+ binding,
313
+ inbound,
314
+ runtimeName: 'Claude Code',
315
+ info: err?.info || {
316
+ ok: false,
317
+ kind: 'exit-error',
318
+ errorMessage: err?.message || 'Claude Code failed',
319
+ durationMs: 0,
320
+ },
321
+ });
322
+ return terminalRuntimeFailure(err?.message || 'Claude Code failed');
323
+ }
324
+ },
325
+
326
+ async reconcileAfterRestart(binding, ctx) {
327
+ const meta = binding.runtimeMeta || {};
328
+ await emitWorkerEvent({
329
+ adapter: ctx.adapter,
330
+ binding,
331
+ agent: this.name,
332
+ sessionId: meta.sessionId || binding.id,
333
+ cwd: meta.projectDir || '',
334
+ event: {
335
+ hook_event_name: 'Stop',
336
+ worker_event_name: 'worker.turn.complete',
337
+ reason: 'connector.restart.reconcile',
338
+ },
339
+ logger: ctx.logger,
340
+ });
341
+ return 1;
342
+ },
343
+
344
+ // Friendly display name for a CC session (encoded project path →
345
+ // last meaningful segment). Used when materializing a binding.
346
+ formatDisplayName(session) {
347
+ return prettyProjectName(session.project || '');
348
+ },
349
+
350
+ // Compute the canonical transcript path for a (projectDir, sessionId)
351
+ // pair.
352
+ getTranscriptPath: getClaudeTranscriptPath,
353
+
354
+ // Adapter scan helper: friendly name from an encoded project string.
355
+ prettyProjectName,
356
+
357
+ // Paths + thresholds the adapter needs to discover sessions and
358
+ // bound sync ranges.
359
+ projectsDir: CC_PROJECTS_DIR,
360
+ maxAgeMs: CC_MAX_AGE_MS,
361
+ };
362
+
363
+ export default claudeCodeRuntime;