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/README.md +189 -0
- package/README.zh.md +109 -0
- package/dist/cli.mjs +831 -0
- package/package.json +31 -0
- package/src/cli.ts +53 -0
- package/src/commands.ts +613 -0
- package/src/pack.ts +184 -0
- package/src/utils.ts +311 -0
- package/tsconfig.json +18 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-teleport",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Agent soul migration ā pack your identity, memory, and tools into one file, unpack on a new machine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-teleport": "./dist/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.mjs --banner:js=\"#!/usr/bin/env node\" --external:commander",
|
|
11
|
+
"dev": "tsx src/cli.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"kagura-project",
|
|
16
|
+
"ai-agent",
|
|
17
|
+
"migration",
|
|
18
|
+
"openclaw"
|
|
19
|
+
],
|
|
20
|
+
"author": "kagura-agent",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^13.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"esbuild": "^0.25.0",
|
|
27
|
+
"tsx": "^4.19.0",
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"@types/node": "^22.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { pack } from './pack.js';
|
|
3
|
+
import { unpack, inspect } from './commands.js';
|
|
4
|
+
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('openclaw-teleport')
|
|
9
|
+
.description('šø Agent soul migration ā pack your identity, memory, and tools into one file')
|
|
10
|
+
.version('0.2.0');
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.command('pack')
|
|
14
|
+
.description('Pack an agent into a .soul archive')
|
|
15
|
+
.argument('[agent-id]', 'Agent ID to pack (defaults to first configured agent)')
|
|
16
|
+
.option('-o, --output <path>', 'Output file path (default: ./{agent}_{date}.soul)')
|
|
17
|
+
.action(async (agentId: string | undefined, opts: { output?: string }) => {
|
|
18
|
+
try {
|
|
19
|
+
await pack(agentId, opts.output);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(`\n${err instanceof Error ? err.message : String(err)}\n`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('unpack')
|
|
28
|
+
.description('Unpack a .soul archive and restore the agent')
|
|
29
|
+
.argument('<file>', 'Path to .soul file')
|
|
30
|
+
.option('-w, --workspace <path>', 'Target workspace directory')
|
|
31
|
+
.action(async (file: string, opts: { workspace?: string }) => {
|
|
32
|
+
try {
|
|
33
|
+
await unpack(file, opts.workspace);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`\n${err instanceof Error ? err.message : String(err)}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('inspect')
|
|
42
|
+
.description('Inspect a .soul archive without unpacking')
|
|
43
|
+
.argument('<file>', 'Path to .soul file')
|
|
44
|
+
.action(async (file: string) => {
|
|
45
|
+
try {
|
|
46
|
+
await inspect(file);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`\n${err instanceof Error ? err.message : String(err)}\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program.parse();
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
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 { loadConfig, commandExists, isGhAuthenticated, type Manifest, type OpenClawConfig, type CronJob } from './utils.js';
|
|
6
|
+
|
|
7
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
8
|
+
const CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
9
|
+
const CRON_DIR = path.join(OPENCLAW_DIR, 'cron');
|
|
10
|
+
|
|
11
|
+
function extractManifest(soulFile: string): { tmpDir: string; manifest: Manifest } {
|
|
12
|
+
const tmpDir = path.join(os.tmpdir(), `soul-unpack-${Date.now()}`);
|
|
13
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
14
|
+
|
|
15
|
+
execSync(`tar -xzf "${path.resolve(soulFile)}" -C "${tmpDir}"`, { encoding: 'utf-8' });
|
|
16
|
+
|
|
17
|
+
const manifestPath = path.join(tmpDir, 'soul', 'manifest.json');
|
|
18
|
+
if (!fs.existsSync(manifestPath)) {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
20
|
+
throw new Error('ā Invalid .soul file: manifest.json not found');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
24
|
+
return { tmpDir, manifest };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// āā Step 1: Install OpenClaw āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
28
|
+
|
|
29
|
+
function ensureOpenClaw(): boolean {
|
|
30
|
+
console.log('š§ Checking OpenClaw installation...');
|
|
31
|
+
|
|
32
|
+
if (commandExists('openclaw')) {
|
|
33
|
+
try {
|
|
34
|
+
const version = execSync('openclaw --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
35
|
+
console.log(` ā
OpenClaw found (${version})`);
|
|
36
|
+
} catch {
|
|
37
|
+
console.log(' ā
OpenClaw found');
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(' ā¬ļø OpenClaw not found, installing...');
|
|
43
|
+
try {
|
|
44
|
+
execSync('npm install -g openclaw', {
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
stdio: 'pipe',
|
|
47
|
+
timeout: 120000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Verify installation
|
|
51
|
+
if (commandExists('openclaw')) {
|
|
52
|
+
console.log(' ā
OpenClaw installed successfully');
|
|
53
|
+
return true;
|
|
54
|
+
} else {
|
|
55
|
+
console.log(' ā ļø Installation completed but openclaw command not found in PATH');
|
|
56
|
+
console.log(' Try: npm install -g openclaw');
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.log(' ā ļø Failed to install OpenClaw automatically');
|
|
61
|
+
console.log(' Run manually: npm install -g openclaw');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// āā Step 2: Write full config āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
67
|
+
|
|
68
|
+
function writeAgentConfig(
|
|
69
|
+
manifest: Manifest,
|
|
70
|
+
stageDir: string,
|
|
71
|
+
targetWorkspace: string
|
|
72
|
+
): void {
|
|
73
|
+
console.log('āļø Writing agent configuration...');
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const agentConfigPath = path.join(stageDir, 'config', 'agent-config.json');
|
|
78
|
+
if (!fs.existsSync(agentConfigPath)) {
|
|
79
|
+
console.log(' ā ļø No agent config in archive, skipping');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const agentConfig = JSON.parse(fs.readFileSync(agentConfigPath, 'utf-8'));
|
|
84
|
+
|
|
85
|
+
// Build the new agent entry with dynamic paths
|
|
86
|
+
const agentDir = path.join(OPENCLAW_DIR, 'agents', manifest.agent_id, 'agent');
|
|
87
|
+
const savedAgent = agentConfig.agent ?? {};
|
|
88
|
+
// Remove old paths from saved config before merging
|
|
89
|
+
delete savedAgent.workspace;
|
|
90
|
+
delete savedAgent.agentDir;
|
|
91
|
+
const newAgent = {
|
|
92
|
+
id: manifest.agent_id,
|
|
93
|
+
name: manifest.agent_name,
|
|
94
|
+
...savedAgent,
|
|
95
|
+
// Set paths dynamically for the new machine
|
|
96
|
+
workspace: targetWorkspace,
|
|
97
|
+
agentDir: agentDir,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
101
|
+
// Merge into existing config
|
|
102
|
+
const existingConfig: OpenClawConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
103
|
+
|
|
104
|
+
if (!existingConfig.agents) {
|
|
105
|
+
existingConfig.agents = { list: [] };
|
|
106
|
+
}
|
|
107
|
+
if (!existingConfig.agents.list) {
|
|
108
|
+
existingConfig.agents.list = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const existingIdx = existingConfig.agents.list.findIndex(
|
|
112
|
+
(a) => a.id === manifest.agent_id
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (existingIdx >= 0) {
|
|
116
|
+
existingConfig.agents.list[existingIdx] = newAgent;
|
|
117
|
+
console.log(' ā
Agent config updated (merged into existing)');
|
|
118
|
+
} else {
|
|
119
|
+
existingConfig.agents.list.push(newAgent);
|
|
120
|
+
console.log(' ā
Agent config added to existing openclaw.json');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Merge agent defaults if present in manifest
|
|
124
|
+
if (manifest.agent_defaults && Object.keys(manifest.agent_defaults).length > 0) {
|
|
125
|
+
if (!existingConfig.agents.defaults) {
|
|
126
|
+
existingConfig.agents.defaults = {};
|
|
127
|
+
}
|
|
128
|
+
// Merge defaults, setting workspace dynamically
|
|
129
|
+
existingConfig.agents.defaults = {
|
|
130
|
+
...existingConfig.agents.defaults,
|
|
131
|
+
...manifest.agent_defaults,
|
|
132
|
+
workspace: targetWorkspace,
|
|
133
|
+
};
|
|
134
|
+
console.log(' ā
Agent defaults merged');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Merge channels config if present
|
|
138
|
+
if (manifest.channels && Object.keys(manifest.channels).length > 0) {
|
|
139
|
+
if (!existingConfig.channels) {
|
|
140
|
+
existingConfig.channels = {};
|
|
141
|
+
}
|
|
142
|
+
for (const [key, val] of Object.entries(manifest.channels)) {
|
|
143
|
+
if (!(key in existingConfig.channels)) {
|
|
144
|
+
(existingConfig.channels as Record<string, unknown>)[key] = val;
|
|
145
|
+
console.log(` ā
Channel '${key}' config added`);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(` āļø Channel '${key}' already exists, skipping`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Merge models config if not present
|
|
153
|
+
if (manifest.models_config && Object.keys(manifest.models_config).length > 0) {
|
|
154
|
+
if (!existingConfig.models) {
|
|
155
|
+
existingConfig.models = manifest.models_config;
|
|
156
|
+
console.log(' ā
Models config restored');
|
|
157
|
+
} else {
|
|
158
|
+
console.log(' āļø Models config already exists, skipping');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Merge bindings
|
|
163
|
+
if (manifest.bindings && manifest.bindings.length > 0) {
|
|
164
|
+
if (!existingConfig.bindings || (existingConfig.bindings as unknown[]).length === 0) {
|
|
165
|
+
existingConfig.bindings = manifest.bindings;
|
|
166
|
+
console.log(' ā
Bindings restored');
|
|
167
|
+
} else {
|
|
168
|
+
console.log(' āļø Bindings already exist, skipping');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(existingConfig, null, 2));
|
|
173
|
+
} else {
|
|
174
|
+
// Create new config from scratch
|
|
175
|
+
const newConfig: OpenClawConfig = {
|
|
176
|
+
agents: {
|
|
177
|
+
defaults: {
|
|
178
|
+
...(manifest.agent_defaults ?? {}),
|
|
179
|
+
workspace: targetWorkspace,
|
|
180
|
+
},
|
|
181
|
+
list: [newAgent],
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Add channels
|
|
186
|
+
if (manifest.channels && Object.keys(manifest.channels).length > 0) {
|
|
187
|
+
newConfig.channels = manifest.channels;
|
|
188
|
+
console.log(' ā
Channel configs restored');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Add models
|
|
192
|
+
if (manifest.models_config && Object.keys(manifest.models_config).length > 0) {
|
|
193
|
+
newConfig.models = manifest.models_config;
|
|
194
|
+
console.log(' ā
Models config restored');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add bindings
|
|
198
|
+
if (manifest.bindings && manifest.bindings.length > 0) {
|
|
199
|
+
newConfig.bindings = manifest.bindings;
|
|
200
|
+
console.log(' ā
Bindings restored');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(newConfig, null, 2));
|
|
204
|
+
console.log(' ā
New openclaw.json created');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Ensure agent directory exists
|
|
208
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// āā Step 3: Restore cron jobs āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
212
|
+
|
|
213
|
+
function restoreCronJobs(manifest: Manifest, stageDir: string): number {
|
|
214
|
+
console.log('ā° Restoring cron jobs...');
|
|
215
|
+
|
|
216
|
+
// Restore cron files from archive
|
|
217
|
+
const cronDir = path.join(stageDir, 'cron');
|
|
218
|
+
let cronFileCount = 0;
|
|
219
|
+
if (fs.existsSync(cronDir)) {
|
|
220
|
+
fs.mkdirSync(CRON_DIR, { recursive: true });
|
|
221
|
+
const files = fs.readdirSync(cronDir);
|
|
222
|
+
for (const f of files) {
|
|
223
|
+
fs.copyFileSync(path.join(cronDir, f), path.join(CRON_DIR, f));
|
|
224
|
+
cronFileCount++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// If manifest has full cron_jobs content, merge them into jobs.json
|
|
229
|
+
if (manifest.cron_jobs && manifest.cron_jobs.length > 0) {
|
|
230
|
+
fs.mkdirSync(CRON_DIR, { recursive: true });
|
|
231
|
+
const jobsPath = path.join(CRON_DIR, 'jobs.json');
|
|
232
|
+
|
|
233
|
+
let existingJobs: CronJob[] = [];
|
|
234
|
+
if (fs.existsSync(jobsPath)) {
|
|
235
|
+
try {
|
|
236
|
+
const data = JSON.parse(fs.readFileSync(jobsPath, 'utf-8'));
|
|
237
|
+
existingJobs = data.jobs ?? [];
|
|
238
|
+
} catch {
|
|
239
|
+
existingJobs = [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Merge: replace jobs with same ID, add new ones
|
|
244
|
+
for (const job of manifest.cron_jobs) {
|
|
245
|
+
const idx = existingJobs.findIndex((j) => j.id === job.id);
|
|
246
|
+
if (idx >= 0) {
|
|
247
|
+
existingJobs[idx] = job;
|
|
248
|
+
} else {
|
|
249
|
+
existingJobs.push(job);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fs.writeFileSync(jobsPath, JSON.stringify({ version: 1, jobs: existingJobs }, null, 2));
|
|
254
|
+
console.log(` ā
${manifest.cron_jobs.length} cron job(s) restored`);
|
|
255
|
+
} else if (cronFileCount > 0) {
|
|
256
|
+
console.log(` ā
${cronFileCount} cron file(s) restored`);
|
|
257
|
+
} else {
|
|
258
|
+
console.log(' (none)');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return manifest.cron_jobs?.length ?? cronFileCount;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// āā Step 4 & 5: GitHub auth + clone repos āāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
265
|
+
|
|
266
|
+
function cloneGitHubRepos(manifest: Manifest, targetWorkspace: string): { cloned: number; skipped: number; failed: number } {
|
|
267
|
+
const result = { cloned: 0, skipped: 0, failed: 0 };
|
|
268
|
+
|
|
269
|
+
if (!manifest.github_repos || manifest.github_repos.length === 0) {
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log('\nš Cloning GitHub repos...');
|
|
274
|
+
|
|
275
|
+
// Check if gh CLI is available
|
|
276
|
+
if (!commandExists('gh')) {
|
|
277
|
+
console.log(' ā ļø GitHub CLI (gh) not installed');
|
|
278
|
+
console.log(' Install it: https://cli.github.com/');
|
|
279
|
+
console.log(' Then run: gh auth login');
|
|
280
|
+
console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
|
|
281
|
+
for (const repo of manifest.github_repos) {
|
|
282
|
+
console.log(` git clone ${repo.url}`);
|
|
283
|
+
}
|
|
284
|
+
result.failed = manifest.github_repos.length;
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check GitHub auth
|
|
289
|
+
if (!isGhAuthenticated()) {
|
|
290
|
+
console.log(' ā ļø GitHub CLI not authenticated');
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
293
|
+
console.log(' ā Please run: gh auth login ā');
|
|
294
|
+
console.log(' ā ā');
|
|
295
|
+
console.log(' ā Then re-run unpack, or clone manually: ā');
|
|
296
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
297
|
+
console.log('');
|
|
298
|
+
for (const repo of manifest.github_repos) {
|
|
299
|
+
const fork = repo.isFork ? ' (fork)' : '';
|
|
300
|
+
console.log(` ⢠${repo.name}${fork}: ${repo.url}`);
|
|
301
|
+
}
|
|
302
|
+
result.failed = manifest.github_repos.length;
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Clone repos
|
|
307
|
+
for (const repo of manifest.github_repos) {
|
|
308
|
+
// Forks go to workspace/forks/, others go directly to workspace/
|
|
309
|
+
const targetDir = repo.isFork
|
|
310
|
+
? path.join(targetWorkspace, 'forks', repo.name)
|
|
311
|
+
: path.join(targetWorkspace, repo.name);
|
|
312
|
+
|
|
313
|
+
if (fs.existsSync(targetDir)) {
|
|
314
|
+
console.log(` āļø ${repo.name} (already exists)`);
|
|
315
|
+
result.skipped++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
321
|
+
console.log(` š„ Cloning ${repo.name}${repo.isFork ? ' (fork)' : ''}...`);
|
|
322
|
+
execSync(`gh repo clone "${repo.url}" "${targetDir}"`, {
|
|
323
|
+
encoding: 'utf-8',
|
|
324
|
+
timeout: 120000,
|
|
325
|
+
stdio: 'pipe',
|
|
326
|
+
});
|
|
327
|
+
console.log(` ā
${repo.name}`);
|
|
328
|
+
result.cloned++;
|
|
329
|
+
} catch {
|
|
330
|
+
console.log(` ā ļø Failed to clone ${repo.name}`);
|
|
331
|
+
result.failed++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// āā Step 6: Start Gateway āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
339
|
+
|
|
340
|
+
function startGateway(): boolean {
|
|
341
|
+
console.log('\nš Starting OpenClaw Gateway...');
|
|
342
|
+
|
|
343
|
+
if (!commandExists('openclaw')) {
|
|
344
|
+
console.log(' ā ļø openclaw command not found, skipping gateway start');
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const output = execSync('openclaw gateway start', {
|
|
350
|
+
encoding: 'utf-8',
|
|
351
|
+
timeout: 30000,
|
|
352
|
+
stdio: 'pipe',
|
|
353
|
+
});
|
|
354
|
+
console.log(' ā
Gateway started');
|
|
355
|
+
if (output.trim()) {
|
|
356
|
+
// Show first few lines of output
|
|
357
|
+
const lines = output.trim().split('\n').slice(0, 3);
|
|
358
|
+
for (const line of lines) {
|
|
359
|
+
console.log(` ${line}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.log(' ā ļø Failed to start gateway');
|
|
365
|
+
if (err instanceof Error && 'stderr' in err) {
|
|
366
|
+
const stderr = (err as { stderr: string }).stderr?.trim();
|
|
367
|
+
if (stderr) {
|
|
368
|
+
console.log(` ${stderr.split('\n')[0]}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
console.log(' Try manually: openclaw gateway start');
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// āā Main unpack āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
377
|
+
|
|
378
|
+
export async function unpack(soulFile: string, workspacePath?: string): Promise<void> {
|
|
379
|
+
console.log('\nšø openclaw-teleport ā unpacking agent soul...\n');
|
|
380
|
+
|
|
381
|
+
if (!fs.existsSync(soulFile)) {
|
|
382
|
+
throw new Error(`ā File not found: ${soulFile}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const { tmpDir, manifest } = extractManifest(soulFile);
|
|
386
|
+
const stageDir = path.join(tmpDir, 'soul');
|
|
387
|
+
|
|
388
|
+
console.log(`š Agent: ${manifest.agent_name} (${manifest.agent_id})`);
|
|
389
|
+
console.log(`š
Packed: ${manifest.packed_at}`);
|
|
390
|
+
console.log(`š Files: ${manifest.files.length}`);
|
|
391
|
+
console.log('');
|
|
392
|
+
|
|
393
|
+
// āā Step 1: Ensure OpenClaw is installed āāāāāāāāāāāāāāāāāāāāāāāāā
|
|
394
|
+
const openclawInstalled = ensureOpenClaw();
|
|
395
|
+
|
|
396
|
+
// Determine workspace
|
|
397
|
+
const targetWorkspace = workspacePath
|
|
398
|
+
? path.resolve(workspacePath)
|
|
399
|
+
: path.join(OPENCLAW_DIR, 'workspace');
|
|
400
|
+
|
|
401
|
+
fs.mkdirSync(targetWorkspace, { recursive: true });
|
|
402
|
+
|
|
403
|
+
// āā Step 2: Restore identity files āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
404
|
+
console.log('\nš Restoring identity files...');
|
|
405
|
+
let identityCount = 0;
|
|
406
|
+
const identityDir = path.join(stageDir, 'identity');
|
|
407
|
+
if (fs.existsSync(identityDir)) {
|
|
408
|
+
const files = fs.readdirSync(identityDir);
|
|
409
|
+
for (const f of files) {
|
|
410
|
+
const src = path.join(identityDir, f);
|
|
411
|
+
const dst = path.join(targetWorkspace, f);
|
|
412
|
+
fs.copyFileSync(src, dst);
|
|
413
|
+
console.log(` ā
${f}`);
|
|
414
|
+
identityCount++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// āā Step 3: Restore memory āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
419
|
+
console.log('š§ Restoring memory...');
|
|
420
|
+
let memoryCount = 0;
|
|
421
|
+
const memoryDir = path.join(stageDir, 'memory');
|
|
422
|
+
if (fs.existsSync(memoryDir)) {
|
|
423
|
+
const copyRecursive = (src: string, dst: string) => {
|
|
424
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
425
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
const srcPath = path.join(src, entry.name);
|
|
428
|
+
const dstPath = path.join(dst, entry.name);
|
|
429
|
+
if (entry.isDirectory()) {
|
|
430
|
+
copyRecursive(srcPath, dstPath);
|
|
431
|
+
} else {
|
|
432
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
433
|
+
memoryCount++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
copyRecursive(memoryDir, path.join(targetWorkspace, 'memory'));
|
|
438
|
+
console.log(` ā
${memoryCount} memory files restored`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// āā Step 4: Restore tool data āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
442
|
+
console.log('šļø Restoring tool data...');
|
|
443
|
+
let dataCount = 0;
|
|
444
|
+
const dataDir = path.join(stageDir, 'data');
|
|
445
|
+
if (fs.existsSync(dataDir)) {
|
|
446
|
+
const copyRecursive = (src: string, dst: string) => {
|
|
447
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
448
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
const srcPath = path.join(src, entry.name);
|
|
451
|
+
const dstPath = path.join(dst, entry.name);
|
|
452
|
+
if (entry.isDirectory()) {
|
|
453
|
+
copyRecursive(srcPath, dstPath);
|
|
454
|
+
} else {
|
|
455
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
456
|
+
console.log(` ā
${entry.name}`);
|
|
457
|
+
dataCount++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
copyRecursive(dataDir, targetWorkspace);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// āā Step 5: Write full agent config (with channels, credentials) ā
|
|
465
|
+
writeAgentConfig(manifest, stageDir, targetWorkspace);
|
|
466
|
+
|
|
467
|
+
// āā Step 6: Restore cron jobs āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
468
|
+
const cronCount = restoreCronJobs(manifest, stageDir);
|
|
469
|
+
|
|
470
|
+
// āā Step 7: Clone GitHub repos āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
471
|
+
const repoResult = cloneGitHubRepos(manifest, targetWorkspace);
|
|
472
|
+
|
|
473
|
+
// Clean up temp directory
|
|
474
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
475
|
+
|
|
476
|
+
// āā Step 8: Start Gateway āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
477
|
+
let gatewayStarted = false;
|
|
478
|
+
if (openclawInstalled) {
|
|
479
|
+
gatewayStarted = startGateway();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// āā Step 9: Welcome summary āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
483
|
+
const configuredServices: string[] = [];
|
|
484
|
+
if (manifest.channels) {
|
|
485
|
+
for (const [key, val] of Object.entries(manifest.channels)) {
|
|
486
|
+
if (val && typeof val === 'object' && (val as Record<string, unknown>).enabled !== false) {
|
|
487
|
+
configuredServices.push(key);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
493
|
+
console.log('šø Restoration Summary');
|
|
494
|
+
console.log('ā'.repeat(50));
|
|
495
|
+
console.log(`š Agent: ${manifest.agent_name} (${manifest.agent_id})`);
|
|
496
|
+
console.log(`š Workspace: ${targetWorkspace}`);
|
|
497
|
+
console.log(`š Files: ${identityCount} identity + ${memoryCount} memory + ${dataCount} data`);
|
|
498
|
+
console.log(`ā° Cron: ${cronCount} job(s)`);
|
|
499
|
+
|
|
500
|
+
if (manifest.github_repos && manifest.github_repos.length > 0) {
|
|
501
|
+
console.log(`š Repos: ${repoResult.cloned} cloned, ${repoResult.skipped} skipped, ${repoResult.failed} failed`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (configuredServices.length > 0) {
|
|
505
|
+
console.log(`š Services: ${configuredServices.join(', ')}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
console.log(`š§ OpenClaw: ${openclawInstalled ? 'ā
' : 'ā ļø needs install'}`);
|
|
509
|
+
console.log(`š Gateway: ${gatewayStarted ? 'ā
running' : 'ā ļø not started'}`);
|
|
510
|
+
|
|
511
|
+
// Services that may need attention
|
|
512
|
+
if (manifest.services_to_rebind && manifest.services_to_rebind.length > 0) {
|
|
513
|
+
const needsRebind = manifest.services_to_rebind.filter(
|
|
514
|
+
(s) => !configuredServices.includes(s)
|
|
515
|
+
);
|
|
516
|
+
if (needsRebind.length > 0) {
|
|
517
|
+
console.log('\nš Services that may need attention:');
|
|
518
|
+
for (const svc of needsRebind) {
|
|
519
|
+
console.log(` ā ${svc}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
525
|
+
console.log(`Welcome back, ${manifest.agent_name} šø`);
|
|
526
|
+
console.log('ā'.repeat(50) + '\n');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// āā Inspect āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
530
|
+
|
|
531
|
+
export async function inspect(soulFile: string): Promise<void> {
|
|
532
|
+
if (!fs.existsSync(soulFile)) {
|
|
533
|
+
throw new Error(`ā File not found: ${soulFile}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Extract just the manifest without full unpack
|
|
537
|
+
const tmpDir = path.join(os.tmpdir(), `soul-inspect-${Date.now()}`);
|
|
538
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
// Extract only manifest.json
|
|
542
|
+
execSync(`tar -xzf "${path.resolve(soulFile)}" -C "${tmpDir}" soul/manifest.json`, {
|
|
543
|
+
encoding: 'utf-8',
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const manifestPath = path.join(tmpDir, 'soul', 'manifest.json');
|
|
547
|
+
if (!fs.existsSync(manifestPath)) {
|
|
548
|
+
throw new Error('ā Invalid .soul file: manifest.json not found');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
552
|
+
|
|
553
|
+
const stats = fs.statSync(path.resolve(soulFile));
|
|
554
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
555
|
+
|
|
556
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
557
|
+
console.log('šø Soul Archive Inspection');
|
|
558
|
+
console.log('ā'.repeat(50));
|
|
559
|
+
console.log(`š Agent: ${manifest.agent_name} (${manifest.agent_id})`);
|
|
560
|
+
console.log(`š
Packed: ${manifest.packed_at}`);
|
|
561
|
+
console.log(`š Size: ${sizeMB} MB`);
|
|
562
|
+
console.log(`š Files: ${manifest.files.length}`);
|
|
563
|
+
|
|
564
|
+
if (manifest.github_repos.length > 0) {
|
|
565
|
+
console.log(`\nš GitHub Repos (${manifest.github_repos.length}):`);
|
|
566
|
+
for (const repo of manifest.github_repos) {
|
|
567
|
+
const fork = repo.isFork ? ' (fork)' : '';
|
|
568
|
+
console.log(` ⢠${repo.name}${fork}`);
|
|
569
|
+
console.log(` ${repo.url}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (manifest.channels && Object.keys(manifest.channels).length > 0) {
|
|
574
|
+
console.log(`\nš Channels (${Object.keys(manifest.channels).length}):`);
|
|
575
|
+
for (const key of Object.keys(manifest.channels)) {
|
|
576
|
+
console.log(` ⢠${key}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (manifest.cron_jobs && manifest.cron_jobs.length > 0) {
|
|
581
|
+
console.log(`\nā° Cron Jobs (${manifest.cron_jobs.length}):`);
|
|
582
|
+
for (const job of manifest.cron_jobs) {
|
|
583
|
+
const status = job.enabled ? 'š¢' : 'š“';
|
|
584
|
+
console.log(` ${status} ${job.name}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (manifest.services_to_rebind.length > 0) {
|
|
589
|
+
console.log(`\nš Services to rebind:`);
|
|
590
|
+
for (const svc of manifest.services_to_rebind) {
|
|
591
|
+
console.log(` ⢠${svc}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Show file breakdown
|
|
596
|
+
const identityFiles = manifest.files.filter((f) => f.startsWith('identity/'));
|
|
597
|
+
const memoryFiles = manifest.files.filter((f) => f.startsWith('memory/'));
|
|
598
|
+
const dataFiles = manifest.files.filter((f) => f.startsWith('data/'));
|
|
599
|
+
const cronFiles = manifest.files.filter((f) => f.startsWith('cron/'));
|
|
600
|
+
const configFiles = manifest.files.filter((f) => f.startsWith('config/'));
|
|
601
|
+
|
|
602
|
+
console.log('\nš Contents breakdown:');
|
|
603
|
+
if (identityFiles.length > 0) console.log(` š Identity: ${identityFiles.length} files`);
|
|
604
|
+
if (memoryFiles.length > 0) console.log(` š§ Memory: ${memoryFiles.length} files`);
|
|
605
|
+
if (dataFiles.length > 0) console.log(` šļø Data: ${dataFiles.length} files`);
|
|
606
|
+
if (cronFiles.length > 0) console.log(` ā° Cron: ${cronFiles.length} files`);
|
|
607
|
+
if (configFiles.length > 0) console.log(` āļø Config: ${configFiles.length} files`);
|
|
608
|
+
|
|
609
|
+
console.log('ā'.repeat(50) + '\n');
|
|
610
|
+
} finally {
|
|
611
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
612
|
+
}
|
|
613
|
+
}
|