openclaw-teleport 0.2.2 → 0.3.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 +26 -21
- package/dist/cli.mjs +82 -124
- package/package.json +1 -1
- package/src/commands.ts +29 -62
- package/src/pack.ts +25 -34
- package/src/utils.ts +64 -46
package/README.md
CHANGED
|
@@ -10,9 +10,7 @@ Built for [OpenClaw](https://github.com/nicepkg/openclaw) agents.
|
|
|
10
10
|
|
|
11
11
|
`openclaw-teleport` captures everything that makes an agent *that agent*:
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
- **Memory** — daily notes, long-term memory, everything in `memory/`
|
|
15
|
-
- **Tool data** — SQLite databases and other `.db` files
|
|
13
|
+
- **Workspace** — entire workspace directory (identity files, memory, daily notes, workflows, skills, tool configs — everything except git repo subdirectories)
|
|
16
14
|
- **Config** — agent configuration from `openclaw.json`
|
|
17
15
|
- **Channel credentials** — Discord tokens, Feishu app secrets, all channel configs
|
|
18
16
|
- **Cron jobs** — full scheduled task definitions (not just file names)
|
|
@@ -22,10 +20,10 @@ Built for [OpenClaw](https://github.com/nicepkg/openclaw) agents.
|
|
|
22
20
|
All packed into a single `.soul` file (tar.gz). On a new machine, `unpack` does a **full one-command restoration**:
|
|
23
21
|
|
|
24
22
|
1. ✅ Installs OpenClaw (if missing)
|
|
25
|
-
2. ✅ Restores
|
|
23
|
+
2. ✅ Restores full workspace (files, memory, workflows, skills, databases)
|
|
26
24
|
3. ✅ Writes agent config + channel credentials to `openclaw.json`
|
|
27
25
|
4. ✅ Restores cron jobs
|
|
28
|
-
5. ✅ Clones GitHub repos (
|
|
26
|
+
5. ✅ Clones GitHub repos (auto-detects forks)
|
|
29
27
|
6. ✅ Guides through GitHub auth if needed
|
|
30
28
|
7. ✅ Starts the OpenClaw gateway
|
|
31
29
|
8. ✅ Prints a welcome summary
|
|
@@ -76,13 +74,12 @@ openclaw-teleport unpack kagura_20260320.soul --workspace /path/to/workspace
|
|
|
76
74
|
|
|
77
75
|
What happens:
|
|
78
76
|
1. **OpenClaw check** — installs via `npm install -g openclaw` if missing
|
|
79
|
-
2. **
|
|
80
|
-
3. **Config written** — agent config + channel credentials merged into `openclaw.json`
|
|
77
|
+
2. **Workspace restored** — full directory structure (identity, memory, workflows, skills, databases)
|
|
78
|
+
3. **Config written** — agent config + channel credentials merged into `openclaw.json`
|
|
81
79
|
4. **Cron jobs restored** — full job definitions written to `~/.openclaw/cron/jobs.json`
|
|
82
|
-
5. **GitHub repos cloned** — using `gh repo clone` (
|
|
83
|
-
6. **
|
|
84
|
-
7. **
|
|
85
|
-
8. **Welcome summary** — file counts, repo status, configured services
|
|
80
|
+
5. **GitHub repos cloned** — using `gh repo clone` (git repo subdirectories that were skipped during pack)
|
|
81
|
+
6. **Gateway started** — `openclaw gateway start`
|
|
82
|
+
7. **Welcome summary** — file counts, repo status, configured services
|
|
86
83
|
|
|
87
84
|
### Inspect a .soul file
|
|
88
85
|
|
|
@@ -99,29 +96,37 @@ Shows manifest info without unpacking: agent name, pack date, file count, repo l
|
|
|
99
96
|
├── openclaw.json ← agent config + channels extracted
|
|
100
97
|
├── cron/jobs.json ← full cron job definitions
|
|
101
98
|
└── workspace/
|
|
102
|
-
├── SOUL.md ← identity files
|
|
99
|
+
├── SOUL.md ← identity files
|
|
103
100
|
├── IDENTITY.md
|
|
104
101
|
├── USER.md
|
|
105
102
|
├── TOOLS.md
|
|
106
|
-
├──
|
|
103
|
+
├── HEARTBEAT.md
|
|
104
|
+
├── NUDGE.md
|
|
105
|
+
├── beliefs-candidates.md
|
|
106
|
+
├── memory/ ← daily notes + long-term memory
|
|
107
107
|
│ ├── 2026-03-15.md
|
|
108
108
|
│ └── ...
|
|
109
|
-
|
|
109
|
+
├── skills/ ← custom skills
|
|
110
|
+
├── flowforge/ ← git repo (skipped, cloned on unpack)
|
|
111
|
+
└── knowledge-base/ ← git repo (skipped, cloned on unpack)
|
|
110
112
|
|
|
111
113
|
↓ openclaw-teleport pack
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
kagura_20260324.soul (tar.gz archive)
|
|
114
116
|
├── manifest.json ← metadata, repos, channels, cron jobs
|
|
115
|
-
├──
|
|
116
|
-
├──
|
|
117
|
-
├──
|
|
117
|
+
├── workspace/ ← full workspace (minus git repos)
|
|
118
|
+
│ ├── SOUL.md
|
|
119
|
+
│ ├── memory/
|
|
120
|
+
│ ├── skills/
|
|
121
|
+
│ └── ...
|
|
118
122
|
├── config/ ← agent config
|
|
119
|
-
|
|
123
|
+
├── cron/ ← cron files
|
|
124
|
+
└── credentials/ ← pairing records
|
|
120
125
|
|
|
121
126
|
↓ openclaw-teleport unpack (on new machine)
|
|
122
127
|
|
|
123
128
|
1. Install OpenClaw (if needed)
|
|
124
|
-
2. Restore
|
|
129
|
+
2. Restore workspace files
|
|
125
130
|
3. Write config + credentials to openclaw.json
|
|
126
131
|
4. Restore cron jobs
|
|
127
132
|
5. Clone GitHub repos (via gh)
|
|
@@ -138,7 +143,7 @@ The manifest contains metadata and embedded configurations:
|
|
|
138
143
|
"agent_id": "kagura",
|
|
139
144
|
"agent_name": "Kagura",
|
|
140
145
|
"packed_at": "2026-03-20T04:25:00.000Z",
|
|
141
|
-
"files": ["
|
|
146
|
+
"files": ["workspace/SOUL.md", "workspace/memory/2026-03-15.md", "..."],
|
|
142
147
|
"github_repos": [
|
|
143
148
|
{ "name": "openclaw-teleport", "url": "https://github.com/kagura-agent/openclaw-teleport", "isFork": false }
|
|
144
149
|
],
|
package/dist/cli.mjs
CHANGED
|
@@ -39,58 +39,32 @@ function findAgent(config, agentId) {
|
|
|
39
39
|
}
|
|
40
40
|
return agents[0];
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
42
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "__pycache__", ".venv", "venv"]);
|
|
43
|
+
function isGitRepo(dirPath) {
|
|
44
|
+
return fs.existsSync(path.join(dirPath, ".git"));
|
|
52
45
|
}
|
|
53
|
-
function
|
|
54
|
-
const memoryDir = path.join(workspace, "memory");
|
|
55
|
-
if (!fs.existsSync(memoryDir)) return [];
|
|
46
|
+
function collectWorkspaceFiles(workspace) {
|
|
56
47
|
const files = [];
|
|
57
48
|
const walk = (dir, prefix) => {
|
|
58
|
-
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
59
55
|
for (const entry of entries) {
|
|
60
|
-
|
|
56
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
57
|
+
const fullPath = path.join(dir, entry.name);
|
|
58
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
61
59
|
if (entry.isDirectory()) {
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if (isGitRepo(fullPath)) continue;
|
|
61
|
+
walk(fullPath, rel);
|
|
62
|
+
} else if (entry.isFile()) {
|
|
64
63
|
files.push(rel);
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
};
|
|
68
|
-
walk(
|
|
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
|
-
}
|
|
67
|
+
walk(workspace, "");
|
|
94
68
|
return files;
|
|
95
69
|
}
|
|
96
70
|
function collectCronFiles(agentId) {
|
|
@@ -185,6 +159,24 @@ function commandExists(cmd) {
|
|
|
185
159
|
return false;
|
|
186
160
|
}
|
|
187
161
|
}
|
|
162
|
+
function installGh() {
|
|
163
|
+
try {
|
|
164
|
+
const platform2 = os.platform();
|
|
165
|
+
if (platform2 === "linux") {
|
|
166
|
+
execSync(
|
|
167
|
+
'(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) && sudo mkdir -p -m 755 /etc/apt/keyrings && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y',
|
|
168
|
+
{ stdio: "pipe", timeout: 12e4 }
|
|
169
|
+
);
|
|
170
|
+
} else if (platform2 === "darwin") {
|
|
171
|
+
execSync("brew install gh", { stdio: "pipe", timeout: 12e4 });
|
|
172
|
+
} else {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return commandExists("gh");
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
188
180
|
function isGhAuthenticated() {
|
|
189
181
|
try {
|
|
190
182
|
execSync("gh auth status", { encoding: "utf-8", stdio: "pipe" });
|
|
@@ -216,36 +208,32 @@ async function pack(agentId, outputPath) {
|
|
|
216
208
|
}
|
|
217
209
|
fs2.mkdirSync(stageDir, { recursive: true });
|
|
218
210
|
const allFiles = [];
|
|
219
|
-
console.log("\u{
|
|
220
|
-
const
|
|
221
|
-
for (const f of
|
|
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) {
|
|
211
|
+
console.log("\u{1F4C2} Collecting workspace files...");
|
|
212
|
+
const wsFiles = collectWorkspaceFiles(agent.workspace);
|
|
213
|
+
for (const f of wsFiles) {
|
|
232
214
|
const src = path2.join(agent.workspace, f);
|
|
233
|
-
const dst = path2.join(stageDir, f);
|
|
215
|
+
const dst = path2.join(stageDir, "workspace", f);
|
|
234
216
|
fs2.mkdirSync(path2.dirname(dst), { recursive: true });
|
|
235
217
|
fs2.copyFileSync(src, dst);
|
|
236
|
-
allFiles.push(f);
|
|
218
|
+
allFiles.push(`workspace/${f}`);
|
|
237
219
|
}
|
|
238
|
-
console.log(` \u2705 ${
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
220
|
+
console.log(` \u2705 ${wsFiles.length} files (skipped git repo subdirs)`);
|
|
221
|
+
try {
|
|
222
|
+
const topEntries = fs2.readdirSync(agent.workspace, { withFileTypes: true });
|
|
223
|
+
const skippedRepos = [];
|
|
224
|
+
for (const entry of topEntries) {
|
|
225
|
+
if (entry.isDirectory()) {
|
|
226
|
+
const gitDir = path2.join(agent.workspace, entry.name, ".git");
|
|
227
|
+
if (fs2.existsSync(gitDir)) {
|
|
228
|
+
skippedRepos.push(entry.name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (skippedRepos.length > 0) {
|
|
233
|
+
console.log(` \u23ED\uFE0F Skipped git repos (will clone on unpack): ${skippedRepos.join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
247
236
|
}
|
|
248
|
-
console.log(` \u2705 ${dbFiles.length} database files`);
|
|
249
237
|
console.log("\u2699\uFE0F Extracting agent config...");
|
|
250
238
|
const agentConfig = extractAgentConfig(config, agent.id);
|
|
251
239
|
const configPath = path2.join(stageDir, "config", "agent-config.json");
|
|
@@ -567,15 +555,19 @@ function cloneGitHubRepos(manifest, targetWorkspace) {
|
|
|
567
555
|
}
|
|
568
556
|
console.log("\n\u{1F419} Cloning GitHub repos...");
|
|
569
557
|
if (!commandExists("gh")) {
|
|
570
|
-
console.log(" \
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
console.log(`
|
|
558
|
+
console.log(" \u2B07\uFE0F GitHub CLI (gh) not found, installing...");
|
|
559
|
+
const installed = installGh();
|
|
560
|
+
if (!installed) {
|
|
561
|
+
console.log(" \u26A0\uFE0F Could not auto-install GitHub CLI");
|
|
562
|
+
console.log(" Install manually: https://cli.github.com/");
|
|
563
|
+
console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
|
|
564
|
+
for (const repo of manifest.github_repos) {
|
|
565
|
+
console.log(` git clone ${repo.url}`);
|
|
566
|
+
}
|
|
567
|
+
result.failed = manifest.github_repos.length;
|
|
568
|
+
return result;
|
|
576
569
|
}
|
|
577
|
-
|
|
578
|
-
return result;
|
|
570
|
+
console.log(" \u2705 GitHub CLI installed");
|
|
579
571
|
}
|
|
580
572
|
if (!isGhAuthenticated()) {
|
|
581
573
|
console.log(" \u26A0\uFE0F GitHub CLI not authenticated");
|
|
@@ -663,44 +655,10 @@ async function unpack(soulFile, workspacePath) {
|
|
|
663
655
|
const openclawInstalled = ensureOpenClaw();
|
|
664
656
|
const targetWorkspace = workspacePath ? path3.resolve(workspacePath) : path3.join(OPENCLAW_DIR3, "workspace");
|
|
665
657
|
fs3.mkdirSync(targetWorkspace, { recursive: true });
|
|
666
|
-
console.log("\n\u{
|
|
667
|
-
let
|
|
668
|
-
const
|
|
669
|
-
if (fs3.existsSync(
|
|
670
|
-
const files = fs3.readdirSync(identityDir);
|
|
671
|
-
for (const f of files) {
|
|
672
|
-
const src = path3.join(identityDir, f);
|
|
673
|
-
const dst = path3.join(targetWorkspace, f);
|
|
674
|
-
fs3.copyFileSync(src, dst);
|
|
675
|
-
console.log(` \u2705 ${f}`);
|
|
676
|
-
identityCount++;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
console.log("\u{1F9E0} Restoring memory...");
|
|
680
|
-
let memoryCount = 0;
|
|
681
|
-
const memoryDir = path3.join(stageDir, "memory");
|
|
682
|
-
if (fs3.existsSync(memoryDir)) {
|
|
683
|
-
const copyRecursive = (src, dst) => {
|
|
684
|
-
fs3.mkdirSync(dst, { recursive: true });
|
|
685
|
-
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
686
|
-
for (const entry of entries) {
|
|
687
|
-
const srcPath = path3.join(src, entry.name);
|
|
688
|
-
const dstPath = path3.join(dst, entry.name);
|
|
689
|
-
if (entry.isDirectory()) {
|
|
690
|
-
copyRecursive(srcPath, dstPath);
|
|
691
|
-
} else {
|
|
692
|
-
fs3.copyFileSync(srcPath, dstPath);
|
|
693
|
-
memoryCount++;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
copyRecursive(memoryDir, path3.join(targetWorkspace, "memory"));
|
|
698
|
-
console.log(` \u2705 ${memoryCount} memory files restored`);
|
|
699
|
-
}
|
|
700
|
-
console.log("\u{1F5C4}\uFE0F Restoring tool data...");
|
|
701
|
-
let dataCount = 0;
|
|
702
|
-
const dataDir = path3.join(stageDir, "data");
|
|
703
|
-
if (fs3.existsSync(dataDir)) {
|
|
658
|
+
console.log("\n\u{1F4C2} Restoring workspace files...");
|
|
659
|
+
let workspaceCount = 0;
|
|
660
|
+
const workspaceDir = path3.join(stageDir, "workspace");
|
|
661
|
+
if (fs3.existsSync(workspaceDir)) {
|
|
704
662
|
const copyRecursive = (src, dst) => {
|
|
705
663
|
fs3.mkdirSync(dst, { recursive: true });
|
|
706
664
|
const entries = fs3.readdirSync(src, { withFileTypes: true });
|
|
@@ -711,12 +669,14 @@ async function unpack(soulFile, workspacePath) {
|
|
|
711
669
|
copyRecursive(srcPath, dstPath);
|
|
712
670
|
} else {
|
|
713
671
|
fs3.copyFileSync(srcPath, dstPath);
|
|
714
|
-
|
|
715
|
-
dataCount++;
|
|
672
|
+
workspaceCount++;
|
|
716
673
|
}
|
|
717
674
|
}
|
|
718
675
|
};
|
|
719
|
-
copyRecursive(
|
|
676
|
+
copyRecursive(workspaceDir, targetWorkspace);
|
|
677
|
+
console.log(` \u2705 ${workspaceCount} files restored`);
|
|
678
|
+
} else {
|
|
679
|
+
console.log(" \u26A0\uFE0F No workspace/ directory in archive");
|
|
720
680
|
}
|
|
721
681
|
writeAgentConfig(manifest, stageDir, targetWorkspace);
|
|
722
682
|
const cronCount = restoreCronJobs(manifest, stageDir);
|
|
@@ -740,7 +700,7 @@ async function unpack(soulFile, workspacePath) {
|
|
|
740
700
|
console.log("\u2550".repeat(50));
|
|
741
701
|
console.log(`\u{1F194} Agent: ${manifest.agent_name} (${manifest.agent_id})`);
|
|
742
702
|
console.log(`\u{1F4C2} Workspace: ${targetWorkspace}`);
|
|
743
|
-
console.log(`\u{1F4DD} Files: ${
|
|
703
|
+
console.log(`\u{1F4DD} Files: ${workspaceCount} workspace files`);
|
|
744
704
|
console.log(`\u23F0 Cron: ${cronCount} job(s)`);
|
|
745
705
|
if (manifest.github_repos && manifest.github_repos.length > 0) {
|
|
746
706
|
console.log(`\u{1F419} Repos: ${repoResult.cloned} cloned, ${repoResult.skipped} skipped, ${repoResult.failed} failed`);
|
|
@@ -820,17 +780,15 @@ async function inspect(soulFile) {
|
|
|
820
780
|
console.log(` \u2022 ${svc}`);
|
|
821
781
|
}
|
|
822
782
|
}
|
|
823
|
-
const
|
|
824
|
-
const memoryFiles = manifest.files.filter((f) => f.startsWith("memory/"));
|
|
825
|
-
const dataFiles = manifest.files.filter((f) => f.startsWith("data/"));
|
|
783
|
+
const workspaceFiles = manifest.files.filter((f) => f.startsWith("workspace/"));
|
|
826
784
|
const cronFiles = manifest.files.filter((f) => f.startsWith("cron/"));
|
|
827
785
|
const configFiles = manifest.files.filter((f) => f.startsWith("config/"));
|
|
786
|
+
const credFiles = manifest.files.filter((f) => f.startsWith("credentials/"));
|
|
828
787
|
console.log("\n\u{1F4CA} Contents breakdown:");
|
|
829
|
-
if (
|
|
830
|
-
if (memoryFiles.length > 0) console.log(` \u{1F9E0} Memory: ${memoryFiles.length} files`);
|
|
831
|
-
if (dataFiles.length > 0) console.log(` \u{1F5C4}\uFE0F Data: ${dataFiles.length} files`);
|
|
788
|
+
if (workspaceFiles.length > 0) console.log(` \u{1F4C2} Workspace: ${workspaceFiles.length} files`);
|
|
832
789
|
if (cronFiles.length > 0) console.log(` \u23F0 Cron: ${cronFiles.length} files`);
|
|
833
790
|
if (configFiles.length > 0) console.log(` \u2699\uFE0F Config: ${configFiles.length} files`);
|
|
791
|
+
if (credFiles.length > 0) console.log(` \u{1F510} Creds: ${credFiles.length} files`);
|
|
834
792
|
console.log("\u2550".repeat(50) + "\n");
|
|
835
793
|
} finally {
|
|
836
794
|
fs3.rmSync(tmpDir, { recursive: true });
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
|
-
import { loadConfig, commandExists, isGhAuthenticated, type Manifest, type OpenClawConfig, type CronJob } from './utils.js';
|
|
5
|
+
import { loadConfig, commandExists, installGh, isGhAuthenticated, type Manifest, type OpenClawConfig, type CronJob } from './utils.js';
|
|
6
6
|
|
|
7
7
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
8
8
|
const CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
@@ -305,17 +305,21 @@ function cloneGitHubRepos(manifest: Manifest, targetWorkspace: string): { cloned
|
|
|
305
305
|
|
|
306
306
|
console.log('\n🐙 Cloning GitHub repos...');
|
|
307
307
|
|
|
308
|
-
// Check if gh CLI is available
|
|
308
|
+
// Check if gh CLI is available, install if not
|
|
309
309
|
if (!commandExists('gh')) {
|
|
310
|
-
console.log('
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.log(`
|
|
310
|
+
console.log(' ⬇️ GitHub CLI (gh) not found, installing...');
|
|
311
|
+
const installed = installGh();
|
|
312
|
+
if (!installed) {
|
|
313
|
+
console.log(' ⚠️ Could not auto-install GitHub CLI');
|
|
314
|
+
console.log(' Install manually: https://cli.github.com/');
|
|
315
|
+
console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
|
|
316
|
+
for (const repo of manifest.github_repos) {
|
|
317
|
+
console.log(` git clone ${repo.url}`);
|
|
318
|
+
}
|
|
319
|
+
result.failed = manifest.github_repos.length;
|
|
320
|
+
return result;
|
|
316
321
|
}
|
|
317
|
-
|
|
318
|
-
return result;
|
|
322
|
+
console.log(' ✅ GitHub CLI installed');
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
// Check GitHub auth
|
|
@@ -433,49 +437,12 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
|
|
|
433
437
|
|
|
434
438
|
fs.mkdirSync(targetWorkspace, { recursive: true });
|
|
435
439
|
|
|
436
|
-
// ── Step 2: Restore
|
|
437
|
-
console.log('\n
|
|
438
|
-
let
|
|
439
|
-
const identityDir = path.join(stageDir, 'identity');
|
|
440
|
-
if (fs.existsSync(identityDir)) {
|
|
441
|
-
const files = fs.readdirSync(identityDir);
|
|
442
|
-
for (const f of files) {
|
|
443
|
-
const src = path.join(identityDir, f);
|
|
444
|
-
const dst = path.join(targetWorkspace, f);
|
|
445
|
-
fs.copyFileSync(src, dst);
|
|
446
|
-
console.log(` ✅ ${f}`);
|
|
447
|
-
identityCount++;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ── Step 3: Restore memory ──────────────────────────────────────
|
|
452
|
-
console.log('🧠 Restoring memory...');
|
|
453
|
-
let memoryCount = 0;
|
|
454
|
-
const memoryDir = path.join(stageDir, 'memory');
|
|
455
|
-
if (fs.existsSync(memoryDir)) {
|
|
456
|
-
const copyRecursive = (src: string, dst: string) => {
|
|
457
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
458
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
459
|
-
for (const entry of entries) {
|
|
460
|
-
const srcPath = path.join(src, entry.name);
|
|
461
|
-
const dstPath = path.join(dst, entry.name);
|
|
462
|
-
if (entry.isDirectory()) {
|
|
463
|
-
copyRecursive(srcPath, dstPath);
|
|
464
|
-
} else {
|
|
465
|
-
fs.copyFileSync(srcPath, dstPath);
|
|
466
|
-
memoryCount++;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
copyRecursive(memoryDir, path.join(targetWorkspace, 'memory'));
|
|
471
|
-
console.log(` ✅ ${memoryCount} memory files restored`);
|
|
472
|
-
}
|
|
440
|
+
// ── Step 2: Restore workspace files ──────────────────────────────
|
|
441
|
+
console.log('\n📂 Restoring workspace files...');
|
|
442
|
+
let workspaceCount = 0;
|
|
473
443
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
let dataCount = 0;
|
|
477
|
-
const dataDir = path.join(stageDir, 'data');
|
|
478
|
-
if (fs.existsSync(dataDir)) {
|
|
444
|
+
const workspaceDir = path.join(stageDir, 'workspace');
|
|
445
|
+
if (fs.existsSync(workspaceDir)) {
|
|
479
446
|
const copyRecursive = (src: string, dst: string) => {
|
|
480
447
|
fs.mkdirSync(dst, { recursive: true });
|
|
481
448
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
@@ -486,12 +453,14 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
|
|
|
486
453
|
copyRecursive(srcPath, dstPath);
|
|
487
454
|
} else {
|
|
488
455
|
fs.copyFileSync(srcPath, dstPath);
|
|
489
|
-
|
|
490
|
-
dataCount++;
|
|
456
|
+
workspaceCount++;
|
|
491
457
|
}
|
|
492
458
|
}
|
|
493
459
|
};
|
|
494
|
-
copyRecursive(
|
|
460
|
+
copyRecursive(workspaceDir, targetWorkspace);
|
|
461
|
+
console.log(` ✅ ${workspaceCount} files restored`);
|
|
462
|
+
} else {
|
|
463
|
+
console.log(' ⚠️ No workspace/ directory in archive');
|
|
495
464
|
}
|
|
496
465
|
|
|
497
466
|
// ── Step 5: Write full agent config (with channels, credentials) ─
|
|
@@ -530,7 +499,7 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
|
|
|
530
499
|
console.log('═'.repeat(50));
|
|
531
500
|
console.log(`🆔 Agent: ${manifest.agent_name} (${manifest.agent_id})`);
|
|
532
501
|
console.log(`📂 Workspace: ${targetWorkspace}`);
|
|
533
|
-
console.log(`📝 Files: ${
|
|
502
|
+
console.log(`📝 Files: ${workspaceCount} workspace files`);
|
|
534
503
|
console.log(`⏰ Cron: ${cronCount} job(s)`);
|
|
535
504
|
|
|
536
505
|
if (manifest.github_repos && manifest.github_repos.length > 0) {
|
|
@@ -629,18 +598,16 @@ export async function inspect(soulFile: string): Promise<void> {
|
|
|
629
598
|
}
|
|
630
599
|
|
|
631
600
|
// Show file breakdown
|
|
632
|
-
const
|
|
633
|
-
const memoryFiles = manifest.files.filter((f) => f.startsWith('memory/'));
|
|
634
|
-
const dataFiles = manifest.files.filter((f) => f.startsWith('data/'));
|
|
601
|
+
const workspaceFiles = manifest.files.filter((f) => f.startsWith('workspace/'));
|
|
635
602
|
const cronFiles = manifest.files.filter((f) => f.startsWith('cron/'));
|
|
636
603
|
const configFiles = manifest.files.filter((f) => f.startsWith('config/'));
|
|
604
|
+
const credFiles = manifest.files.filter((f) => f.startsWith('credentials/'));
|
|
637
605
|
|
|
638
606
|
console.log('\n📊 Contents breakdown:');
|
|
639
|
-
if (
|
|
640
|
-
if (memoryFiles.length > 0) console.log(` 🧠 Memory: ${memoryFiles.length} files`);
|
|
641
|
-
if (dataFiles.length > 0) console.log(` 🗄️ Data: ${dataFiles.length} files`);
|
|
607
|
+
if (workspaceFiles.length > 0) console.log(` 📂 Workspace: ${workspaceFiles.length} files`);
|
|
642
608
|
if (cronFiles.length > 0) console.log(` ⏰ Cron: ${cronFiles.length} files`);
|
|
643
609
|
if (configFiles.length > 0) console.log(` ⚙️ Config: ${configFiles.length} files`);
|
|
610
|
+
if (credFiles.length > 0) console.log(` 🔐 Creds: ${credFiles.length} files`);
|
|
644
611
|
|
|
645
612
|
console.log('═'.repeat(50) + '\n');
|
|
646
613
|
} finally {
|
package/src/pack.ts
CHANGED
|
@@ -5,9 +5,7 @@ import { execSync } from 'node:child_process';
|
|
|
5
5
|
import {
|
|
6
6
|
loadConfig,
|
|
7
7
|
findAgent,
|
|
8
|
-
|
|
9
|
-
collectMemoryDir,
|
|
10
|
-
collectDbFiles,
|
|
8
|
+
collectWorkspaceFiles,
|
|
11
9
|
collectCronFiles,
|
|
12
10
|
getGitHubRepos,
|
|
13
11
|
detectServices,
|
|
@@ -49,41 +47,34 @@ export async function pack(agentId?: string, outputPath?: string): Promise<void>
|
|
|
49
47
|
|
|
50
48
|
const allFiles: string[] = [];
|
|
51
49
|
|
|
52
|
-
// 1. Collect
|
|
53
|
-
console.log('
|
|
54
|
-
const
|
|
55
|
-
for (const f of
|
|
50
|
+
// 1. Collect entire workspace recursively (skips git repos, node_modules, etc.)
|
|
51
|
+
console.log('📂 Collecting workspace files...');
|
|
52
|
+
const wsFiles = collectWorkspaceFiles(agent.workspace);
|
|
53
|
+
for (const f of wsFiles) {
|
|
56
54
|
const src = path.join(agent.workspace, f);
|
|
57
|
-
const dst = path.join(stageDir, '
|
|
55
|
+
const dst = path.join(stageDir, 'workspace', f);
|
|
58
56
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
59
57
|
fs.copyFileSync(src, dst);
|
|
60
|
-
allFiles.push(`
|
|
58
|
+
allFiles.push(`workspace/${f}`);
|
|
61
59
|
}
|
|
62
|
-
console.log(` ✅ ${
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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`);
|
|
60
|
+
console.log(` ✅ ${wsFiles.length} files (skipped git repo subdirs)`);
|
|
61
|
+
|
|
62
|
+
// List skipped git repos for transparency
|
|
63
|
+
try {
|
|
64
|
+
const topEntries = fs.readdirSync(agent.workspace, { withFileTypes: true });
|
|
65
|
+
const skippedRepos: string[] = [];
|
|
66
|
+
for (const entry of topEntries) {
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
const gitDir = path.join(agent.workspace, entry.name, '.git');
|
|
69
|
+
if (fs.existsSync(gitDir)) {
|
|
70
|
+
skippedRepos.push(entry.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (skippedRepos.length > 0) {
|
|
75
|
+
console.log(` ⏭️ Skipped git repos (will clone on unpack): ${skippedRepos.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
87
78
|
|
|
88
79
|
// 4. Extract agent config
|
|
89
80
|
console.log('⚙️ Extracting agent config...');
|
package/src/utils.ts
CHANGED
|
@@ -93,62 +93,51 @@ export function findAgent(config: OpenClawConfig, agentId?: string): AgentConfig
|
|
|
93
93
|
|
|
94
94
|
// ── File collection ────────────────────────────────────────────────
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return
|
|
96
|
+
/** Directories to always skip when recursively walking the workspace. */
|
|
97
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '__pycache__', '.venv', 'venv']);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check whether a directory is the root of its own git repository.
|
|
101
|
+
* Used to skip cloneable sub-repos (knowledge-base, gogetajob, etc.)
|
|
102
|
+
* so they are restored via `gh repo clone` instead of being packed.
|
|
103
|
+
*/
|
|
104
|
+
function isGitRepo(dirPath: string): boolean {
|
|
105
|
+
return fs.existsSync(path.join(dirPath, '.git'));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Recursively collect all files in the workspace, preserving the
|
|
110
|
+
* directory structure. Skips:
|
|
111
|
+
* - `node_modules`, `.git`, `dist`, build/cache dirs
|
|
112
|
+
* - Sub-directories that are their own git repos (restored via clone)
|
|
113
|
+
*
|
|
114
|
+
* Returns paths relative to `workspace`.
|
|
115
|
+
*/
|
|
116
|
+
export function collectWorkspaceFiles(workspace: string): string[] {
|
|
111
117
|
const files: string[] = [];
|
|
112
118
|
const walk = (dir: string, prefix: string) => {
|
|
113
|
-
|
|
119
|
+
let entries: fs.Dirent[];
|
|
120
|
+
try {
|
|
121
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
122
|
+
} catch {
|
|
123
|
+
return; // permission errors, broken symlinks, etc.
|
|
124
|
+
}
|
|
114
125
|
for (const entry of entries) {
|
|
115
|
-
|
|
126
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
127
|
+
|
|
128
|
+
const fullPath = path.join(dir, entry.name);
|
|
129
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
130
|
+
|
|
116
131
|
if (entry.isDirectory()) {
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
// Skip sub-directories that are standalone git repos
|
|
133
|
+
if (isGitRepo(fullPath)) continue;
|
|
134
|
+
walk(fullPath, rel);
|
|
135
|
+
} else if (entry.isFile()) {
|
|
119
136
|
files.push(rel);
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
};
|
|
123
|
-
walk(
|
|
124
|
-
return files;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function collectDbFiles(workspace: string): string[] {
|
|
128
|
-
const files: string[] = [];
|
|
129
|
-
// Only collect .db files from known tool data directories, not recursively
|
|
130
|
-
// This prevents grabbing test DBs or unrelated data from project subdirs
|
|
131
|
-
const knownPaths = [
|
|
132
|
-
'gogetajob/data/gogetajob.db',
|
|
133
|
-
'flowforge/flowforge.db',
|
|
134
|
-
'data/gogetajob.db',
|
|
135
|
-
'data/flowforge.db',
|
|
136
|
-
];
|
|
137
|
-
for (const rel of knownPaths) {
|
|
138
|
-
const full = path.join(workspace, rel);
|
|
139
|
-
if (fs.existsSync(full)) {
|
|
140
|
-
files.push(rel);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Also check workspace root for any .db files
|
|
144
|
-
try {
|
|
145
|
-
const rootEntries = fs.readdirSync(workspace, { withFileTypes: true });
|
|
146
|
-
for (const entry of rootEntries) {
|
|
147
|
-
if (entry.isFile() && entry.name.endsWith('.db')) {
|
|
148
|
-
files.push(entry.name);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch {}
|
|
140
|
+
walk(workspace, '');
|
|
152
141
|
return files;
|
|
153
142
|
}
|
|
154
143
|
|
|
@@ -299,6 +288,35 @@ export function commandExists(cmd: string): boolean {
|
|
|
299
288
|
}
|
|
300
289
|
}
|
|
301
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Install GitHub CLI (gh) automatically.
|
|
293
|
+
* Supports apt (Debian/Ubuntu) and brew (macOS).
|
|
294
|
+
*/
|
|
295
|
+
export function installGh(): boolean {
|
|
296
|
+
try {
|
|
297
|
+
const platform = os.platform();
|
|
298
|
+
if (platform === 'linux') {
|
|
299
|
+
// Try apt-based install (Debian/Ubuntu)
|
|
300
|
+
execSync(
|
|
301
|
+
'(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) && ' +
|
|
302
|
+
'sudo mkdir -p -m 755 /etc/apt/keyrings && ' +
|
|
303
|
+
'wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && ' +
|
|
304
|
+
'sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && ' +
|
|
305
|
+
'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && ' +
|
|
306
|
+
'sudo apt update && sudo apt install gh -y',
|
|
307
|
+
{ stdio: 'pipe', timeout: 120000 }
|
|
308
|
+
);
|
|
309
|
+
} else if (platform === 'darwin') {
|
|
310
|
+
execSync('brew install gh', { stdio: 'pipe', timeout: 120000 });
|
|
311
|
+
} else {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return commandExists('gh');
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
302
320
|
/**
|
|
303
321
|
* Check GitHub CLI auth status.
|
|
304
322
|
* Returns true if authenticated.
|