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 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
- - **Identity files** — SOUL.md, IDENTITY.md, USER.md, AGENTS.md, etc.
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 identity, memory, and data files
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 (forks go to `forks/` subdirectory)
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. **Files restored** — identity, memory, tool databases
80
- 3. **Config written** — agent config + channel credentials merged into `openclaw.json` (paths dynamically generated for the new machine)
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` (forks `workspace/forks/`, others `workspace/`)
83
- 6. **GitHub auth guided** — if `gh auth login` is needed, clear instructions printed
84
- 7. **Gateway started** — `openclaw gateway start` (diagnostic info on failure)
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 packed
99
+ ├── SOUL.md ← identity files
103
100
  ├── IDENTITY.md
104
101
  ├── USER.md
105
102
  ├── TOOLS.md
106
- ├── memory/ ← full memory directory packed
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
- └── *.db tool databases packed
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
- kagura_20260320.soul (tar.gz archive)
115
+ kagura_20260324.soul (tar.gz archive)
114
116
  ├── manifest.json ← metadata, repos, channels, cron jobs
115
- ├── identity/ .md files
116
- ├── memory/ ← memory directory
117
- ├── data/ ← .db files
117
+ ├── workspace/ full workspace (minus git repos)
118
+ ├── SOUL.md
119
+ ├── memory/
120
+ │ ├── skills/
121
+ │ └── ...
118
122
  ├── config/ ← agent config
119
- └── cron/ ← cron files
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 all files
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": ["identity/SOUL.md", "memory/2026-03-15.md", "..."],
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
- 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;
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 collectMemoryDir(workspace) {
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
- const entries = fs.readdirSync(dir, { withFileTypes: true });
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
- const rel = path.join(prefix, entry.name);
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
- walk(path.join(dir, entry.name), rel);
63
- } else {
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(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
- }
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{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) {
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 ${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}`);
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(" \u26A0\uFE0F GitHub CLI (gh) not installed");
571
- console.log(" Install it: https://cli.github.com/");
572
- console.log(" Then run: gh auth login");
573
- console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
574
- for (const repo of manifest.github_repos) {
575
- console.log(` git clone ${repo.url}`);
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
- result.failed = manifest.github_repos.length;
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{1F4DD} Restoring identity files...");
667
- let identityCount = 0;
668
- const identityDir = path3.join(stageDir, "identity");
669
- if (fs3.existsSync(identityDir)) {
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
- console.log(` \u2705 ${entry.name}`);
715
- dataCount++;
672
+ workspaceCount++;
716
673
  }
717
674
  }
718
675
  };
719
- copyRecursive(dataDir, targetWorkspace);
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: ${identityCount} identity + ${memoryCount} memory + ${dataCount} data`);
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 identityFiles = manifest.files.filter((f) => f.startsWith("identity/"));
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 (identityFiles.length > 0) console.log(` \u{1F4DD} Identity: ${identityFiles.length} files`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-teleport",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Agent soul migration — pack your identity, memory, and tools into one file, unpack on a new machine",
5
5
  "type": "module",
6
6
  "bin": {
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(' ⚠️ GitHub CLI (gh) not installed');
311
- console.log(' Install it: https://cli.github.com/');
312
- console.log(' Then run: gh auth login');
313
- console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
314
- for (const repo of manifest.github_repos) {
315
- console.log(` git clone ${repo.url}`);
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
- result.failed = manifest.github_repos.length;
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 identity files ───────────────────────────────
437
- console.log('\n📝 Restoring identity files...');
438
- let identityCount = 0;
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
- // ── Step 4: Restore tool data ───────────────────────────────────
475
- console.log('🗄️ Restoring tool data...');
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
- console.log(` ✅ ${entry.name}`);
490
- dataCount++;
456
+ workspaceCount++;
491
457
  }
492
458
  }
493
459
  };
494
- copyRecursive(dataDir, targetWorkspace);
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: ${identityCount} identity + ${memoryCount} memory + ${dataCount} data`);
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 identityFiles = manifest.files.filter((f) => f.startsWith('identity/'));
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 (identityFiles.length > 0) console.log(` 📝 Identity: ${identityFiles.length} files`);
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
- collectMarkdownFiles,
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 identity files (.md in workspace root)
53
- console.log('📝 Collecting identity files...');
54
- const mdFiles = collectMarkdownFiles(agent.workspace);
55
- for (const f of mdFiles) {
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, 'identity', f);
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(`identity/${f}`);
58
+ allFiles.push(`workspace/${f}`);
61
59
  }
62
- console.log(` ✅ ${mdFiles.length} markdown files`);
63
-
64
- // 2. Collect memory directory
65
- console.log('🧠 Collecting memory...');
66
- const memFiles = collectMemoryDir(agent.workspace);
67
- for (const f of memFiles) {
68
- const src = path.join(agent.workspace, f);
69
- const dst = path.join(stageDir, f);
70
- fs.mkdirSync(path.dirname(dst), { recursive: true });
71
- fs.copyFileSync(src, dst);
72
- allFiles.push(f);
73
- }
74
- console.log(` ✅ ${memFiles.length} memory files`);
75
-
76
- // 3. Collect .db files
77
- console.log('🗄️ Collecting tool data...');
78
- const dbFiles = collectDbFiles(agent.workspace);
79
- for (const f of dbFiles) {
80
- const src = path.join(agent.workspace, f);
81
- const dst = path.join(stageDir, 'data', f);
82
- fs.mkdirSync(path.dirname(dst), { recursive: true });
83
- fs.copyFileSync(src, dst);
84
- allFiles.push(`data/${f}`);
85
- }
86
- console.log(` ✅ ${dbFiles.length} database files`);
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
- export function collectMarkdownFiles(workspace: string): string[] {
97
- const files: string[] = [];
98
- const entries = fs.readdirSync(workspace, { withFileTypes: true });
99
- for (const entry of entries) {
100
- if (entry.name === 'node_modules' || entry.name === '.git') continue;
101
- if (entry.isFile() && entry.name.endsWith('.md')) {
102
- files.push(entry.name);
103
- }
104
- }
105
- return files;
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
- export function collectMemoryDir(workspace: string): string[] {
109
- const memoryDir = path.join(workspace, 'memory');
110
- if (!fs.existsSync(memoryDir)) return [];
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
- const entries = fs.readdirSync(dir, { withFileTypes: true });
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
- const rel = path.join(prefix, entry.name);
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
- walk(path.join(dir, entry.name), rel);
118
- } else {
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(memoryDir, 'memory');
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.