openclaw-teleport 0.2.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/src/pack.ts ADDED
@@ -0,0 +1,184 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { execSync } from 'node:child_process';
5
+ import {
6
+ loadConfig,
7
+ findAgent,
8
+ collectMarkdownFiles,
9
+ collectMemoryDir,
10
+ collectDbFiles,
11
+ collectCronFiles,
12
+ getGitHubRepos,
13
+ detectServices,
14
+ extractAgentConfig,
15
+ extractChannelsConfig,
16
+ sanitizeAgentDefaults,
17
+ loadCronJobs,
18
+ type Manifest,
19
+ } from './utils.js';
20
+
21
+ const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
22
+ const CRON_DIR = path.join(OPENCLAW_DIR, 'cron');
23
+
24
+ export async function pack(agentId?: string, outputPath?: string): Promise<void> {
25
+ console.log('\n🌸 openclaw-teleport — packing agent soul...\n');
26
+
27
+ // Load config and find agent
28
+ const config = loadConfig();
29
+ const agent = findAgent(config, agentId);
30
+
31
+ console.log(`šŸ“¦ Agent: ${agent.name} (${agent.id})`);
32
+ console.log(`šŸ“‚ Workspace: ${agent.workspace}\n`);
33
+
34
+ if (!fs.existsSync(agent.workspace)) {
35
+ throw new Error(`āŒ Workspace not found: ${agent.workspace}`);
36
+ }
37
+
38
+ // Create temp directory for staging
39
+ const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
40
+ const soulName = `${agent.id}_${date}`;
41
+ const tmpDir = path.join(os.tmpdir(), `openclaw-teleport-${soulName}`);
42
+ const stageDir = path.join(tmpDir, 'soul');
43
+
44
+ // Clean up any previous staging
45
+ if (fs.existsSync(tmpDir)) {
46
+ fs.rmSync(tmpDir, { recursive: true });
47
+ }
48
+ fs.mkdirSync(stageDir, { recursive: true });
49
+
50
+ const allFiles: string[] = [];
51
+
52
+ // 1. Collect identity files (.md in workspace root)
53
+ console.log('šŸ“ Collecting identity files...');
54
+ const mdFiles = collectMarkdownFiles(agent.workspace);
55
+ for (const f of mdFiles) {
56
+ const src = path.join(agent.workspace, f);
57
+ const dst = path.join(stageDir, 'identity', f);
58
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
59
+ fs.copyFileSync(src, dst);
60
+ allFiles.push(`identity/${f}`);
61
+ }
62
+ console.log(` āœ… ${mdFiles.length} markdown files`);
63
+
64
+ // 2. Collect memory directory
65
+ console.log('🧠 Collecting memory...');
66
+ const memFiles = collectMemoryDir(agent.workspace);
67
+ for (const f of memFiles) {
68
+ const src = path.join(agent.workspace, f);
69
+ const dst = path.join(stageDir, f);
70
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
71
+ fs.copyFileSync(src, dst);
72
+ allFiles.push(f);
73
+ }
74
+ console.log(` āœ… ${memFiles.length} memory files`);
75
+
76
+ // 3. Collect .db files
77
+ console.log('šŸ—„ļø Collecting tool data...');
78
+ const dbFiles = collectDbFiles(agent.workspace);
79
+ for (const f of dbFiles) {
80
+ const src = path.join(agent.workspace, f);
81
+ const dst = path.join(stageDir, 'data', f);
82
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
83
+ fs.copyFileSync(src, dst);
84
+ allFiles.push(`data/${f}`);
85
+ }
86
+ console.log(` āœ… ${dbFiles.length} database files`);
87
+
88
+ // 4. Extract agent config
89
+ console.log('āš™ļø Extracting agent config...');
90
+ const agentConfig = extractAgentConfig(config, agent.id);
91
+ const configPath = path.join(stageDir, 'config', 'agent-config.json');
92
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
93
+ fs.writeFileSync(configPath, JSON.stringify(agentConfig, null, 2));
94
+ allFiles.push('config/agent-config.json');
95
+ console.log(' āœ… Agent config saved');
96
+
97
+ // 5. Collect cron job files
98
+ console.log('ā° Collecting cron jobs...');
99
+ const cronFiles = collectCronFiles(agent.id);
100
+ for (const f of cronFiles) {
101
+ const src = path.join(CRON_DIR, f);
102
+ const dst = path.join(stageDir, 'cron', f);
103
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
104
+ fs.copyFileSync(src, dst);
105
+ allFiles.push(`cron/${f}`);
106
+ }
107
+ console.log(` āœ… ${cronFiles.length} cron files`);
108
+
109
+ // 6. Load full cron job content for this agent
110
+ console.log('ā° Extracting cron job definitions...');
111
+ const cronJobs = loadCronJobs(agent.id);
112
+ console.log(` āœ… ${cronJobs.length} cron jobs for ${agent.id}`);
113
+
114
+ // 7. Get GitHub repos
115
+ console.log('šŸ™ Fetching GitHub repos...');
116
+ const repos = getGitHubRepos('kagura-agent');
117
+ console.log(` āœ… ${repos.length} repos found`);
118
+
119
+ // 8. Detect services
120
+ const services = detectServices(config);
121
+ console.log(`šŸ”— Services to rebind: ${services.length > 0 ? services.join(', ') : 'none'}`);
122
+
123
+ // 9. Extract channels config (with credentials)
124
+ console.log('šŸ”‘ Extracting channel credentials...');
125
+ const channelsConfig = extractChannelsConfig(config, agent.id);
126
+ const channelCount = Object.keys(channelsConfig).length;
127
+ console.log(` āœ… ${channelCount} channel(s) saved`);
128
+
129
+ // 10. Extract agent defaults and models config
130
+ const agentDefaults = sanitizeAgentDefaults(config.agents?.defaults ?? {});
131
+ const modelsConfig = config.models ?? {};
132
+ const bindingsConfig = config.bindings ?? [];
133
+
134
+ // 11. Generate manifest
135
+ const manifest: Manifest = {
136
+ agent_id: agent.id,
137
+ agent_name: agent.name,
138
+ packed_at: new Date().toISOString(),
139
+ files: allFiles,
140
+ github_repos: repos,
141
+ services_to_rebind: services,
142
+ channels: channelsConfig,
143
+ cron_jobs: cronJobs,
144
+ agent_defaults: agentDefaults,
145
+ models_config: modelsConfig,
146
+ bindings: bindingsConfig as Array<Record<string, unknown>>,
147
+ };
148
+
149
+ const manifestPath = path.join(stageDir, 'manifest.json');
150
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
151
+
152
+ // 12. Create tarball
153
+ const outputFile = outputPath ? path.resolve(outputPath) : path.resolve(`${soulName}.soul`);
154
+ console.log('\nšŸ“¦ Packing soul archive...');
155
+
156
+ execSync(`tar -czf "${outputFile}" -C "${tmpDir}" soul`, {
157
+ encoding: 'utf-8',
158
+ });
159
+
160
+ // Clean up staging
161
+ fs.rmSync(tmpDir, { recursive: true });
162
+
163
+ // Summary
164
+ const stats = fs.statSync(outputFile);
165
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
166
+
167
+ console.log('\n' + '═'.repeat(50));
168
+ console.log('🌸 Soul packed successfully!');
169
+ console.log('═'.repeat(50));
170
+ console.log(`šŸ“¦ File: ${outputFile}`);
171
+ console.log(`šŸ“ Size: ${sizeMB} MB`);
172
+ console.log(`šŸ†” Agent: ${agent.name} (${agent.id})`);
173
+ console.log(`šŸ“ Files: ${allFiles.length}`);
174
+ console.log(`šŸ™ Repos: ${repos.length}`);
175
+ console.log(`šŸ”— Services: ${services.join(', ') || 'none'}`);
176
+ console.log(`šŸ”‘ Channels: ${channelCount}`);
177
+ console.log(`ā° Cron: ${cronJobs.length} jobs`);
178
+ console.log(`šŸ“… Packed: ${manifest.packed_at}`);
179
+ console.log('═'.repeat(50));
180
+
181
+ console.log('\nāš ļø SECURITY WARNING: The .soul file contains credentials');
182
+ console.log(' (API tokens, app secrets). Treat it like a password file.');
183
+ console.log(' Do NOT commit it to git or share publicly.\n');
184
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,311 @@
1
+ import { execSync } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+
6
+ // ── Paths ──────────────────────────────────────────────────────────
7
+
8
+ const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
9
+ const CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
10
+ const CRON_DIR = path.join(OPENCLAW_DIR, 'cron');
11
+
12
+ // ── Types ──────────────────────────────────────────────────────────
13
+
14
+ export interface AgentConfig {
15
+ id: string;
16
+ name: string;
17
+ workspace: string;
18
+ agentDir: string;
19
+ }
20
+
21
+ export interface Manifest {
22
+ agent_id: string;
23
+ agent_name: string;
24
+ packed_at: string;
25
+ files: string[];
26
+ github_repos: Array<{ name: string; url: string; isFork: boolean }>;
27
+ services_to_rebind: string[];
28
+ /** Channel configurations with credentials (added in v0.2) */
29
+ channels?: Record<string, unknown>;
30
+ /** Full cron jobs content (added in v0.2) */
31
+ cron_jobs?: CronJob[];
32
+ /** Agent defaults from openclaw.json (added in v0.2) */
33
+ agent_defaults?: Record<string, unknown>;
34
+ /** Models configuration (added in v0.2) */
35
+ models_config?: Record<string, unknown>;
36
+ /** Bindings configuration (added in v0.2) */
37
+ bindings?: Array<Record<string, unknown>>;
38
+ }
39
+
40
+ export interface CronJob {
41
+ id: string;
42
+ agentId: string;
43
+ name: string;
44
+ enabled: boolean;
45
+ schedule: Record<string, unknown>;
46
+ sessionTarget?: string;
47
+ wakeMode?: string;
48
+ payload: Record<string, unknown>;
49
+ delivery?: Record<string, unknown>;
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ export interface OpenClawConfig {
54
+ agents?: {
55
+ defaults?: Record<string, unknown>;
56
+ list?: AgentConfig[];
57
+ };
58
+ channels?: Record<string, unknown>;
59
+ models?: Record<string, unknown>;
60
+ bindings?: Array<Record<string, unknown>>;
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ // ── Config helpers ─────────────────────────────────────────────────
65
+
66
+ export function loadConfig(): OpenClawConfig {
67
+ if (!fs.existsSync(CONFIG_PATH)) {
68
+ throw new Error(`āŒ Config not found: ${CONFIG_PATH}\n Is OpenClaw installed?`);
69
+ }
70
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
71
+ }
72
+
73
+ export function findAgent(config: OpenClawConfig, agentId?: string): AgentConfig {
74
+ const agents = config.agents?.list ?? [];
75
+ if (agents.length === 0) {
76
+ throw new Error('āŒ No agents configured in openclaw.json');
77
+ }
78
+
79
+ if (agentId) {
80
+ const agent = agents.find((a) => a.id === agentId);
81
+ if (!agent) {
82
+ const ids = agents.map((a) => a.id).join(', ');
83
+ throw new Error(`āŒ Agent "${agentId}" not found. Available: ${ids}`);
84
+ }
85
+ return agent;
86
+ }
87
+
88
+ // Default to first agent
89
+ return agents[0];
90
+ }
91
+
92
+ // ── File collection ────────────────────────────────────────────────
93
+
94
+ export function collectMarkdownFiles(workspace: string): string[] {
95
+ const files: string[] = [];
96
+ const entries = fs.readdirSync(workspace, { withFileTypes: true });
97
+ for (const entry of entries) {
98
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
99
+ if (entry.isFile() && entry.name.endsWith('.md')) {
100
+ files.push(entry.name);
101
+ }
102
+ }
103
+ return files;
104
+ }
105
+
106
+ export function collectMemoryDir(workspace: string): string[] {
107
+ const memoryDir = path.join(workspace, 'memory');
108
+ if (!fs.existsSync(memoryDir)) return [];
109
+ const files: string[] = [];
110
+ const walk = (dir: string, prefix: string) => {
111
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
112
+ for (const entry of entries) {
113
+ const rel = path.join(prefix, entry.name);
114
+ if (entry.isDirectory()) {
115
+ walk(path.join(dir, entry.name), rel);
116
+ } else {
117
+ files.push(rel);
118
+ }
119
+ }
120
+ };
121
+ walk(memoryDir, 'memory');
122
+ return files;
123
+ }
124
+
125
+ export function collectDbFiles(workspace: string): string[] {
126
+ const files: string[] = [];
127
+ // Only collect .db files from known tool data directories, not recursively
128
+ // This prevents grabbing test DBs or unrelated data from project subdirs
129
+ const knownPaths = [
130
+ 'gogetajob/data/gogetajob.db',
131
+ 'flowforge/flowforge.db',
132
+ 'data/gogetajob.db',
133
+ 'data/flowforge.db',
134
+ ];
135
+ for (const rel of knownPaths) {
136
+ const full = path.join(workspace, rel);
137
+ if (fs.existsSync(full)) {
138
+ files.push(rel);
139
+ }
140
+ }
141
+ // Also check workspace root for any .db files
142
+ try {
143
+ const rootEntries = fs.readdirSync(workspace, { withFileTypes: true });
144
+ for (const entry of rootEntries) {
145
+ if (entry.isFile() && entry.name.endsWith('.db')) {
146
+ files.push(entry.name);
147
+ }
148
+ }
149
+ } catch {}
150
+ return files;
151
+ }
152
+
153
+ export function collectCronFiles(agentId: string): string[] {
154
+ if (!fs.existsSync(CRON_DIR)) return [];
155
+ const files: string[] = [];
156
+ const entries = fs.readdirSync(CRON_DIR, { withFileTypes: true });
157
+ for (const entry of entries) {
158
+ // Include jobs.json and any agent-specific files
159
+ if (entry.isFile()) {
160
+ files.push(entry.name);
161
+ }
162
+ }
163
+ return files;
164
+ }
165
+
166
+ // ── Cron job content extraction ────────────────────────────────────
167
+
168
+ /**
169
+ * Load full cron jobs for a specific agent from jobs.json.
170
+ * Returns the actual job objects (not just file names).
171
+ */
172
+ export function loadCronJobs(agentId: string): CronJob[] {
173
+ const jobsPath = path.join(CRON_DIR, 'jobs.json');
174
+ if (!fs.existsSync(jobsPath)) return [];
175
+
176
+ try {
177
+ const data = JSON.parse(fs.readFileSync(jobsPath, 'utf-8'));
178
+ const jobs: CronJob[] = data.jobs ?? [];
179
+ // Filter to this agent's jobs
180
+ return jobs.filter((j) => j.agentId === agentId);
181
+ } catch {
182
+ return [];
183
+ }
184
+ }
185
+
186
+ // ── GitHub repos ───────────────────────────────────────────────────
187
+
188
+ export function getGitHubRepos(owner: string): Array<{ name: string; url: string; isFork: boolean }> {
189
+ try {
190
+ const output = execSync(`gh repo list ${owner} --json name,url,isFork --limit 100`, {
191
+ encoding: 'utf-8',
192
+ timeout: 30000,
193
+ });
194
+ return JSON.parse(output);
195
+ } catch (err) {
196
+ console.log('āš ļø Could not fetch GitHub repos (gh CLI not available or not authenticated)');
197
+ return [];
198
+ }
199
+ }
200
+
201
+ // ── Services detection ─────────────────────────────────────────────
202
+
203
+ export function detectServices(config: OpenClawConfig): string[] {
204
+ const services = new Set<string>();
205
+ const channels = config.channels ?? (config as Record<string, unknown>);
206
+
207
+ // Walk the config looking for channel-like keys
208
+ for (const key of Object.keys(config)) {
209
+ if (['feishu', 'discord', 'telegram', 'slack', 'whatsapp', 'github', 'twitter', 'email'].includes(key)) {
210
+ services.add(key);
211
+ }
212
+ }
213
+
214
+ // Also check if channels object exists
215
+ if (config.channels && typeof config.channels === 'object') {
216
+ for (const key of Object.keys(config.channels)) {
217
+ services.add(key);
218
+ }
219
+ }
220
+
221
+ return Array.from(services);
222
+ }
223
+
224
+ // ── Agent config extraction ────────────────────────────────────────
225
+
226
+ export function extractAgentConfig(config: OpenClawConfig, agentId: string): Record<string, unknown> {
227
+ const agent = config.agents?.list?.find((a) => a.id === agentId);
228
+ const defaults = config.agents?.defaults ?? {};
229
+ return {
230
+ agent,
231
+ defaults,
232
+ };
233
+ }
234
+
235
+ // ── Channel config extraction ──────────────────────────────────────
236
+
237
+ /**
238
+ * Extract channel configurations (including credentials) relevant to an agent.
239
+ * Strips absolute paths but preserves tokens, appIds, appSecrets, etc.
240
+ */
241
+ export function extractChannelsConfig(config: OpenClawConfig, agentId: string): Record<string, unknown> {
242
+ if (!config.channels) return {};
243
+
244
+ // Deep clone to avoid mutating original
245
+ const channels = JSON.parse(JSON.stringify(config.channels));
246
+
247
+ // Strip absolute paths from the cloned config
248
+ stripAbsolutePaths(channels);
249
+
250
+ return channels;
251
+ }
252
+
253
+ /**
254
+ * Recursively strip values that look like absolute paths.
255
+ * We preserve tokens, keys, IDs — only remove filesystem paths.
256
+ */
257
+ function stripAbsolutePaths(obj: Record<string, unknown>): void {
258
+ for (const key of Object.keys(obj)) {
259
+ const val = obj[key];
260
+ if (typeof val === 'string' && val.startsWith('/') && (val.includes('/home/') || val.includes('/Users/') || val.includes('/root/'))) {
261
+ // Mark as path-to-regenerate
262
+ obj[key] = `__PATH_PLACEHOLDER__`;
263
+ } else if (val && typeof val === 'object' && !Array.isArray(val)) {
264
+ stripAbsolutePaths(val as Record<string, unknown>);
265
+ } else if (Array.isArray(val)) {
266
+ for (let i = 0; i < val.length; i++) {
267
+ if (val[i] && typeof val[i] === 'object') {
268
+ stripAbsolutePaths(val[i] as Record<string, unknown>);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Strip absolute paths from agent defaults config.
277
+ * Replaces workspace, agentDir, and other path-like values with placeholders.
278
+ */
279
+ export function sanitizeAgentDefaults(defaults: Record<string, unknown>): Record<string, unknown> {
280
+ const sanitized = JSON.parse(JSON.stringify(defaults));
281
+ // Remove workspace — it will be set dynamically on unpack
282
+ delete sanitized.workspace;
283
+ return sanitized;
284
+ }
285
+
286
+ // ── Command helpers ────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Check if a command exists on the system.
290
+ */
291
+ export function commandExists(cmd: string): boolean {
292
+ try {
293
+ execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: 'pipe' });
294
+ return true;
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Check GitHub CLI auth status.
302
+ * Returns true if authenticated.
303
+ */
304
+ export function isGhAuthenticated(): boolean {
305
+ try {
306
+ execSync('gh auth status', { encoding: 'utf-8', stdio: 'pipe' });
307
+ return true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "declaration": true,
13
+ "resolveJsonModule": true,
14
+ "allowImportingTsExtensions": false
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }