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/dist/cli.mjs ADDED
@@ -0,0 +1,831 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/pack.ts
7
+ import * as fs2 from "node:fs";
8
+ import * as path2 from "node:path";
9
+ import * as os2 from "node:os";
10
+ import { execSync as execSync2 } from "node:child_process";
11
+
12
+ // src/utils.ts
13
+ import { execSync } from "node:child_process";
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import * as os from "node:os";
17
+ var OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
18
+ var CONFIG_PATH = path.join(OPENCLAW_DIR, "openclaw.json");
19
+ var CRON_DIR = path.join(OPENCLAW_DIR, "cron");
20
+ function loadConfig() {
21
+ if (!fs.existsSync(CONFIG_PATH)) {
22
+ throw new Error(`\u274C Config not found: ${CONFIG_PATH}
23
+ Is OpenClaw installed?`);
24
+ }
25
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
26
+ }
27
+ function findAgent(config, agentId) {
28
+ const agents = config.agents?.list ?? [];
29
+ if (agents.length === 0) {
30
+ throw new Error("\u274C No agents configured in openclaw.json");
31
+ }
32
+ if (agentId) {
33
+ const agent = agents.find((a) => a.id === agentId);
34
+ if (!agent) {
35
+ const ids = agents.map((a) => a.id).join(", ");
36
+ throw new Error(`\u274C Agent "${agentId}" not found. Available: ${ids}`);
37
+ }
38
+ return agent;
39
+ }
40
+ return agents[0];
41
+ }
42
+ function collectMarkdownFiles(workspace) {
43
+ const files = [];
44
+ const entries = fs.readdirSync(workspace, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
47
+ if (entry.isFile() && entry.name.endsWith(".md")) {
48
+ files.push(entry.name);
49
+ }
50
+ }
51
+ return files;
52
+ }
53
+ function collectMemoryDir(workspace) {
54
+ const memoryDir = path.join(workspace, "memory");
55
+ if (!fs.existsSync(memoryDir)) return [];
56
+ const files = [];
57
+ const walk = (dir, prefix) => {
58
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
59
+ for (const entry of entries) {
60
+ const rel = path.join(prefix, entry.name);
61
+ if (entry.isDirectory()) {
62
+ walk(path.join(dir, entry.name), rel);
63
+ } else {
64
+ files.push(rel);
65
+ }
66
+ }
67
+ };
68
+ walk(memoryDir, "memory");
69
+ return files;
70
+ }
71
+ function collectDbFiles(workspace) {
72
+ const files = [];
73
+ const knownPaths = [
74
+ "gogetajob/data/gogetajob.db",
75
+ "flowforge/flowforge.db",
76
+ "data/gogetajob.db",
77
+ "data/flowforge.db"
78
+ ];
79
+ for (const rel of knownPaths) {
80
+ const full = path.join(workspace, rel);
81
+ if (fs.existsSync(full)) {
82
+ files.push(rel);
83
+ }
84
+ }
85
+ try {
86
+ const rootEntries = fs.readdirSync(workspace, { withFileTypes: true });
87
+ for (const entry of rootEntries) {
88
+ if (entry.isFile() && entry.name.endsWith(".db")) {
89
+ files.push(entry.name);
90
+ }
91
+ }
92
+ } catch {
93
+ }
94
+ return files;
95
+ }
96
+ function collectCronFiles(agentId) {
97
+ if (!fs.existsSync(CRON_DIR)) return [];
98
+ const files = [];
99
+ const entries = fs.readdirSync(CRON_DIR, { withFileTypes: true });
100
+ for (const entry of entries) {
101
+ if (entry.isFile()) {
102
+ files.push(entry.name);
103
+ }
104
+ }
105
+ return files;
106
+ }
107
+ function loadCronJobs(agentId) {
108
+ const jobsPath = path.join(CRON_DIR, "jobs.json");
109
+ if (!fs.existsSync(jobsPath)) return [];
110
+ try {
111
+ const data = JSON.parse(fs.readFileSync(jobsPath, "utf-8"));
112
+ const jobs = data.jobs ?? [];
113
+ return jobs.filter((j) => j.agentId === agentId);
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+ function getGitHubRepos(owner) {
119
+ try {
120
+ const output = execSync(`gh repo list ${owner} --json name,url,isFork --limit 100`, {
121
+ encoding: "utf-8",
122
+ timeout: 3e4
123
+ });
124
+ return JSON.parse(output);
125
+ } catch (err) {
126
+ console.log("\u26A0\uFE0F Could not fetch GitHub repos (gh CLI not available or not authenticated)");
127
+ return [];
128
+ }
129
+ }
130
+ function detectServices(config) {
131
+ const services = /* @__PURE__ */ new Set();
132
+ const channels = config.channels ?? config;
133
+ for (const key of Object.keys(config)) {
134
+ if (["feishu", "discord", "telegram", "slack", "whatsapp", "github", "twitter", "email"].includes(key)) {
135
+ services.add(key);
136
+ }
137
+ }
138
+ if (config.channels && typeof config.channels === "object") {
139
+ for (const key of Object.keys(config.channels)) {
140
+ services.add(key);
141
+ }
142
+ }
143
+ return Array.from(services);
144
+ }
145
+ function extractAgentConfig(config, agentId) {
146
+ const agent = config.agents?.list?.find((a) => a.id === agentId);
147
+ const defaults = config.agents?.defaults ?? {};
148
+ return {
149
+ agent,
150
+ defaults
151
+ };
152
+ }
153
+ function extractChannelsConfig(config, agentId) {
154
+ if (!config.channels) return {};
155
+ const channels = JSON.parse(JSON.stringify(config.channels));
156
+ stripAbsolutePaths(channels);
157
+ return channels;
158
+ }
159
+ function stripAbsolutePaths(obj) {
160
+ for (const key of Object.keys(obj)) {
161
+ const val = obj[key];
162
+ if (typeof val === "string" && val.startsWith("/") && (val.includes("/home/") || val.includes("/Users/") || val.includes("/root/"))) {
163
+ obj[key] = `__PATH_PLACEHOLDER__`;
164
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
165
+ stripAbsolutePaths(val);
166
+ } else if (Array.isArray(val)) {
167
+ for (let i = 0; i < val.length; i++) {
168
+ if (val[i] && typeof val[i] === "object") {
169
+ stripAbsolutePaths(val[i]);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ function sanitizeAgentDefaults(defaults) {
176
+ const sanitized = JSON.parse(JSON.stringify(defaults));
177
+ delete sanitized.workspace;
178
+ return sanitized;
179
+ }
180
+ function commandExists(cmd) {
181
+ try {
182
+ execSync(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
183
+ return true;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ function isGhAuthenticated() {
189
+ try {
190
+ execSync("gh auth status", { encoding: "utf-8", stdio: "pipe" });
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ // src/pack.ts
198
+ var OPENCLAW_DIR2 = path2.join(os2.homedir(), ".openclaw");
199
+ var CRON_DIR2 = path2.join(OPENCLAW_DIR2, "cron");
200
+ async function pack(agentId, outputPath) {
201
+ console.log("\n\u{1F338} openclaw-teleport \u2014 packing agent soul...\n");
202
+ const config = loadConfig();
203
+ const agent = findAgent(config, agentId);
204
+ console.log(`\u{1F4E6} Agent: ${agent.name} (${agent.id})`);
205
+ console.log(`\u{1F4C2} Workspace: ${agent.workspace}
206
+ `);
207
+ if (!fs2.existsSync(agent.workspace)) {
208
+ throw new Error(`\u274C Workspace not found: ${agent.workspace}`);
209
+ }
210
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
211
+ const soulName = `${agent.id}_${date}`;
212
+ const tmpDir = path2.join(os2.tmpdir(), `openclaw-teleport-${soulName}`);
213
+ const stageDir = path2.join(tmpDir, "soul");
214
+ if (fs2.existsSync(tmpDir)) {
215
+ fs2.rmSync(tmpDir, { recursive: true });
216
+ }
217
+ fs2.mkdirSync(stageDir, { recursive: true });
218
+ const allFiles = [];
219
+ console.log("\u{1F4DD} Collecting identity files...");
220
+ const mdFiles = collectMarkdownFiles(agent.workspace);
221
+ for (const f of mdFiles) {
222
+ const src = path2.join(agent.workspace, f);
223
+ const dst = path2.join(stageDir, "identity", f);
224
+ fs2.mkdirSync(path2.dirname(dst), { recursive: true });
225
+ fs2.copyFileSync(src, dst);
226
+ allFiles.push(`identity/${f}`);
227
+ }
228
+ console.log(` \u2705 ${mdFiles.length} markdown files`);
229
+ console.log("\u{1F9E0} Collecting memory...");
230
+ const memFiles = collectMemoryDir(agent.workspace);
231
+ for (const f of memFiles) {
232
+ const src = path2.join(agent.workspace, f);
233
+ const dst = path2.join(stageDir, f);
234
+ fs2.mkdirSync(path2.dirname(dst), { recursive: true });
235
+ fs2.copyFileSync(src, dst);
236
+ allFiles.push(f);
237
+ }
238
+ console.log(` \u2705 ${memFiles.length} memory files`);
239
+ console.log("\u{1F5C4}\uFE0F Collecting tool data...");
240
+ const dbFiles = collectDbFiles(agent.workspace);
241
+ for (const f of dbFiles) {
242
+ const src = path2.join(agent.workspace, f);
243
+ const dst = path2.join(stageDir, "data", f);
244
+ fs2.mkdirSync(path2.dirname(dst), { recursive: true });
245
+ fs2.copyFileSync(src, dst);
246
+ allFiles.push(`data/${f}`);
247
+ }
248
+ console.log(` \u2705 ${dbFiles.length} database files`);
249
+ console.log("\u2699\uFE0F Extracting agent config...");
250
+ const agentConfig = extractAgentConfig(config, agent.id);
251
+ const configPath = path2.join(stageDir, "config", "agent-config.json");
252
+ fs2.mkdirSync(path2.dirname(configPath), { recursive: true });
253
+ fs2.writeFileSync(configPath, JSON.stringify(agentConfig, null, 2));
254
+ allFiles.push("config/agent-config.json");
255
+ console.log(" \u2705 Agent config saved");
256
+ console.log("\u23F0 Collecting cron jobs...");
257
+ const cronFiles = collectCronFiles(agent.id);
258
+ for (const f of cronFiles) {
259
+ const src = path2.join(CRON_DIR2, f);
260
+ const dst = path2.join(stageDir, "cron", f);
261
+ fs2.mkdirSync(path2.dirname(dst), { recursive: true });
262
+ fs2.copyFileSync(src, dst);
263
+ allFiles.push(`cron/${f}`);
264
+ }
265
+ console.log(` \u2705 ${cronFiles.length} cron files`);
266
+ console.log("\u23F0 Extracting cron job definitions...");
267
+ const cronJobs = loadCronJobs(agent.id);
268
+ console.log(` \u2705 ${cronJobs.length} cron jobs for ${agent.id}`);
269
+ console.log("\u{1F419} Fetching GitHub repos...");
270
+ const repos = getGitHubRepos("kagura-agent");
271
+ console.log(` \u2705 ${repos.length} repos found`);
272
+ const services = detectServices(config);
273
+ console.log(`\u{1F517} Services to rebind: ${services.length > 0 ? services.join(", ") : "none"}`);
274
+ console.log("\u{1F511} Extracting channel credentials...");
275
+ const channelsConfig = extractChannelsConfig(config, agent.id);
276
+ const channelCount = Object.keys(channelsConfig).length;
277
+ console.log(` \u2705 ${channelCount} channel(s) saved`);
278
+ const agentDefaults = sanitizeAgentDefaults(config.agents?.defaults ?? {});
279
+ const modelsConfig = config.models ?? {};
280
+ const bindingsConfig = config.bindings ?? [];
281
+ const manifest = {
282
+ agent_id: agent.id,
283
+ agent_name: agent.name,
284
+ packed_at: (/* @__PURE__ */ new Date()).toISOString(),
285
+ files: allFiles,
286
+ github_repos: repos,
287
+ services_to_rebind: services,
288
+ channels: channelsConfig,
289
+ cron_jobs: cronJobs,
290
+ agent_defaults: agentDefaults,
291
+ models_config: modelsConfig,
292
+ bindings: bindingsConfig
293
+ };
294
+ const manifestPath = path2.join(stageDir, "manifest.json");
295
+ fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
296
+ const outputFile = outputPath ? path2.resolve(outputPath) : path2.resolve(`${soulName}.soul`);
297
+ console.log("\n\u{1F4E6} Packing soul archive...");
298
+ execSync2(`tar -czf "${outputFile}" -C "${tmpDir}" soul`, {
299
+ encoding: "utf-8"
300
+ });
301
+ fs2.rmSync(tmpDir, { recursive: true });
302
+ const stats = fs2.statSync(outputFile);
303
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
304
+ console.log("\n" + "\u2550".repeat(50));
305
+ console.log("\u{1F338} Soul packed successfully!");
306
+ console.log("\u2550".repeat(50));
307
+ console.log(`\u{1F4E6} File: ${outputFile}`);
308
+ console.log(`\u{1F4CF} Size: ${sizeMB} MB`);
309
+ console.log(`\u{1F194} Agent: ${agent.name} (${agent.id})`);
310
+ console.log(`\u{1F4DD} Files: ${allFiles.length}`);
311
+ console.log(`\u{1F419} Repos: ${repos.length}`);
312
+ console.log(`\u{1F517} Services: ${services.join(", ") || "none"}`);
313
+ console.log(`\u{1F511} Channels: ${channelCount}`);
314
+ console.log(`\u23F0 Cron: ${cronJobs.length} jobs`);
315
+ console.log(`\u{1F4C5} Packed: ${manifest.packed_at}`);
316
+ console.log("\u2550".repeat(50));
317
+ console.log("\n\u26A0\uFE0F SECURITY WARNING: The .soul file contains credentials");
318
+ console.log(" (API tokens, app secrets). Treat it like a password file.");
319
+ console.log(" Do NOT commit it to git or share publicly.\n");
320
+ }
321
+
322
+ // src/commands.ts
323
+ import * as fs3 from "node:fs";
324
+ import * as path3 from "node:path";
325
+ import * as os3 from "node:os";
326
+ import { execSync as execSync3 } from "node:child_process";
327
+ var OPENCLAW_DIR3 = path3.join(os3.homedir(), ".openclaw");
328
+ var CONFIG_PATH2 = path3.join(OPENCLAW_DIR3, "openclaw.json");
329
+ var CRON_DIR3 = path3.join(OPENCLAW_DIR3, "cron");
330
+ function extractManifest(soulFile) {
331
+ const tmpDir = path3.join(os3.tmpdir(), `soul-unpack-${Date.now()}`);
332
+ fs3.mkdirSync(tmpDir, { recursive: true });
333
+ execSync3(`tar -xzf "${path3.resolve(soulFile)}" -C "${tmpDir}"`, { encoding: "utf-8" });
334
+ const manifestPath = path3.join(tmpDir, "soul", "manifest.json");
335
+ if (!fs3.existsSync(manifestPath)) {
336
+ fs3.rmSync(tmpDir, { recursive: true });
337
+ throw new Error("\u274C Invalid .soul file: manifest.json not found");
338
+ }
339
+ const manifest = JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
340
+ return { tmpDir, manifest };
341
+ }
342
+ function ensureOpenClaw() {
343
+ console.log("\u{1F527} Checking OpenClaw installation...");
344
+ if (commandExists("openclaw")) {
345
+ try {
346
+ const version = execSync3("openclaw --version", { encoding: "utf-8", stdio: "pipe" }).trim();
347
+ console.log(` \u2705 OpenClaw found (${version})`);
348
+ } catch {
349
+ console.log(" \u2705 OpenClaw found");
350
+ }
351
+ return true;
352
+ }
353
+ console.log(" \u2B07\uFE0F OpenClaw not found, installing...");
354
+ try {
355
+ execSync3("npm install -g openclaw", {
356
+ encoding: "utf-8",
357
+ stdio: "pipe",
358
+ timeout: 12e4
359
+ });
360
+ if (commandExists("openclaw")) {
361
+ console.log(" \u2705 OpenClaw installed successfully");
362
+ return true;
363
+ } else {
364
+ console.log(" \u26A0\uFE0F Installation completed but openclaw command not found in PATH");
365
+ console.log(" Try: npm install -g openclaw");
366
+ return false;
367
+ }
368
+ } catch (err) {
369
+ console.log(" \u26A0\uFE0F Failed to install OpenClaw automatically");
370
+ console.log(" Run manually: npm install -g openclaw");
371
+ return false;
372
+ }
373
+ }
374
+ function writeAgentConfig(manifest, stageDir, targetWorkspace) {
375
+ console.log("\u2699\uFE0F Writing agent configuration...");
376
+ fs3.mkdirSync(OPENCLAW_DIR3, { recursive: true });
377
+ const agentConfigPath = path3.join(stageDir, "config", "agent-config.json");
378
+ if (!fs3.existsSync(agentConfigPath)) {
379
+ console.log(" \u26A0\uFE0F No agent config in archive, skipping");
380
+ return;
381
+ }
382
+ const agentConfig = JSON.parse(fs3.readFileSync(agentConfigPath, "utf-8"));
383
+ const agentDir = path3.join(OPENCLAW_DIR3, "agents", manifest.agent_id, "agent");
384
+ const savedAgent = agentConfig.agent ?? {};
385
+ delete savedAgent.workspace;
386
+ delete savedAgent.agentDir;
387
+ const newAgent = {
388
+ id: manifest.agent_id,
389
+ name: manifest.agent_name,
390
+ ...savedAgent,
391
+ // Set paths dynamically for the new machine
392
+ workspace: targetWorkspace,
393
+ agentDir
394
+ };
395
+ if (fs3.existsSync(CONFIG_PATH2)) {
396
+ const existingConfig = JSON.parse(fs3.readFileSync(CONFIG_PATH2, "utf-8"));
397
+ if (!existingConfig.agents) {
398
+ existingConfig.agents = { list: [] };
399
+ }
400
+ if (!existingConfig.agents.list) {
401
+ existingConfig.agents.list = [];
402
+ }
403
+ const existingIdx = existingConfig.agents.list.findIndex(
404
+ (a) => a.id === manifest.agent_id
405
+ );
406
+ if (existingIdx >= 0) {
407
+ existingConfig.agents.list[existingIdx] = newAgent;
408
+ console.log(" \u2705 Agent config updated (merged into existing)");
409
+ } else {
410
+ existingConfig.agents.list.push(newAgent);
411
+ console.log(" \u2705 Agent config added to existing openclaw.json");
412
+ }
413
+ if (manifest.agent_defaults && Object.keys(manifest.agent_defaults).length > 0) {
414
+ if (!existingConfig.agents.defaults) {
415
+ existingConfig.agents.defaults = {};
416
+ }
417
+ existingConfig.agents.defaults = {
418
+ ...existingConfig.agents.defaults,
419
+ ...manifest.agent_defaults,
420
+ workspace: targetWorkspace
421
+ };
422
+ console.log(" \u2705 Agent defaults merged");
423
+ }
424
+ if (manifest.channels && Object.keys(manifest.channels).length > 0) {
425
+ if (!existingConfig.channels) {
426
+ existingConfig.channels = {};
427
+ }
428
+ for (const [key, val] of Object.entries(manifest.channels)) {
429
+ if (!(key in existingConfig.channels)) {
430
+ existingConfig.channels[key] = val;
431
+ console.log(` \u2705 Channel '${key}' config added`);
432
+ } else {
433
+ console.log(` \u23ED\uFE0F Channel '${key}' already exists, skipping`);
434
+ }
435
+ }
436
+ }
437
+ if (manifest.models_config && Object.keys(manifest.models_config).length > 0) {
438
+ if (!existingConfig.models) {
439
+ existingConfig.models = manifest.models_config;
440
+ console.log(" \u2705 Models config restored");
441
+ } else {
442
+ console.log(" \u23ED\uFE0F Models config already exists, skipping");
443
+ }
444
+ }
445
+ if (manifest.bindings && manifest.bindings.length > 0) {
446
+ if (!existingConfig.bindings || existingConfig.bindings.length === 0) {
447
+ existingConfig.bindings = manifest.bindings;
448
+ console.log(" \u2705 Bindings restored");
449
+ } else {
450
+ console.log(" \u23ED\uFE0F Bindings already exist, skipping");
451
+ }
452
+ }
453
+ fs3.writeFileSync(CONFIG_PATH2, JSON.stringify(existingConfig, null, 2));
454
+ } else {
455
+ const newConfig = {
456
+ agents: {
457
+ defaults: {
458
+ ...manifest.agent_defaults ?? {},
459
+ workspace: targetWorkspace
460
+ },
461
+ list: [newAgent]
462
+ }
463
+ };
464
+ if (manifest.channels && Object.keys(manifest.channels).length > 0) {
465
+ newConfig.channels = manifest.channels;
466
+ console.log(" \u2705 Channel configs restored");
467
+ }
468
+ if (manifest.models_config && Object.keys(manifest.models_config).length > 0) {
469
+ newConfig.models = manifest.models_config;
470
+ console.log(" \u2705 Models config restored");
471
+ }
472
+ if (manifest.bindings && manifest.bindings.length > 0) {
473
+ newConfig.bindings = manifest.bindings;
474
+ console.log(" \u2705 Bindings restored");
475
+ }
476
+ fs3.writeFileSync(CONFIG_PATH2, JSON.stringify(newConfig, null, 2));
477
+ console.log(" \u2705 New openclaw.json created");
478
+ }
479
+ fs3.mkdirSync(agentDir, { recursive: true });
480
+ }
481
+ function restoreCronJobs(manifest, stageDir) {
482
+ console.log("\u23F0 Restoring cron jobs...");
483
+ const cronDir = path3.join(stageDir, "cron");
484
+ let cronFileCount = 0;
485
+ if (fs3.existsSync(cronDir)) {
486
+ fs3.mkdirSync(CRON_DIR3, { recursive: true });
487
+ const files = fs3.readdirSync(cronDir);
488
+ for (const f of files) {
489
+ fs3.copyFileSync(path3.join(cronDir, f), path3.join(CRON_DIR3, f));
490
+ cronFileCount++;
491
+ }
492
+ }
493
+ if (manifest.cron_jobs && manifest.cron_jobs.length > 0) {
494
+ fs3.mkdirSync(CRON_DIR3, { recursive: true });
495
+ const jobsPath = path3.join(CRON_DIR3, "jobs.json");
496
+ let existingJobs = [];
497
+ if (fs3.existsSync(jobsPath)) {
498
+ try {
499
+ const data = JSON.parse(fs3.readFileSync(jobsPath, "utf-8"));
500
+ existingJobs = data.jobs ?? [];
501
+ } catch {
502
+ existingJobs = [];
503
+ }
504
+ }
505
+ for (const job of manifest.cron_jobs) {
506
+ const idx = existingJobs.findIndex((j) => j.id === job.id);
507
+ if (idx >= 0) {
508
+ existingJobs[idx] = job;
509
+ } else {
510
+ existingJobs.push(job);
511
+ }
512
+ }
513
+ fs3.writeFileSync(jobsPath, JSON.stringify({ version: 1, jobs: existingJobs }, null, 2));
514
+ console.log(` \u2705 ${manifest.cron_jobs.length} cron job(s) restored`);
515
+ } else if (cronFileCount > 0) {
516
+ console.log(` \u2705 ${cronFileCount} cron file(s) restored`);
517
+ } else {
518
+ console.log(" (none)");
519
+ }
520
+ return manifest.cron_jobs?.length ?? cronFileCount;
521
+ }
522
+ function cloneGitHubRepos(manifest, targetWorkspace) {
523
+ const result = { cloned: 0, skipped: 0, failed: 0 };
524
+ if (!manifest.github_repos || manifest.github_repos.length === 0) {
525
+ return result;
526
+ }
527
+ console.log("\n\u{1F419} Cloning GitHub repos...");
528
+ if (!commandExists("gh")) {
529
+ console.log(" \u26A0\uFE0F GitHub CLI (gh) not installed");
530
+ console.log(" Install it: https://cli.github.com/");
531
+ console.log(" Then run: gh auth login");
532
+ console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
533
+ for (const repo of manifest.github_repos) {
534
+ console.log(` git clone ${repo.url}`);
535
+ }
536
+ result.failed = manifest.github_repos.length;
537
+ return result;
538
+ }
539
+ if (!isGhAuthenticated()) {
540
+ console.log(" \u26A0\uFE0F GitHub CLI not authenticated");
541
+ console.log("");
542
+ console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
543
+ console.log(" \u2502 Please run: gh auth login \u2502");
544
+ console.log(" \u2502 \u2502");
545
+ console.log(" \u2502 Then re-run unpack, or clone manually: \u2502");
546
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
547
+ console.log("");
548
+ for (const repo of manifest.github_repos) {
549
+ const fork = repo.isFork ? " (fork)" : "";
550
+ console.log(` \u2022 ${repo.name}${fork}: ${repo.url}`);
551
+ }
552
+ result.failed = manifest.github_repos.length;
553
+ return result;
554
+ }
555
+ for (const repo of manifest.github_repos) {
556
+ const targetDir = repo.isFork ? path3.join(targetWorkspace, "forks", repo.name) : path3.join(targetWorkspace, repo.name);
557
+ if (fs3.existsSync(targetDir)) {
558
+ console.log(` \u23ED\uFE0F ${repo.name} (already exists)`);
559
+ result.skipped++;
560
+ continue;
561
+ }
562
+ try {
563
+ fs3.mkdirSync(path3.dirname(targetDir), { recursive: true });
564
+ console.log(` \u{1F4E5} Cloning ${repo.name}${repo.isFork ? " (fork)" : ""}...`);
565
+ execSync3(`gh repo clone "${repo.url}" "${targetDir}"`, {
566
+ encoding: "utf-8",
567
+ timeout: 12e4,
568
+ stdio: "pipe"
569
+ });
570
+ console.log(` \u2705 ${repo.name}`);
571
+ result.cloned++;
572
+ } catch {
573
+ console.log(` \u26A0\uFE0F Failed to clone ${repo.name}`);
574
+ result.failed++;
575
+ }
576
+ }
577
+ return result;
578
+ }
579
+ function startGateway() {
580
+ console.log("\n\u{1F680} Starting OpenClaw Gateway...");
581
+ if (!commandExists("openclaw")) {
582
+ console.log(" \u26A0\uFE0F openclaw command not found, skipping gateway start");
583
+ return false;
584
+ }
585
+ try {
586
+ const output = execSync3("openclaw gateway start", {
587
+ encoding: "utf-8",
588
+ timeout: 3e4,
589
+ stdio: "pipe"
590
+ });
591
+ console.log(" \u2705 Gateway started");
592
+ if (output.trim()) {
593
+ const lines = output.trim().split("\n").slice(0, 3);
594
+ for (const line of lines) {
595
+ console.log(` ${line}`);
596
+ }
597
+ }
598
+ return true;
599
+ } catch (err) {
600
+ console.log(" \u26A0\uFE0F Failed to start gateway");
601
+ if (err instanceof Error && "stderr" in err) {
602
+ const stderr = err.stderr?.trim();
603
+ if (stderr) {
604
+ console.log(` ${stderr.split("\n")[0]}`);
605
+ }
606
+ }
607
+ console.log(" Try manually: openclaw gateway start");
608
+ return false;
609
+ }
610
+ }
611
+ async function unpack(soulFile, workspacePath) {
612
+ console.log("\n\u{1F338} openclaw-teleport \u2014 unpacking agent soul...\n");
613
+ if (!fs3.existsSync(soulFile)) {
614
+ throw new Error(`\u274C File not found: ${soulFile}`);
615
+ }
616
+ const { tmpDir, manifest } = extractManifest(soulFile);
617
+ const stageDir = path3.join(tmpDir, "soul");
618
+ console.log(`\u{1F194} Agent: ${manifest.agent_name} (${manifest.agent_id})`);
619
+ console.log(`\u{1F4C5} Packed: ${manifest.packed_at}`);
620
+ console.log(`\u{1F4DD} Files: ${manifest.files.length}`);
621
+ console.log("");
622
+ const openclawInstalled = ensureOpenClaw();
623
+ const targetWorkspace = workspacePath ? path3.resolve(workspacePath) : path3.join(OPENCLAW_DIR3, "workspace");
624
+ fs3.mkdirSync(targetWorkspace, { recursive: true });
625
+ console.log("\n\u{1F4DD} Restoring identity files...");
626
+ let identityCount = 0;
627
+ const identityDir = path3.join(stageDir, "identity");
628
+ if (fs3.existsSync(identityDir)) {
629
+ const files = fs3.readdirSync(identityDir);
630
+ for (const f of files) {
631
+ const src = path3.join(identityDir, f);
632
+ const dst = path3.join(targetWorkspace, f);
633
+ fs3.copyFileSync(src, dst);
634
+ console.log(` \u2705 ${f}`);
635
+ identityCount++;
636
+ }
637
+ }
638
+ console.log("\u{1F9E0} Restoring memory...");
639
+ let memoryCount = 0;
640
+ const memoryDir = path3.join(stageDir, "memory");
641
+ if (fs3.existsSync(memoryDir)) {
642
+ const copyRecursive = (src, dst) => {
643
+ fs3.mkdirSync(dst, { recursive: true });
644
+ const entries = fs3.readdirSync(src, { withFileTypes: true });
645
+ for (const entry of entries) {
646
+ const srcPath = path3.join(src, entry.name);
647
+ const dstPath = path3.join(dst, entry.name);
648
+ if (entry.isDirectory()) {
649
+ copyRecursive(srcPath, dstPath);
650
+ } else {
651
+ fs3.copyFileSync(srcPath, dstPath);
652
+ memoryCount++;
653
+ }
654
+ }
655
+ };
656
+ copyRecursive(memoryDir, path3.join(targetWorkspace, "memory"));
657
+ console.log(` \u2705 ${memoryCount} memory files restored`);
658
+ }
659
+ console.log("\u{1F5C4}\uFE0F Restoring tool data...");
660
+ let dataCount = 0;
661
+ const dataDir = path3.join(stageDir, "data");
662
+ if (fs3.existsSync(dataDir)) {
663
+ const copyRecursive = (src, dst) => {
664
+ fs3.mkdirSync(dst, { recursive: true });
665
+ const entries = fs3.readdirSync(src, { withFileTypes: true });
666
+ for (const entry of entries) {
667
+ const srcPath = path3.join(src, entry.name);
668
+ const dstPath = path3.join(dst, entry.name);
669
+ if (entry.isDirectory()) {
670
+ copyRecursive(srcPath, dstPath);
671
+ } else {
672
+ fs3.copyFileSync(srcPath, dstPath);
673
+ console.log(` \u2705 ${entry.name}`);
674
+ dataCount++;
675
+ }
676
+ }
677
+ };
678
+ copyRecursive(dataDir, targetWorkspace);
679
+ }
680
+ writeAgentConfig(manifest, stageDir, targetWorkspace);
681
+ const cronCount = restoreCronJobs(manifest, stageDir);
682
+ const repoResult = cloneGitHubRepos(manifest, targetWorkspace);
683
+ fs3.rmSync(tmpDir, { recursive: true });
684
+ let gatewayStarted = false;
685
+ if (openclawInstalled) {
686
+ gatewayStarted = startGateway();
687
+ }
688
+ const configuredServices = [];
689
+ if (manifest.channels) {
690
+ for (const [key, val] of Object.entries(manifest.channels)) {
691
+ if (val && typeof val === "object" && val.enabled !== false) {
692
+ configuredServices.push(key);
693
+ }
694
+ }
695
+ }
696
+ console.log("\n" + "\u2550".repeat(50));
697
+ console.log("\u{1F338} Restoration Summary");
698
+ console.log("\u2550".repeat(50));
699
+ console.log(`\u{1F194} Agent: ${manifest.agent_name} (${manifest.agent_id})`);
700
+ console.log(`\u{1F4C2} Workspace: ${targetWorkspace}`);
701
+ console.log(`\u{1F4DD} Files: ${identityCount} identity + ${memoryCount} memory + ${dataCount} data`);
702
+ console.log(`\u23F0 Cron: ${cronCount} job(s)`);
703
+ if (manifest.github_repos && manifest.github_repos.length > 0) {
704
+ console.log(`\u{1F419} Repos: ${repoResult.cloned} cloned, ${repoResult.skipped} skipped, ${repoResult.failed} failed`);
705
+ }
706
+ if (configuredServices.length > 0) {
707
+ console.log(`\u{1F517} Services: ${configuredServices.join(", ")}`);
708
+ }
709
+ console.log(`\u{1F527} OpenClaw: ${openclawInstalled ? "\u2705" : "\u26A0\uFE0F needs install"}`);
710
+ console.log(`\u{1F680} Gateway: ${gatewayStarted ? "\u2705 running" : "\u26A0\uFE0F not started"}`);
711
+ if (manifest.services_to_rebind && manifest.services_to_rebind.length > 0) {
712
+ const needsRebind = manifest.services_to_rebind.filter(
713
+ (s) => !configuredServices.includes(s)
714
+ );
715
+ if (needsRebind.length > 0) {
716
+ console.log("\n\u{1F517} Services that may need attention:");
717
+ for (const svc of needsRebind) {
718
+ console.log(` \u2610 ${svc}`);
719
+ }
720
+ }
721
+ }
722
+ console.log("\n" + "\u2550".repeat(50));
723
+ console.log(`Welcome back, ${manifest.agent_name} \u{1F338}`);
724
+ console.log("\u2550".repeat(50) + "\n");
725
+ }
726
+ async function inspect(soulFile) {
727
+ if (!fs3.existsSync(soulFile)) {
728
+ throw new Error(`\u274C File not found: ${soulFile}`);
729
+ }
730
+ const tmpDir = path3.join(os3.tmpdir(), `soul-inspect-${Date.now()}`);
731
+ fs3.mkdirSync(tmpDir, { recursive: true });
732
+ try {
733
+ execSync3(`tar -xzf "${path3.resolve(soulFile)}" -C "${tmpDir}" soul/manifest.json`, {
734
+ encoding: "utf-8"
735
+ });
736
+ const manifestPath = path3.join(tmpDir, "soul", "manifest.json");
737
+ if (!fs3.existsSync(manifestPath)) {
738
+ throw new Error("\u274C Invalid .soul file: manifest.json not found");
739
+ }
740
+ const manifest = JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
741
+ const stats = fs3.statSync(path3.resolve(soulFile));
742
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
743
+ console.log("\n" + "\u2550".repeat(50));
744
+ console.log("\u{1F338} Soul Archive Inspection");
745
+ console.log("\u2550".repeat(50));
746
+ console.log(`\u{1F194} Agent: ${manifest.agent_name} (${manifest.agent_id})`);
747
+ console.log(`\u{1F4C5} Packed: ${manifest.packed_at}`);
748
+ console.log(`\u{1F4CF} Size: ${sizeMB} MB`);
749
+ console.log(`\u{1F4DD} Files: ${manifest.files.length}`);
750
+ if (manifest.github_repos.length > 0) {
751
+ console.log(`
752
+ \u{1F419} GitHub Repos (${manifest.github_repos.length}):`);
753
+ for (const repo of manifest.github_repos) {
754
+ const fork = repo.isFork ? " (fork)" : "";
755
+ console.log(` \u2022 ${repo.name}${fork}`);
756
+ console.log(` ${repo.url}`);
757
+ }
758
+ }
759
+ if (manifest.channels && Object.keys(manifest.channels).length > 0) {
760
+ console.log(`
761
+ \u{1F511} Channels (${Object.keys(manifest.channels).length}):`);
762
+ for (const key of Object.keys(manifest.channels)) {
763
+ console.log(` \u2022 ${key}`);
764
+ }
765
+ }
766
+ if (manifest.cron_jobs && manifest.cron_jobs.length > 0) {
767
+ console.log(`
768
+ \u23F0 Cron Jobs (${manifest.cron_jobs.length}):`);
769
+ for (const job of manifest.cron_jobs) {
770
+ const status = job.enabled ? "\u{1F7E2}" : "\u{1F534}";
771
+ console.log(` ${status} ${job.name}`);
772
+ }
773
+ }
774
+ if (manifest.services_to_rebind.length > 0) {
775
+ console.log(`
776
+ \u{1F517} Services to rebind:`);
777
+ for (const svc of manifest.services_to_rebind) {
778
+ console.log(` \u2022 ${svc}`);
779
+ }
780
+ }
781
+ const identityFiles = manifest.files.filter((f) => f.startsWith("identity/"));
782
+ const memoryFiles = manifest.files.filter((f) => f.startsWith("memory/"));
783
+ const dataFiles = manifest.files.filter((f) => f.startsWith("data/"));
784
+ const cronFiles = manifest.files.filter((f) => f.startsWith("cron/"));
785
+ const configFiles = manifest.files.filter((f) => f.startsWith("config/"));
786
+ console.log("\n\u{1F4CA} Contents breakdown:");
787
+ if (identityFiles.length > 0) console.log(` \u{1F4DD} Identity: ${identityFiles.length} files`);
788
+ if (memoryFiles.length > 0) console.log(` \u{1F9E0} Memory: ${memoryFiles.length} files`);
789
+ if (dataFiles.length > 0) console.log(` \u{1F5C4}\uFE0F Data: ${dataFiles.length} files`);
790
+ if (cronFiles.length > 0) console.log(` \u23F0 Cron: ${cronFiles.length} files`);
791
+ if (configFiles.length > 0) console.log(` \u2699\uFE0F Config: ${configFiles.length} files`);
792
+ console.log("\u2550".repeat(50) + "\n");
793
+ } finally {
794
+ fs3.rmSync(tmpDir, { recursive: true });
795
+ }
796
+ }
797
+
798
+ // src/cli.ts
799
+ var program = new Command();
800
+ program.name("openclaw-teleport").description("\u{1F338} Agent soul migration \u2014 pack your identity, memory, and tools into one file").version("0.2.0");
801
+ program.command("pack").description("Pack an agent into a .soul archive").argument("[agent-id]", "Agent ID to pack (defaults to first configured agent)").option("-o, --output <path>", "Output file path (default: ./{agent}_{date}.soul)").action(async (agentId, opts) => {
802
+ try {
803
+ await pack(agentId, opts.output);
804
+ } catch (err) {
805
+ console.error(`
806
+ ${err instanceof Error ? err.message : String(err)}
807
+ `);
808
+ process.exit(1);
809
+ }
810
+ });
811
+ program.command("unpack").description("Unpack a .soul archive and restore the agent").argument("<file>", "Path to .soul file").option("-w, --workspace <path>", "Target workspace directory").action(async (file, opts) => {
812
+ try {
813
+ await unpack(file, opts.workspace);
814
+ } catch (err) {
815
+ console.error(`
816
+ ${err instanceof Error ? err.message : String(err)}
817
+ `);
818
+ process.exit(1);
819
+ }
820
+ });
821
+ program.command("inspect").description("Inspect a .soul archive without unpacking").argument("<file>", "Path to .soul file").action(async (file) => {
822
+ try {
823
+ await inspect(file);
824
+ } catch (err) {
825
+ console.error(`
826
+ ${err instanceof Error ? err.message : String(err)}
827
+ `);
828
+ process.exit(1);
829
+ }
830
+ });
831
+ program.parse();