ticlawk 0.1.12-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +426 -0
- package/agent-freeway.mjs +2 -0
- package/assets/ticlawk-concept.svg +137 -0
- package/bin/agent-freeway.mjs +4 -0
- package/bin/ticlawk.mjs +594 -0
- package/cc-watcher.mjs +3 -0
- package/package.json +72 -0
- package/scripts/postinstall.mjs +61 -0
- package/src/adapters/telegram/index.mjs +359 -0
- package/src/adapters/ticlawk/api.mjs +360 -0
- package/src/adapters/ticlawk/cards.mjs +149 -0
- package/src/adapters/ticlawk/credentials.mjs +25 -0
- package/src/adapters/ticlawk/index.mjs +1229 -0
- package/src/adapters/ticlawk/wake-client.mjs +204 -0
- package/src/core/adapter-registry.mjs +50 -0
- package/src/core/argv.mjs +38 -0
- package/src/core/bindings/store.mjs +81 -0
- package/src/core/bus.mjs +91 -0
- package/src/core/config.mjs +203 -0
- package/src/core/daemon-install.mjs +246 -0
- package/src/core/diagnostics.mjs +79 -0
- package/src/core/events/worker-events.mjs +80 -0
- package/src/core/executables.mjs +106 -0
- package/src/core/host-id.mjs +48 -0
- package/src/core/http.mjs +65 -0
- package/src/core/logger.mjs +34 -0
- package/src/core/media/inbound.mjs +127 -0
- package/src/core/media/outbound.mjs +163 -0
- package/src/core/profiles.mjs +173 -0
- package/src/core/runtime-contract.mjs +68 -0
- package/src/core/runtime-env.mjs +9 -0
- package/src/core/runtime-registry.mjs +93 -0
- package/src/core/runtime-support.mjs +197 -0
- package/src/core/setup-readiness.mjs +86 -0
- package/src/core/store/json-file-store.mjs +47 -0
- package/src/core/ticlawk-control.mjs +92 -0
- package/src/core/uninstall.mjs +142 -0
- package/src/core/update-state.mjs +62 -0
- package/src/core/update.mjs +178 -0
- package/src/runtimes/claude-code/index.mjs +363 -0
- package/src/runtimes/claude-code/session.mjs +388 -0
- package/src/runtimes/claude-code/transcripts.mjs +206 -0
- package/src/runtimes/codex/index.mjs +306 -0
- package/src/runtimes/codex/session.mjs +750 -0
- package/src/runtimes/openclaw/gateway.mjs +269 -0
- package/src/runtimes/openclaw/identity.mjs +34 -0
- package/src/runtimes/openclaw/index.mjs +228 -0
- package/src/runtimes/openclaw/inflight.mjs +46 -0
- package/src/runtimes/openclaw/target.mjs +57 -0
- package/src/runtimes/opencode/index.mjs +318 -0
- package/src/runtimes/opencode/session.mjs +413 -0
- package/src/runtimes/pi/index.mjs +287 -0
- package/src/runtimes/pi/session.mjs +423 -0
- package/ticlawk.mjs +260 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const EXT_TO_MIME = {
|
|
6
|
+
'.jpg': 'image/jpeg',
|
|
7
|
+
'.jpeg': 'image/jpeg',
|
|
8
|
+
'.png': 'image/png',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.webp': 'image/webp',
|
|
11
|
+
'.bmp': 'image/bmp',
|
|
12
|
+
'.tiff': 'image/tiff',
|
|
13
|
+
'.tif': 'image/tiff',
|
|
14
|
+
'.heic': 'image/heic',
|
|
15
|
+
'.heif': 'image/heif',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function resolveMediaDir(subdir, options = {}) {
|
|
19
|
+
const baseDir = options.baseDir || '/tmp/ticlawk';
|
|
20
|
+
return `${baseDir}/${subdir}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function downloadRemoteMedia(items, subdir, options = {}) {
|
|
24
|
+
const mediaDir = resolveMediaDir(subdir, options);
|
|
25
|
+
mkdirSync(mediaDir, { recursive: true });
|
|
26
|
+
const successful = [];
|
|
27
|
+
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
if (item.kind === 'local_path' && item.value && existsSync(item.value)) {
|
|
30
|
+
const label = `Image #${successful.length + 1}`;
|
|
31
|
+
successful.push({
|
|
32
|
+
path: item.value,
|
|
33
|
+
mime: item.mime || EXT_TO_MIME[extname(item.value).toLowerCase()] || 'application/octet-stream',
|
|
34
|
+
source: item.value,
|
|
35
|
+
label,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (item.kind !== 'remote_url' || !item.value) continue;
|
|
40
|
+
const guessedExt = extname(item.value.split('?')[0] || '').toLowerCase() || '.jpg';
|
|
41
|
+
const label = `Image #${successful.length + 1}`;
|
|
42
|
+
const localPath = `${mediaDir}/image-${successful.length + 1}-${randomUUID()}${guessedExt}`;
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(item.value);
|
|
45
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
46
|
+
writeFileSync(localPath, buf);
|
|
47
|
+
successful.push({
|
|
48
|
+
path: localPath,
|
|
49
|
+
mime: item.mime || EXT_TO_MIME[guessedExt] || 'application/octet-stream',
|
|
50
|
+
source: item.value,
|
|
51
|
+
label,
|
|
52
|
+
});
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return successful;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function buildImageMessageFromInbound(inbound, subdir = 'inbound-media', options = {}) {
|
|
60
|
+
const successful = await downloadRemoteMedia(inbound.media || [], subdir, options);
|
|
61
|
+
const userText = (inbound.text || '').trim();
|
|
62
|
+
if (successful.length === 0) {
|
|
63
|
+
const fallbackList = (inbound.media || [])
|
|
64
|
+
.map((item) => `[media attached: ${item.value}]`)
|
|
65
|
+
.join('\n');
|
|
66
|
+
return [userText, fallbackList].filter(Boolean).join('\n\n').trim() || '(image attached)';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const lines = successful.map((entry, index) => {
|
|
70
|
+
const prefix = successful.length > 1
|
|
71
|
+
? `[media attached ${index + 1}/${successful.length}: `
|
|
72
|
+
: '[media attached: ';
|
|
73
|
+
const mimePart = entry.mime ? ` (${entry.mime})` : '';
|
|
74
|
+
const sourcePart = entry.source ? ` | ${entry.source}` : '';
|
|
75
|
+
return `${prefix}${entry.path}${mimePart}${sourcePart}]`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return [userText, ...lines].filter(Boolean).join('\n\n').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildLocalImagePathSummary(entries) {
|
|
82
|
+
if (!entries.length) return '';
|
|
83
|
+
return [
|
|
84
|
+
'Attached images:',
|
|
85
|
+
...entries.map((entry, index) => `${entry.label || `Image #${index + 1}`} local path: ${entry.path}`),
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function buildOpenCodeInputFromInbound(inbound, subdir = 'opencode-media', options = {}) {
|
|
90
|
+
const successful = await downloadRemoteMedia(inbound.media || [], subdir, options);
|
|
91
|
+
const userText = (inbound.text || '').trim();
|
|
92
|
+
if (successful.length === 0) {
|
|
93
|
+
// No usable image bytes — caller decides whether to error or fall through to text-only
|
|
94
|
+
return {
|
|
95
|
+
text: userText,
|
|
96
|
+
files: [],
|
|
97
|
+
downloaded: 0,
|
|
98
|
+
attempted: (inbound.media || []).length,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
text: userText,
|
|
103
|
+
files: successful.map((entry) => entry.path),
|
|
104
|
+
downloaded: successful.length,
|
|
105
|
+
attempted: (inbound.media || []).length,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function buildCodexInputFromInbound(inbound, subdir = 'codex-media', options = {}) {
|
|
110
|
+
const successful = await downloadRemoteMedia(inbound.media || [], subdir, options);
|
|
111
|
+
if (successful.length === 0) {
|
|
112
|
+
const fallbackList = (inbound.media || []).map((item) => `[media attached: ${item.value}]`).join('\n');
|
|
113
|
+
const text = [(inbound.text || '').trim(), fallbackList].filter(Boolean).join('\n\n').trim() || '(image attached)';
|
|
114
|
+
return [{ type: 'text', text }];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const input = [];
|
|
118
|
+
const text = [
|
|
119
|
+
(inbound.text || '').trim(),
|
|
120
|
+
buildLocalImagePathSummary(successful),
|
|
121
|
+
].filter(Boolean).join('\n\n');
|
|
122
|
+
input.push({ type: 'text', text });
|
|
123
|
+
for (const entry of successful) {
|
|
124
|
+
input.push({ type: 'localImage', path: entry.path });
|
|
125
|
+
}
|
|
126
|
+
return input;
|
|
127
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export const MIME_MAP = {
|
|
6
|
+
'.png': 'image/png',
|
|
7
|
+
'.jpg': 'image/jpeg',
|
|
8
|
+
'.jpeg': 'image/jpeg',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.webp': 'image/webp',
|
|
11
|
+
'.svg': 'image/svg+xml',
|
|
12
|
+
'.mp4': 'video/mp4',
|
|
13
|
+
'.webm': 'video/webm',
|
|
14
|
+
'.pdf': 'application/pdf',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function mediaExtensionPattern() {
|
|
18
|
+
return Object.keys(MIME_MAP).map((entry) => entry.replace('.', '\\.')).join('|');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stripUrlSuffix(value) {
|
|
22
|
+
return String(value || '').replace(/[?#].*$/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function trimCandidate(value) {
|
|
26
|
+
return String(value || '')
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/^<|>$/g, '')
|
|
29
|
+
.replace(/^["']|["']$/g, '')
|
|
30
|
+
.replace(/\\\//g, '/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasKnownMediaExtension(value) {
|
|
34
|
+
return Boolean(MIME_MAP[extname(stripUrlSuffix(value)).toLowerCase()]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeMediaPath(value) {
|
|
38
|
+
let candidate = trimCandidate(value);
|
|
39
|
+
if (!candidate) return null;
|
|
40
|
+
|
|
41
|
+
candidate = candidate.replace(/^about:/i, '');
|
|
42
|
+
|
|
43
|
+
if (/^file:/i.test(candidate)) {
|
|
44
|
+
try {
|
|
45
|
+
candidate = fileURLToPath(candidate);
|
|
46
|
+
} catch {
|
|
47
|
+
candidate = candidate.replace(/^file:(?:\/\/)?/i, '');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
candidate = stripUrlSuffix(candidate);
|
|
52
|
+
|
|
53
|
+
if (candidate.startsWith('~')) {
|
|
54
|
+
candidate = candidate.replace(/^~/, homedir());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (candidate.startsWith('/')) {
|
|
58
|
+
candidate = candidate.replace(/^\/{2,}/, '/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!candidate.startsWith('/')) return null;
|
|
62
|
+
if (!hasKnownMediaExtension(candidate)) return null;
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addMediaCandidate(paths, value) {
|
|
67
|
+
const normalized = normalizeMediaPath(value);
|
|
68
|
+
if (normalized) paths.add(normalized);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function extractMediaPaths(text) {
|
|
72
|
+
const extPattern = mediaExtensionPattern();
|
|
73
|
+
const paths = new Set();
|
|
74
|
+
const candidates = [];
|
|
75
|
+
|
|
76
|
+
const body = String(text || '');
|
|
77
|
+
for (const match of body.matchAll(/<img[^>]+src=["']([^"']+)["'][^>]*>/gi)) {
|
|
78
|
+
candidates.push({ index: match.index || 0, value: match[1] });
|
|
79
|
+
}
|
|
80
|
+
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*<([^>\n]+)>/gi)) {
|
|
81
|
+
candidates.push({ index: match.index || 0, value: match[1] });
|
|
82
|
+
}
|
|
83
|
+
for (const match of body.matchAll(/!?\[[^\]]*\]\(\s*([^\s)>]+)>?/gi)) {
|
|
84
|
+
candidates.push({ index: match.index || 0, value: match[1] });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const re = new RegExp(`((?:file:(?://)?|about:(?://)?)?(?:~|/{1,3})[^\\s"'<>)]*?(?:${extPattern})(?:[?#][^\\s"'<>)]*)?)`, 'gi');
|
|
88
|
+
for (const match of body.matchAll(re)) {
|
|
89
|
+
candidates.push({ index: match.index || 0, value: match[1] });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
candidates
|
|
93
|
+
.sort((a, b) => a.index - b.index)
|
|
94
|
+
.forEach((candidate) => addMediaCandidate(paths, candidate.value));
|
|
95
|
+
|
|
96
|
+
return [...paths];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function escapeRegExp(value) {
|
|
100
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function referencesPath(value, normalizedPaths) {
|
|
104
|
+
const normalized = normalizeMediaPath(value);
|
|
105
|
+
return Boolean(normalized && normalizedPaths.has(normalized));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pathReferenceVariants(path) {
|
|
109
|
+
const normalized = normalizeMediaPath(path) || path;
|
|
110
|
+
const variants = new Set([normalized]);
|
|
111
|
+
if (normalized.startsWith('/')) {
|
|
112
|
+
variants.add(pathToFileURL(normalized).href);
|
|
113
|
+
variants.add(`about:${normalized}`);
|
|
114
|
+
variants.add(`about://${normalized}`);
|
|
115
|
+
}
|
|
116
|
+
return [...variants].sort((a, b) => b.length - a.length);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function stripMediaPathReferences(text, explicitPaths = []) {
|
|
120
|
+
let next = String(text || '');
|
|
121
|
+
const paths = [...new Set([
|
|
122
|
+
...explicitPaths,
|
|
123
|
+
...extractMediaPaths(next),
|
|
124
|
+
].map((path) => normalizeMediaPath(path)).filter(Boolean))];
|
|
125
|
+
const normalizedPaths = new Set(paths);
|
|
126
|
+
|
|
127
|
+
next = next
|
|
128
|
+
.replace(/<img[^>]+src=["']([^"']+)["'][^>]*>/gi, (match, src) => (
|
|
129
|
+
referencesPath(src, normalizedPaths) ? '' : match
|
|
130
|
+
))
|
|
131
|
+
.replace(/!\[[^\]]*\]\(\s*<([^>\n]+)>(?:\s+["'][^"']*["'])?\s*\)/gi, (match, target) => (
|
|
132
|
+
referencesPath(target, normalizedPaths) ? '' : match
|
|
133
|
+
))
|
|
134
|
+
.replace(/!\[[^\]]*\]\(\s*([^\s)>]+)>?(?:\s+["'][^"']*["'])?\s*\)/gi, (match, target) => (
|
|
135
|
+
referencesPath(target, normalizedPaths) ? '' : match
|
|
136
|
+
));
|
|
137
|
+
|
|
138
|
+
for (const path of paths) {
|
|
139
|
+
for (const variant of pathReferenceVariants(path)) {
|
|
140
|
+
const escapedPath = escapeRegExp(variant);
|
|
141
|
+
next = next
|
|
142
|
+
.replace(new RegExp(`\\[media attached[^\\]]*${escapedPath}[^\\]]*\\]\\s*`, 'gi'), '')
|
|
143
|
+
.replace(new RegExp(`^\\s*${escapedPath}\\s*$`, 'gim'), '')
|
|
144
|
+
.replace(new RegExp(escapedPath, 'g'), '');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return next
|
|
149
|
+
.replace(/<img[^>]+src=["'](?:file:|about:)(?:\/){0,3}(?:tmp|private|var|Users|home)[^"']*["'][^>]*>/gi, '')
|
|
150
|
+
.replace(/!\[[^\]]*\]\(\s*<?(?:file:|about:)(?:\/){0,3}(?:tmp|private|var|Users|home)[^)]*\)/gi, '')
|
|
151
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function normalizeOutboundMedia(result) {
|
|
156
|
+
const explicit = Array.isArray(result?.mediaUrls) ? result.mediaUrls : [];
|
|
157
|
+
const inferred = extractMediaPaths(result?.text || '');
|
|
158
|
+
return [...new Set([...explicit, ...inferred])].map((path) => ({
|
|
159
|
+
kind: 'local_path',
|
|
160
|
+
value: path,
|
|
161
|
+
mime: MIME_MAP[extname(path).toLowerCase()] || 'application/octet-stream',
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { AF_HOME, AF_CONFIG_PATH, loadPersistentConfig } from './config.mjs';
|
|
5
|
+
|
|
6
|
+
export const AF_ACTIVE_PROFILE_PATH = join(AF_HOME, 'active-profile');
|
|
7
|
+
export const AF_PROFILE_ROOT = join(AF_HOME, 'profiles');
|
|
8
|
+
|
|
9
|
+
function parseProfileRef(value) {
|
|
10
|
+
const normalized = String(value || '').trim();
|
|
11
|
+
const [adapter, ...rest] = normalized.split(':');
|
|
12
|
+
const userId = rest.join(':');
|
|
13
|
+
if (!adapter || !userId) return null;
|
|
14
|
+
return { adapter, userId, ref: `${adapter}:${userId}` };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeTextAtomic(filePath, text, mode = 0o600) {
|
|
18
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
19
|
+
const tempPath = `${filePath}.tmp`;
|
|
20
|
+
writeFileSync(tempPath, text, { mode });
|
|
21
|
+
renameSync(tempPath, filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeJsonAtomic(filePath, value) {
|
|
25
|
+
writeTextAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeDotenvAtomic(filePath, value) {
|
|
29
|
+
const lines = [
|
|
30
|
+
'# Persistent ticlawk profile config',
|
|
31
|
+
...Object.entries(value || {})
|
|
32
|
+
.filter(([, item]) => item !== undefined && item !== null && item !== '')
|
|
33
|
+
.map(([key, item]) => `${key}=${String(item)}`),
|
|
34
|
+
];
|
|
35
|
+
writeTextAtomic(filePath, `${lines.join('\n')}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function profileRef(adapter, userId) {
|
|
39
|
+
return `${adapter}:${userId}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getProfileDir(adapter, userId) {
|
|
43
|
+
return join(AF_PROFILE_ROOT, adapter, userId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getProfileConfigPath(adapter, userId) {
|
|
47
|
+
return join(getProfileDir(adapter, userId), '.config');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getProfileBindingsPath(adapter, userId) {
|
|
51
|
+
return join(getProfileDir(adapter, userId), 'bindings.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getActiveProfile() {
|
|
55
|
+
if (!existsSync(AF_ACTIVE_PROFILE_PATH)) return null;
|
|
56
|
+
try {
|
|
57
|
+
return parseProfileRef(readFileSync(AF_ACTIVE_PROFILE_PATH, 'utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getActiveBindingsPath(fallbackPath) {
|
|
64
|
+
const active = getActiveProfile();
|
|
65
|
+
if (!active) return fallbackPath;
|
|
66
|
+
return getProfileBindingsPath(active.adapter, active.userId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readProfileConfig(adapter, userId) {
|
|
70
|
+
const filePath = getProfileConfigPath(adapter, userId);
|
|
71
|
+
if (!existsSync(filePath)) return {};
|
|
72
|
+
try {
|
|
73
|
+
return dotenv.parse(readFileSync(filePath, 'utf8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readProfileMeta(adapter, userId) {
|
|
80
|
+
const filePath = join(getProfileDir(adapter, userId), 'profile.json');
|
|
81
|
+
if (!existsSync(filePath)) return null;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function saveProfile({ adapter, userId, config = {}, meta = {} }) {
|
|
90
|
+
if (!adapter || !userId) throw new Error('profile adapter and userId are required');
|
|
91
|
+
const dir = getProfileDir(adapter, userId);
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
const currentConfig = readProfileConfig(adapter, userId);
|
|
94
|
+
const nextConfig = { ...currentConfig, ...config };
|
|
95
|
+
delete nextConfig.TICLAWK_SETUP_CODE;
|
|
96
|
+
writeDotenvAtomic(getProfileConfigPath(adapter, userId), nextConfig);
|
|
97
|
+
|
|
98
|
+
const currentMeta = readProfileMeta(adapter, userId) || {};
|
|
99
|
+
writeJsonAtomic(join(dir, 'profile.json'), {
|
|
100
|
+
...currentMeta,
|
|
101
|
+
...meta,
|
|
102
|
+
adapter,
|
|
103
|
+
userId,
|
|
104
|
+
updatedAt: new Date().toISOString(),
|
|
105
|
+
createdAt: currentMeta.createdAt || meta.createdAt || new Date().toISOString(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function activateProfile(adapter, userId) {
|
|
110
|
+
const config = readProfileConfig(adapter, userId);
|
|
111
|
+
if (!Object.keys(config).length) {
|
|
112
|
+
throw new Error(`profile does not exist: ${profileRef(adapter, userId)}`);
|
|
113
|
+
}
|
|
114
|
+
writeTextAtomic(AF_ACTIVE_PROFILE_PATH, `${profileRef(adapter, userId)}\n`);
|
|
115
|
+
writeDotenvAtomic(AF_CONFIG_PATH, config);
|
|
116
|
+
for (const [key, value] of Object.entries(config)) {
|
|
117
|
+
process.env[key] = String(value);
|
|
118
|
+
}
|
|
119
|
+
return { adapter, userId, ref: profileRef(adapter, userId) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function saveAndActivateProfile({ adapter, userId, config = {}, meta = {} }) {
|
|
123
|
+
saveProfile({ adapter, userId, config, meta });
|
|
124
|
+
return activateProfile(adapter, userId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function listProfiles() {
|
|
128
|
+
const result = [];
|
|
129
|
+
for (const adapter of fsSafeReaddir(AF_PROFILE_ROOT)) {
|
|
130
|
+
for (const userId of fsSafeReaddir(join(AF_PROFILE_ROOT, adapter))) {
|
|
131
|
+
const meta = readProfileMeta(adapter, userId) || {};
|
|
132
|
+
result.push({
|
|
133
|
+
adapter,
|
|
134
|
+
userId,
|
|
135
|
+
ref: profileRef(adapter, userId),
|
|
136
|
+
emailMasked: meta.emailMasked || meta.email_masked || '',
|
|
137
|
+
phoneMasked: meta.phoneMasked || meta.phone_masked || '',
|
|
138
|
+
meta,
|
|
139
|
+
config: readProfileConfig(adapter, userId),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fsSafeReaddir(path) {
|
|
147
|
+
try {
|
|
148
|
+
return existsSync(path)
|
|
149
|
+
? readdirSync(path, { withFileTypes: true })
|
|
150
|
+
.filter((entry) => entry.isDirectory())
|
|
151
|
+
.map((entry) => entry.name)
|
|
152
|
+
: [];
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function ensureLegacyProfile({ adapter, userId, meta = {} }) {
|
|
159
|
+
if (!adapter || !userId) return null;
|
|
160
|
+
const profileConfigPath = getProfileConfigPath(adapter, userId);
|
|
161
|
+
if (!existsSync(profileConfigPath)) {
|
|
162
|
+
saveProfile({
|
|
163
|
+
adapter,
|
|
164
|
+
userId,
|
|
165
|
+
config: loadPersistentConfig(),
|
|
166
|
+
meta,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (!getActiveProfile()) {
|
|
170
|
+
writeTextAtomic(AF_ACTIVE_PROFILE_PATH, `${profileRef(adapter, userId)}\n`);
|
|
171
|
+
}
|
|
172
|
+
return readProfileMeta(adapter, userId);
|
|
173
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime contract shared by the daemon and CLI.
|
|
3
|
+
*
|
|
4
|
+
* Runtimes expose a small common surface:
|
|
5
|
+
* - `resolveBinding(payload)` validates a runtime locator such as sessionId,
|
|
6
|
+
* workdir, or agentId and returns the metadata persisted in the binding store
|
|
7
|
+
* - `deliverTurn(inbound, ctx)` handles a routed inbound turn
|
|
8
|
+
* - optional lifecycle hooks let the daemon replay bindings and recover
|
|
9
|
+
* after restart without adapter-specific branching in startTiclawk()
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { normalizeServiceType } from './runtime-registry.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {string} RuntimeServiceType
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} RuntimeBindingPayload
|
|
20
|
+
* @property {string} [adapter]
|
|
21
|
+
* @property {RuntimeServiceType} serviceType
|
|
22
|
+
* @property {string} [sessionId]
|
|
23
|
+
* @property {string} [workdir]
|
|
24
|
+
* @property {string} [projectDir]
|
|
25
|
+
* @property {string} [cwd]
|
|
26
|
+
* @property {string} [agentId]
|
|
27
|
+
* @property {string} [code]
|
|
28
|
+
* @property {string} [name]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} ResolvedRuntimeBinding
|
|
33
|
+
* @property {string} runtime
|
|
34
|
+
* @property {string} displayName
|
|
35
|
+
* @property {Record<string, any>} runtimeMeta
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} RuntimeDeliveryContext
|
|
40
|
+
* @property {any} adapter
|
|
41
|
+
* @property {Record<string, AgentRuntime>} runtimes
|
|
42
|
+
* @property {(bindingId: string) => any} getBinding
|
|
43
|
+
* @property {(filters?: Record<string, any>) => any[]} listBindings
|
|
44
|
+
* @property {(bindingId: string) => Promise<void>} [deleteBinding]
|
|
45
|
+
* @property {(binding: any) => Promise<any>} [cacheBinding]
|
|
46
|
+
* @property {(binding: any) => Promise<any>} upsertBinding
|
|
47
|
+
* @property {(inbound: any, runtimeName: string) => Promise<string>} buildImageMessageFromInbound
|
|
48
|
+
* @property {any} logger
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} AgentRuntime
|
|
53
|
+
* @property {string} name
|
|
54
|
+
* @property {(payload: RuntimeBindingPayload) => Promise<ResolvedRuntimeBinding>} resolveBinding
|
|
55
|
+
* @property {(inbound: any, ctx: RuntimeDeliveryContext) => Promise<boolean>} deliverTurn
|
|
56
|
+
* @property {(binding: any, ctx: { adapter: any, logger: any }) => (Promise<void>|void)} [onBindingUpdated]
|
|
57
|
+
* @property {(ctx: { adapter: any, getBinding: (bindingId: string) => any }) => (Promise<number>|number)} [recoverInFlight]
|
|
58
|
+
* @property {(binding: any, ctx: { adapter: any, logger: any }) => (Promise<number>|number)} [reconcileAfterRestart]
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize user-facing runtime selectors into the canonical serviceType
|
|
63
|
+
* stored in bindings and passed over the local control API.
|
|
64
|
+
*
|
|
65
|
+
* Supported inputs are defined by `RUNTIME_DEFINITIONS` in
|
|
66
|
+
* `runtime-registry.mjs`.
|
|
67
|
+
*/
|
|
68
|
+
export { normalizeServiceType };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime registry.
|
|
3
|
+
*
|
|
4
|
+
* New runtime integrations should be added here plus their own
|
|
5
|
+
* `src/runtimes/<name>/` implementation. Core code consumes this registry for
|
|
6
|
+
* aliases, config keys, CLI labels, and lazy runtime loading.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function normalizeRuntimeToken(value) {
|
|
10
|
+
return String(value || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const RUNTIME_DEFINITIONS = Object.freeze([
|
|
14
|
+
{
|
|
15
|
+
name: 'claude_code',
|
|
16
|
+
displayName: 'Claude Code',
|
|
17
|
+
aliases: ['', 'claude', 'claude_code', 'claude-code'],
|
|
18
|
+
streamingEnvKey: 'AF_STREAMING_CLAUDE_CODE',
|
|
19
|
+
executableConfigKey: 'CLAUDE_CODE_BIN',
|
|
20
|
+
executableCliKey: 'runtimes.claude_code.path',
|
|
21
|
+
connect: { locator: 'workdir', session: true },
|
|
22
|
+
loadRuntime: async () => (await import('../runtimes/claude-code/index.mjs')).claudeCodeRuntime,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'codex',
|
|
26
|
+
displayName: 'Codex',
|
|
27
|
+
aliases: ['codex'],
|
|
28
|
+
streamingEnvKey: 'AF_STREAMING_CODEX',
|
|
29
|
+
executableConfigKey: 'CODEX_BIN',
|
|
30
|
+
executableCliKey: 'runtimes.codex.path',
|
|
31
|
+
connect: { locator: 'workdir', session: true },
|
|
32
|
+
loadRuntime: async () => (await import('../runtimes/codex/index.mjs')).codexRuntime,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'openclaw',
|
|
36
|
+
displayName: 'OpenClaw',
|
|
37
|
+
aliases: ['openclaw', 'open_claw', 'open-claw'],
|
|
38
|
+
streamingEnvKey: 'AF_STREAMING_OPENCLAW',
|
|
39
|
+
connect: { locator: 'agentId', session: false },
|
|
40
|
+
loadRuntime: async () => (await import('../runtimes/openclaw/index.mjs')).openClawRuntime,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'opencode',
|
|
44
|
+
displayName: 'opencode',
|
|
45
|
+
aliases: ['opencode', 'open_code', 'open-code'],
|
|
46
|
+
streamingEnvKey: 'AF_STREAMING_OPENCODE',
|
|
47
|
+
executableConfigKey: 'OPENCODE_BIN',
|
|
48
|
+
executableCliKey: 'runtimes.opencode.path',
|
|
49
|
+
connect: { locator: 'workdir', session: true },
|
|
50
|
+
loadRuntime: async () => (await import('../runtimes/opencode/index.mjs')).openCodeRuntime,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'pi',
|
|
54
|
+
displayName: 'pi',
|
|
55
|
+
aliases: ['pi', 'pi_agent', 'pi-agent'],
|
|
56
|
+
streamingEnvKey: 'AF_STREAMING_PI',
|
|
57
|
+
executableConfigKey: 'PI_BIN',
|
|
58
|
+
executableCliKey: 'runtimes.pi.path',
|
|
59
|
+
connect: { locator: 'workdir', session: true },
|
|
60
|
+
loadRuntime: async () => (await import('../runtimes/pi/index.mjs')).piRuntime,
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export function normalizeServiceType(value) {
|
|
65
|
+
const normalized = normalizeRuntimeToken(value);
|
|
66
|
+
const match = RUNTIME_DEFINITIONS.find((runtime) => {
|
|
67
|
+
return runtime.name === normalized || runtime.aliases.map(normalizeRuntimeToken).includes(normalized);
|
|
68
|
+
});
|
|
69
|
+
return match?.name || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getRuntimeDefinition(value) {
|
|
73
|
+
const name = normalizeServiceType(value);
|
|
74
|
+
return RUNTIME_DEFINITIONS.find((runtime) => runtime.name === name) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getRuntimeDisplayName(value) {
|
|
78
|
+
return getRuntimeDefinition(value)?.displayName || String(value || '').trim() || 'runtime';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getRuntimeExecutableDefinitions() {
|
|
82
|
+
return RUNTIME_DEFINITIONS.filter((runtime) => runtime.executableConfigKey && runtime.executableCliKey);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function buildRuntimeContext() {
|
|
86
|
+
const loaded = await Promise.all(RUNTIME_DEFINITIONS.map(async (definition) => {
|
|
87
|
+
const runtime = await definition.loadRuntime();
|
|
88
|
+
return { definition, runtime };
|
|
89
|
+
}));
|
|
90
|
+
const runtimeList = loaded.map(({ runtime }) => runtime);
|
|
91
|
+
const runtimes = Object.fromEntries(loaded.map(({ runtime }) => [runtime.name, runtime]));
|
|
92
|
+
return { runtimeList, runtimes };
|
|
93
|
+
}
|