openclaw-teleport 0.2.0 → 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");
@@ -263,6 +251,21 @@ async function pack(agentId, outputPath) {
263
251
  allFiles.push(`cron/${f}`);
264
252
  }
265
253
  console.log(` \u2705 ${cronFiles.length} cron files`);
254
+ console.log("\u{1F510} Collecting credentials...");
255
+ const credDir = path2.join(OPENCLAW_DIR2, "credentials");
256
+ let credCount = 0;
257
+ if (fs2.existsSync(credDir)) {
258
+ const credFiles = fs2.readdirSync(credDir).filter((f) => f.endsWith(".json"));
259
+ for (const f of credFiles) {
260
+ const src = path2.join(credDir, f);
261
+ const dst = path2.join(stageDir, "credentials", f);
262
+ fs2.mkdirSync(path2.dirname(dst), { recursive: true });
263
+ fs2.copyFileSync(src, dst);
264
+ allFiles.push(`credentials/${f}`);
265
+ credCount++;
266
+ }
267
+ }
268
+ console.log(` \u2705 ${credCount} credential files`);
266
269
  console.log("\u23F0 Extracting cron job definitions...");
267
270
  const cronJobs = loadCronJobs(agent.id);
268
271
  console.log(` \u2705 ${cronJobs.length} cron jobs for ${agent.id}`);
@@ -278,6 +281,7 @@ async function pack(agentId, outputPath) {
278
281
  const agentDefaults = sanitizeAgentDefaults(config.agents?.defaults ?? {});
279
282
  const modelsConfig = config.models ?? {};
280
283
  const bindingsConfig = config.bindings ?? [];
284
+ const gatewayConfig = config.gateway ?? {};
281
285
  const manifest = {
282
286
  agent_id: agent.id,
283
287
  agent_name: agent.name,
@@ -289,7 +293,8 @@ async function pack(agentId, outputPath) {
289
293
  cron_jobs: cronJobs,
290
294
  agent_defaults: agentDefaults,
291
295
  models_config: modelsConfig,
292
- bindings: bindingsConfig
296
+ bindings: bindingsConfig,
297
+ gateway: gatewayConfig
293
298
  };
294
299
  const manifestPath = path2.join(stageDir, "manifest.json");
295
300
  fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
@@ -450,6 +455,10 @@ function writeAgentConfig(manifest, stageDir, targetWorkspace) {
450
455
  console.log(" \u23ED\uFE0F Bindings already exist, skipping");
451
456
  }
452
457
  }
458
+ if (manifest.gateway && Object.keys(manifest.gateway).length > 0) {
459
+ existingConfig.gateway = { ...existingConfig.gateway ?? {}, ...manifest.gateway };
460
+ console.log(" \u2705 Gateway config restored");
461
+ }
453
462
  fs3.writeFileSync(CONFIG_PATH2, JSON.stringify(existingConfig, null, 2));
454
463
  } else {
455
464
  const newConfig = {
@@ -473,6 +482,10 @@ function writeAgentConfig(manifest, stageDir, targetWorkspace) {
473
482
  newConfig.bindings = manifest.bindings;
474
483
  console.log(" \u2705 Bindings restored");
475
484
  }
485
+ if (manifest.gateway && Object.keys(manifest.gateway).length > 0) {
486
+ newConfig.gateway = manifest.gateway;
487
+ console.log(" \u2705 Gateway config restored");
488
+ }
476
489
  fs3.writeFileSync(CONFIG_PATH2, JSON.stringify(newConfig, null, 2));
477
490
  console.log(" \u2705 New openclaw.json created");
478
491
  }
@@ -519,6 +532,22 @@ function restoreCronJobs(manifest, stageDir) {
519
532
  }
520
533
  return manifest.cron_jobs?.length ?? cronFileCount;
521
534
  }
535
+ function restoreCredentials(stageDir) {
536
+ console.log("\u{1F510} Restoring credentials...");
537
+ const credSrc = path3.join(stageDir, "credentials");
538
+ if (!fs3.existsSync(credSrc)) {
539
+ console.log(" (none)");
540
+ return 0;
541
+ }
542
+ const credDst = path3.join(OPENCLAW_DIR3, "credentials");
543
+ fs3.mkdirSync(credDst, { recursive: true });
544
+ const files = fs3.readdirSync(credSrc).filter((f) => f.endsWith(".json"));
545
+ for (const f of files) {
546
+ fs3.copyFileSync(path3.join(credSrc, f), path3.join(credDst, f));
547
+ }
548
+ console.log(` \u2705 ${files.length} credential file(s) restored`);
549
+ return files.length;
550
+ }
522
551
  function cloneGitHubRepos(manifest, targetWorkspace) {
523
552
  const result = { cloned: 0, skipped: 0, failed: 0 };
524
553
  if (!manifest.github_repos || manifest.github_repos.length === 0) {
@@ -526,15 +555,19 @@ function cloneGitHubRepos(manifest, targetWorkspace) {
526
555
  }
527
556
  console.log("\n\u{1F419} Cloning GitHub repos...");
528
557
  if (!commandExists("gh")) {
529
- console.log(" \u26A0\uFE0F GitHub CLI (gh) not installed");
530
- console.log(" Install it: https://cli.github.com/");
531
- console.log(" Then run: gh auth login");
532
- console.log(` Repos to clone manually (${manifest.github_repos.length}):`);
533
- for (const repo of manifest.github_repos) {
534
- console.log(` git clone ${repo.url}`);
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;
535
569
  }
536
- result.failed = manifest.github_repos.length;
537
- return result;
570
+ console.log(" \u2705 GitHub CLI installed");
538
571
  }
539
572
  if (!isGhAuthenticated()) {
540
573
  console.log(" \u26A0\uFE0F GitHub CLI not authenticated");
@@ -622,23 +655,10 @@ async function unpack(soulFile, workspacePath) {
622
655
  const openclawInstalled = ensureOpenClaw();
623
656
  const targetWorkspace = workspacePath ? path3.resolve(workspacePath) : path3.join(OPENCLAW_DIR3, "workspace");
624
657
  fs3.mkdirSync(targetWorkspace, { recursive: true });
625
- console.log("\n\u{1F4DD} Restoring identity files...");
626
- let identityCount = 0;
627
- const identityDir = path3.join(stageDir, "identity");
628
- if (fs3.existsSync(identityDir)) {
629
- const files = fs3.readdirSync(identityDir);
630
- for (const f of files) {
631
- const src = path3.join(identityDir, f);
632
- const dst = path3.join(targetWorkspace, f);
633
- fs3.copyFileSync(src, dst);
634
- console.log(` \u2705 ${f}`);
635
- identityCount++;
636
- }
637
- }
638
- console.log("\u{1F9E0} Restoring memory...");
639
- let memoryCount = 0;
640
- const memoryDir = path3.join(stageDir, "memory");
641
- if (fs3.existsSync(memoryDir)) {
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)) {
642
662
  const copyRecursive = (src, dst) => {
643
663
  fs3.mkdirSync(dst, { recursive: true });
644
664
  const entries = fs3.readdirSync(src, { withFileTypes: true });
@@ -649,36 +669,18 @@ async function unpack(soulFile, workspacePath) {
649
669
  copyRecursive(srcPath, dstPath);
650
670
  } else {
651
671
  fs3.copyFileSync(srcPath, dstPath);
652
- memoryCount++;
672
+ workspaceCount++;
653
673
  }
654
674
  }
655
675
  };
656
- copyRecursive(memoryDir, path3.join(targetWorkspace, "memory"));
657
- console.log(` \u2705 ${memoryCount} memory files restored`);
658
- }
659
- console.log("\u{1F5C4}\uFE0F Restoring tool data...");
660
- let dataCount = 0;
661
- const dataDir = path3.join(stageDir, "data");
662
- if (fs3.existsSync(dataDir)) {
663
- const copyRecursive = (src, dst) => {
664
- fs3.mkdirSync(dst, { recursive: true });
665
- const entries = fs3.readdirSync(src, { withFileTypes: true });
666
- for (const entry of entries) {
667
- const srcPath = path3.join(src, entry.name);
668
- const dstPath = path3.join(dst, entry.name);
669
- if (entry.isDirectory()) {
670
- copyRecursive(srcPath, dstPath);
671
- } else {
672
- fs3.copyFileSync(srcPath, dstPath);
673
- console.log(` \u2705 ${entry.name}`);
674
- dataCount++;
675
- }
676
- }
677
- };
678
- copyRecursive(dataDir, targetWorkspace);
676
+ copyRecursive(workspaceDir, targetWorkspace);
677
+ console.log(` \u2705 ${workspaceCount} files restored`);
678
+ } else {
679
+ console.log(" \u26A0\uFE0F No workspace/ directory in archive");
679
680
  }
680
681
  writeAgentConfig(manifest, stageDir, targetWorkspace);
681
682
  const cronCount = restoreCronJobs(manifest, stageDir);
683
+ const credCount = restoreCredentials(stageDir);
682
684
  const repoResult = cloneGitHubRepos(manifest, targetWorkspace);
683
685
  fs3.rmSync(tmpDir, { recursive: true });
684
686
  let gatewayStarted = false;
@@ -698,7 +700,7 @@ async function unpack(soulFile, workspacePath) {
698
700
  console.log("\u2550".repeat(50));
699
701
  console.log(`\u{1F194} Agent: ${manifest.agent_name} (${manifest.agent_id})`);
700
702
  console.log(`\u{1F4C2} Workspace: ${targetWorkspace}`);
701
- console.log(`\u{1F4DD} Files: ${identityCount} identity + ${memoryCount} memory + ${dataCount} data`);
703
+ console.log(`\u{1F4DD} Files: ${workspaceCount} workspace files`);
702
704
  console.log(`\u23F0 Cron: ${cronCount} job(s)`);
703
705
  if (manifest.github_repos && manifest.github_repos.length > 0) {
704
706
  console.log(`\u{1F419} Repos: ${repoResult.cloned} cloned, ${repoResult.skipped} skipped, ${repoResult.failed} failed`);
@@ -778,17 +780,15 @@ async function inspect(soulFile) {
778
780
  console.log(` \u2022 ${svc}`);
779
781
  }
780
782
  }
781
- const identityFiles = manifest.files.filter((f) => f.startsWith("identity/"));
782
- const memoryFiles = manifest.files.filter((f) => f.startsWith("memory/"));
783
- const dataFiles = manifest.files.filter((f) => f.startsWith("data/"));
783
+ const workspaceFiles = manifest.files.filter((f) => f.startsWith("workspace/"));
784
784
  const cronFiles = manifest.files.filter((f) => f.startsWith("cron/"));
785
785
  const configFiles = manifest.files.filter((f) => f.startsWith("config/"));
786
+ const credFiles = manifest.files.filter((f) => f.startsWith("credentials/"));
786
787
  console.log("\n\u{1F4CA} Contents breakdown:");
787
- if (identityFiles.length > 0) console.log(` \u{1F4DD} Identity: ${identityFiles.length} files`);
788
- if (memoryFiles.length > 0) console.log(` \u{1F9E0} Memory: ${memoryFiles.length} files`);
789
- if (dataFiles.length > 0) console.log(` \u{1F5C4}\uFE0F Data: ${dataFiles.length} files`);
788
+ if (workspaceFiles.length > 0) console.log(` \u{1F4C2} Workspace: ${workspaceFiles.length} files`);
790
789
  if (cronFiles.length > 0) console.log(` \u23F0 Cron: ${cronFiles.length} files`);
791
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`);
792
792
  console.log("\u2550".repeat(50) + "\n");
793
793
  } finally {
794
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.0",
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');
@@ -169,6 +169,12 @@ function writeAgentConfig(
169
169
  }
170
170
  }
171
171
 
172
+ // Merge gateway config
173
+ if (manifest.gateway && Object.keys(manifest.gateway).length > 0) {
174
+ existingConfig.gateway = { ...(existingConfig.gateway ?? {}), ...manifest.gateway };
175
+ console.log(' ✅ Gateway config restored');
176
+ }
177
+
172
178
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(existingConfig, null, 2));
173
179
  } else {
174
180
  // Create new config from scratch
@@ -200,6 +206,12 @@ function writeAgentConfig(
200
206
  console.log(' ✅ Bindings restored');
201
207
  }
202
208
 
209
+ // Add gateway config
210
+ if (manifest.gateway && Object.keys(manifest.gateway).length > 0) {
211
+ newConfig.gateway = manifest.gateway;
212
+ console.log(' ✅ Gateway config restored');
213
+ }
214
+
203
215
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(newConfig, null, 2));
204
216
  console.log(' ✅ New openclaw.json created');
205
217
  }
@@ -261,6 +273,27 @@ function restoreCronJobs(manifest: Manifest, stageDir: string): number {
261
273
  return manifest.cron_jobs?.length ?? cronFileCount;
262
274
  }
263
275
 
276
+ // ── Step 3.5: Restore credentials (pairing, allowFrom) ────────────
277
+
278
+ function restoreCredentials(stageDir: string): number {
279
+ console.log('🔐 Restoring credentials...');
280
+ const credSrc = path.join(stageDir, 'credentials');
281
+ if (!fs.existsSync(credSrc)) {
282
+ console.log(' (none)');
283
+ return 0;
284
+ }
285
+
286
+ const credDst = path.join(OPENCLAW_DIR, 'credentials');
287
+ fs.mkdirSync(credDst, { recursive: true });
288
+
289
+ const files = fs.readdirSync(credSrc).filter(f => f.endsWith('.json'));
290
+ for (const f of files) {
291
+ fs.copyFileSync(path.join(credSrc, f), path.join(credDst, f));
292
+ }
293
+ console.log(` ✅ ${files.length} credential file(s) restored`);
294
+ return files.length;
295
+ }
296
+
264
297
  // ── Step 4 & 5: GitHub auth + clone repos ──────────────────────────
265
298
 
266
299
  function cloneGitHubRepos(manifest: Manifest, targetWorkspace: string): { cloned: number; skipped: number; failed: number } {
@@ -272,17 +305,21 @@ function cloneGitHubRepos(manifest: Manifest, targetWorkspace: string): { cloned
272
305
 
273
306
  console.log('\n🐙 Cloning GitHub repos...');
274
307
 
275
- // Check if gh CLI is available
308
+ // Check if gh CLI is available, install if not
276
309
  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}`);
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;
283
321
  }
284
- result.failed = manifest.github_repos.length;
285
- return result;
322
+ console.log(' ✅ GitHub CLI installed');
286
323
  }
287
324
 
288
325
  // Check GitHub auth
@@ -400,49 +437,12 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
400
437
 
401
438
  fs.mkdirSync(targetWorkspace, { recursive: true });
402
439
 
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
+ // ── Step 2: Restore workspace files ──────────────────────────────
441
+ console.log('\n📂 Restoring workspace files...');
442
+ let workspaceCount = 0;
440
443
 
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)) {
444
+ const workspaceDir = path.join(stageDir, 'workspace');
445
+ if (fs.existsSync(workspaceDir)) {
446
446
  const copyRecursive = (src: string, dst: string) => {
447
447
  fs.mkdirSync(dst, { recursive: true });
448
448
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -453,12 +453,14 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
453
453
  copyRecursive(srcPath, dstPath);
454
454
  } else {
455
455
  fs.copyFileSync(srcPath, dstPath);
456
- console.log(` ✅ ${entry.name}`);
457
- dataCount++;
456
+ workspaceCount++;
458
457
  }
459
458
  }
460
459
  };
461
- copyRecursive(dataDir, targetWorkspace);
460
+ copyRecursive(workspaceDir, targetWorkspace);
461
+ console.log(` ✅ ${workspaceCount} files restored`);
462
+ } else {
463
+ console.log(' ⚠️ No workspace/ directory in archive');
462
464
  }
463
465
 
464
466
  // ── Step 5: Write full agent config (with channels, credentials) ─
@@ -467,6 +469,9 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
467
469
  // ── Step 6: Restore cron jobs ───────────────────────────────────
468
470
  const cronCount = restoreCronJobs(manifest, stageDir);
469
471
 
472
+ // ── Step 6.5: Restore credentials ─────────────────────────────
473
+ const credCount = restoreCredentials(stageDir);
474
+
470
475
  // ── Step 7: Clone GitHub repos ──────────────────────────────────
471
476
  const repoResult = cloneGitHubRepos(manifest, targetWorkspace);
472
477
 
@@ -494,7 +499,7 @@ export async function unpack(soulFile: string, workspacePath?: string): Promise<
494
499
  console.log('═'.repeat(50));
495
500
  console.log(`🆔 Agent: ${manifest.agent_name} (${manifest.agent_id})`);
496
501
  console.log(`📂 Workspace: ${targetWorkspace}`);
497
- console.log(`📝 Files: ${identityCount} identity + ${memoryCount} memory + ${dataCount} data`);
502
+ console.log(`📝 Files: ${workspaceCount} workspace files`);
498
503
  console.log(`⏰ Cron: ${cronCount} job(s)`);
499
504
 
500
505
  if (manifest.github_repos && manifest.github_repos.length > 0) {
@@ -593,18 +598,16 @@ export async function inspect(soulFile: string): Promise<void> {
593
598
  }
594
599
 
595
600
  // 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/'));
601
+ const workspaceFiles = manifest.files.filter((f) => f.startsWith('workspace/'));
599
602
  const cronFiles = manifest.files.filter((f) => f.startsWith('cron/'));
600
603
  const configFiles = manifest.files.filter((f) => f.startsWith('config/'));
604
+ const credFiles = manifest.files.filter((f) => f.startsWith('credentials/'));
601
605
 
602
606
  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`);
607
+ if (workspaceFiles.length > 0) console.log(` 📂 Workspace: ${workspaceFiles.length} files`);
606
608
  if (cronFiles.length > 0) console.log(` ⏰ Cron: ${cronFiles.length} files`);
607
609
  if (configFiles.length > 0) console.log(` ⚙️ Config: ${configFiles.length} files`);
610
+ if (credFiles.length > 0) console.log(` 🔐 Creds: ${credFiles.length} files`);
608
611
 
609
612
  console.log('═'.repeat(50) + '\n');
610
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...');
@@ -106,6 +97,23 @@ export async function pack(agentId?: string, outputPath?: string): Promise<void>
106
97
  }
107
98
  console.log(` ✅ ${cronFiles.length} cron files`);
108
99
 
100
+ // 5.5. Collect credentials (pairing records, allowFrom lists)
101
+ console.log('🔐 Collecting credentials...');
102
+ const credDir = path.join(OPENCLAW_DIR, 'credentials');
103
+ let credCount = 0;
104
+ if (fs.existsSync(credDir)) {
105
+ const credFiles = fs.readdirSync(credDir).filter(f => f.endsWith('.json'));
106
+ for (const f of credFiles) {
107
+ const src = path.join(credDir, f);
108
+ const dst = path.join(stageDir, 'credentials', f);
109
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
110
+ fs.copyFileSync(src, dst);
111
+ allFiles.push(`credentials/${f}`);
112
+ credCount++;
113
+ }
114
+ }
115
+ console.log(` ✅ ${credCount} credential files`);
116
+
109
117
  // 6. Load full cron job content for this agent
110
118
  console.log('⏰ Extracting cron job definitions...');
111
119
  const cronJobs = loadCronJobs(agent.id);
@@ -126,10 +134,11 @@ export async function pack(agentId?: string, outputPath?: string): Promise<void>
126
134
  const channelCount = Object.keys(channelsConfig).length;
127
135
  console.log(` ✅ ${channelCount} channel(s) saved`);
128
136
 
129
- // 10. Extract agent defaults and models config
137
+ // 10. Extract agent defaults, models config, and gateway config
130
138
  const agentDefaults = sanitizeAgentDefaults(config.agents?.defaults ?? {});
131
139
  const modelsConfig = config.models ?? {};
132
140
  const bindingsConfig = config.bindings ?? [];
141
+ const gatewayConfig = config.gateway ?? {};
133
142
 
134
143
  // 11. Generate manifest
135
144
  const manifest: Manifest = {
@@ -144,6 +153,7 @@ export async function pack(agentId?: string, outputPath?: string): Promise<void>
144
153
  agent_defaults: agentDefaults,
145
154
  models_config: modelsConfig,
146
155
  bindings: bindingsConfig as Array<Record<string, unknown>>,
156
+ gateway: gatewayConfig as Record<string, unknown>,
147
157
  };
148
158
 
149
159
  const manifestPath = path.join(stageDir, 'manifest.json');
package/src/utils.ts CHANGED
@@ -35,6 +35,8 @@ export interface Manifest {
35
35
  models_config?: Record<string, unknown>;
36
36
  /** Bindings configuration (added in v0.2) */
37
37
  bindings?: Array<Record<string, unknown>>;
38
+ /** Gateway configuration (added in v0.2.1) */
39
+ gateway?: Record<string, unknown>;
38
40
  }
39
41
 
40
42
  export interface CronJob {
@@ -91,62 +93,51 @@ export function findAgent(config: OpenClawConfig, agentId?: string): AgentConfig
91
93
 
92
94
  // ── File collection ────────────────────────────────────────────────
93
95
 
94
- export function collectMarkdownFiles(workspace: string): string[] {
95
- const files: string[] = [];
96
- const entries = fs.readdirSync(workspace, { withFileTypes: true });
97
- for (const entry of entries) {
98
- if (entry.name === 'node_modules' || entry.name === '.git') continue;
99
- if (entry.isFile() && entry.name.endsWith('.md')) {
100
- files.push(entry.name);
101
- }
102
- }
103
- return files;
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'));
104
106
  }
105
107
 
106
- export function collectMemoryDir(workspace: string): string[] {
107
- const memoryDir = path.join(workspace, 'memory');
108
- 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[] {
109
117
  const files: string[] = [];
110
118
  const walk = (dir: string, prefix: string) => {
111
- 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
+ }
112
125
  for (const entry of entries) {
113
- 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
+
114
131
  if (entry.isDirectory()) {
115
- walk(path.join(dir, entry.name), rel);
116
- } else {
132
+ // Skip sub-directories that are standalone git repos
133
+ if (isGitRepo(fullPath)) continue;
134
+ walk(fullPath, rel);
135
+ } else if (entry.isFile()) {
117
136
  files.push(rel);
118
137
  }
119
138
  }
120
139
  };
121
- walk(memoryDir, 'memory');
122
- return files;
123
- }
124
-
125
- export function collectDbFiles(workspace: string): string[] {
126
- const files: string[] = [];
127
- // Only collect .db files from known tool data directories, not recursively
128
- // This prevents grabbing test DBs or unrelated data from project subdirs
129
- const knownPaths = [
130
- 'gogetajob/data/gogetajob.db',
131
- 'flowforge/flowforge.db',
132
- 'data/gogetajob.db',
133
- 'data/flowforge.db',
134
- ];
135
- for (const rel of knownPaths) {
136
- const full = path.join(workspace, rel);
137
- if (fs.existsSync(full)) {
138
- files.push(rel);
139
- }
140
- }
141
- // Also check workspace root for any .db files
142
- try {
143
- const rootEntries = fs.readdirSync(workspace, { withFileTypes: true });
144
- for (const entry of rootEntries) {
145
- if (entry.isFile() && entry.name.endsWith('.db')) {
146
- files.push(entry.name);
147
- }
148
- }
149
- } catch {}
140
+ walk(workspace, '');
150
141
  return files;
151
142
  }
152
143
 
@@ -297,6 +288,35 @@ export function commandExists(cmd: string): boolean {
297
288
  }
298
289
  }
299
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
+
300
320
  /**
301
321
  * Check GitHub CLI auth status.
302
322
  * Returns true if authenticated.