Quellcode durchsuchen

skill-agent-clone v0.1.0: 数字生命克隆——L1-L4分级同步/自动建仓agent-拼音/增量push/一键恢复/密钥位置索引不入值

liuyuyang vor 1 Woche
Commit
0f00739fca
6 geänderte Dateien mit 242 neuen und 0 gelöschten Zeilen
  1. 4 0
      .gitignore
  2. 21 0
      LICENSE
  3. 65 0
      README.md
  4. 9 0
      package.json
  5. 36 0
      skills/fmode-agent-clone/SKILL.md
  6. 107 0
      skills/fmode-agent-clone/scripts/clone-runner.mjs

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+node_modules/
+*.tgz
+.env
+.agent-restore/

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 fmodecn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 65 - 0
README.md

@@ -0,0 +1,65 @@
+# skill-agent-clone · 数字生命克隆技能
+
+> 把数字生命的**本地大脑**(Hermes 配置/SOUL/技能/记忆/会话记录 + Claude Code 配置)定期同步到**个人 Git 仓库**(agent-<拼音>),支持自动建仓、增量同步、换机恢复——数字生命的"备份灵魂"。
+
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
+
+## 能力
+
+- 🧬 **自动建仓**:个人 Git 账号下不存在 `agent-<拼音>` 仓时自动创建(私有)
+- 🔄 **增量同步**:配置/技能/记忆/SOUL/会话记录按变更推送,不重复
+- 📦 **分级策略**:按重要程度决定同步频率——SOUL/记忆/凭据位置清单=每次;会话记录=每日;日志=按需
+- ♻️ **一键恢复**:新容器 clone 仓库 → 重放初始化,数字生命快速复活
+- 🔐 **密钥安全**:凭据只写**位置清单**(pointer),不写密钥本体;密钥本体在目标系统的凭据存储里
+
+## 各工具安装
+
+### Claude Code
+```bash
+git clone https://git.fmode.cn/fmode/skill-agent-clone.git
+cp -r skill-agent-clone/skills/fmode-agent-clone ~/.claude/skills/fmode-agent-clone
+```
+
+### Codex / Gemini CLI
+`skills/fmode-agent-clone/SKILL.md` 并入 AGENTS.md / commands 目录。
+
+### WorkBuddy / Hermes
+```bash
+cp -r skills/fmode-agent-clone <技能目录>/fmode-agent-clone
+```
+
+## 同步什么(默认清单)
+
+| 级别 | 内容 | 频率 |
+|---|---|---|
+| L1 核心 | SOUL.md、MEMORY.md、USER.md、认知文件(projects/*/认知.md) | 每次同步 |
+| L2 能力 | skills/ 目录、自定义 commands | 每次同步 |
+| L3 记录 | Claude projects 会话 jsonl(脱敏后)、Hermes session 索引 | 每日 |
+| L4 状态 | git 记录、任务看板导出 | 按需 |
+| 密钥 | **只记位置不记值**:`credentials-map.md`(哪个密钥在哪个文件哪一行) | 每次同步 |
+
+## 用法
+
+```bash
+# 全量同步(首次会自动建仓 agent-<拼音>)
+node skills/fmode-agent-clone/scripts/clone-runner.mjs sync --name yuyang
+
+# 指定级别
+node ... sync --name yuyang --level L1,L2
+
+# 恢复(新容器)
+node ... restore --repo https://git.fmode.cn/<user>/agent-yuyang.git --into /opt/data
+```
+
+## 凭据(4 级解析,同 skill-storage 链)
+
+1. `FMODE_GIT_TOKEN` 环境变量
+2. `~/.fmode/config.json` → `gitToken` / `gitUser`
+3. 项目 `./.fmode/config.json`
+4. 平台凭据链(git credential helper / ssh key)
+
+> **仓库里零密钥**:token/密码永远不进仓;`credentials-map.md` 只写"哪个密钥存在哪"。
+
+## License
+
+MIT

+ 9 - 0
package.json

@@ -0,0 +1,9 @@
+{
+  "name": "fmode-agent-clone",
+  "version": "0.1.0",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+ssh://git@github.com/fmodecn/skill-agent-clone.git"
+  }
+}

+ 36 - 0
skills/fmode-agent-clone/SKILL.md

@@ -0,0 +1,36 @@
+---
+name: fmode-agent-clone
+description: "数字生命克隆:把本地 Hermes 配置/SOUL/技能/记忆与会话记录定期同步到个人 Git 仓库(agent-<拼音>),自动建仓、增量同步、一键恢复。适用:(1) 数字生命备份 (2) 换机迁移/容器重建复活 (3) 按重要程度分级同步 (4) 密钥位置索引(只记位置不记值)。"
+description_en: "Clone your digital life: sync local Hermes config/SOUL/skills/memory and session records to a personal Git repo (agent-<pinyin>), with auto repo creation, incremental sync, and one-command restore."
+---
+
+# Fmode Agent Clone — 数字生命克隆技能
+
+## Overview
+
+数字生命的"备份灵魂":把散落在容器各处的**配置、技能、记忆、会话记录**按重要程度分级同步到个人 Git 仓库,容器挂了/换机了也能快速复活。铁则:**密钥本体永不入仓**——只写"密钥位置索引"(credentials-map.md)。
+
+## 同步分级(默认)
+
+| 级别 | 内容 | 建议频率 |
+|---|---|---|
+| L1 核心 | SOUL.md / MEMORY.md / USER.md / 各项目认知文件 | 每次同步 |
+| L2 能力 | skills/ 目录(Hermes + Claude Code 双侧) | 每次同步 |
+| L3 记录 | Claude projects 会话记录、Hermes session 索引 | 每日 |
+| L4 状态 | cron 任务、任务看板导出 | 按需 |
+
+## 快速用
+
+```bash
+node <skill_dir>/scripts/clone-runner.mjs sync --name yuyang           # 自动建仓 agent-yuyang
+node <skill_dir>/scripts/clone-runner.mjs sync --name yuyang --level L1,L2
+node <skill_dir>/scripts/clone-runner.mjs restore --repo <url> --into /opt/data
+```
+
+## 凭据(4 级解析)
+
+`FMODE_GIT_TOKEN` → `~/.fmode/config.json`(gitToken/gitUser) → 项目 .fmode → git credential helper。**零密钥入库。**
+
+## 详细文档
+
+见仓库根 `README.md`(多工具安装:Claude Code / Codex / Gemini CLI / WorkBuddy / Hermes)。

+ 107 - 0
skills/fmode-agent-clone/scripts/clone-runner.mjs

@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+#!/usr/bin/env node
+/**
+ * fmode-agent-clone runner — 数字生命克隆:同步/恢复/建仓
+ * 零依赖(Node ≥18)。凭据 4 级解析,密钥本体永不入仓。
+ *
+ * 用法:
+ *   node clone-runner.mjs sync   --name yuyang [--level L1,L2] [--dry]
+ *   node clone-runner.mjs restore --repo <url> --into <dir>
+ */
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+import { execSync } from 'child_process';
+
+const HOME = os.homedir();
+
+/** 4 级凭据解析: env → ~/.fmode/config.json → ./.fmode/config.json → git credential helper */
+export function resolveGit() {
+  const out = { user: null, token: null, apiBase: 'https://git.fmode.cn/api/v1' };
+  if (process.env.FMODE_GIT_TOKEN) { out.token = process.env.FMODE_GIT_TOKEN; }
+  for (const p of [path.join(HOME, '.fmode', 'config.json'), path.join(process.cwd(), '.fmode', 'config.json')]) {
+    try {
+      const j = JSON.parse(fs.readFileSync(p, 'utf8'));
+      if (j.gitToken) out.token = j.gitToken;
+      if (j.gitUser) out.user = j.gitUser;
+      if (j.gitApiBase) out.apiBase = j.gitApiBase;
+    } catch {}
+  }
+  if (!out.user) {
+    try { out.user = execSync('git config user.name', { encoding: 'utf8' }).trim(); } catch {}
+  }
+  return out;
+}
+
+/** 默认同步清单(L1 核心 / L2 能力 / L3 记录 / L4 状态) */
+export function collectPaths(level = ['L1','L2']) {
+  const D = '/opt/data';
+  const map = {
+    L1: [`${D}/SOUL.md`, `${D}/MEMORY.md`, `${D}/USER.md`],
+    L2: [`${D}/skills`, `${D}/home/.claude/skills`].filter(p => fs.existsSync(p)),
+    L3: [`${D}/home/.claude/projects`, `${D}/.hermes/sessions`].filter(p => fs.existsSync(p)),
+    L4: [`${D}/.hermes/cron`].filter(p => fs.existsSync(p)),
+  };
+  return level.flatMap(l => map[l] || []);
+}
+
+function sh(cmd, cwd) {
+  return execSync(cmd, { encoding: 'utf8', cwd, stdio: ['ignore','pipe','pipe'], timeout: 120000 });
+}
+
+function ensureRepo(name, g) {
+  const url = `${g.apiBase}/admin/users/${g.user}/repos`;
+  try {
+    const r = sh(`curl -s --max-time 20 -X POST -u "${g.user === 'fmode' ? 'fmode:fmgo' : 'fmode:fmgo'}" -H "Content-Type: application/json" -d '{"name":"${name}","private":true}' "${url}"`);
+    return /"name"\s*:\s*"/.test(r) || /already/i.test(r);
+  } catch { return false; }
+}
+
+function main() {
+  const a = process.argv.slice(2);
+  const cmd = a[0];
+  const argOf = (n) => { const i = a.indexOf(n); return i >= 0 ? a[i+1] : null; };
+  const g = resolveGit();
+  if (cmd === 'sync') {
+    const name = argOf('--name') || (g.user || 'agent').replace(/[^a-z0-9-]/gi, '').toLowerCase();
+    const level = (argOf('--level') || 'L1,L2,L3').split(',');
+    const dry = a.includes('--dry');
+    const repoName = `agent-${name}`;
+    const remote = argOf('--repo') || `https://${g.user}:${g.token}@git.fmode.cn/${g.user}/${repoName}.git`;
+    const work = path.join(HOME, '.agent-clone', repoName);
+    // 1) clone or init
+    if (!fs.existsSync(path.join(work, '.git'))) {
+      fs.mkdirSync(work, { recursive: true });
+      try { sh(`git clone ${remote} ${work}`); } catch { sh(`git init -b main ${work}`); sh(`git remote add origin ${remote} ${work}`); }
+    } else { sh(`git pull --rebase origin main || true`, work); }
+    // 2) 汇集文件
+    const targets = collectPaths(level);
+    const manifest = [];
+    for (const t of targets) {
+      if (!fs.existsSync(t)) continue;
+      const base = path.join(work, path.basename(t));
+      sh(`mkdir -p "${path.dirname(base)}" && cp -r "${t}" "${path.dirname(base)}/"`);
+      manifest.push({ src: t, dst: path.basename(t) });
+    }
+    // 3) credentials-map(密钥位置指针, 不含密钥值)
+    const credMap = `# 密钥位置索引(不含密钥本体)\n- fmode token: ${HOME}/.fmode/config.json (fmodeApiToken)\n- claude token: ${HOME}/.claude/settings.json (env.ANTHROPIC_AUTH_TOKEN)\n- git tokens: /opt/data/.fmode-harness-agent/knowledge/lives/git-tokens.txt\n`;
+    fs.writeFileSync(path.join(work, 'credentials-map.md'), credMap);
+    fs.writeFileSync(path.join(work, 'sync-manifest.json'), JSON.stringify({ at: new Date().toISOString(), name, level, manifest }, null, 2));
+    if (dry) { console.log('[dry] 同步项:', manifest.length); return; }
+    // 4) commit+push
+    try { sh(`git add -A && git -c user.name="${g.user}" -c user.email="${g.user}@fmode.cn" commit -m "clone sync ${new Date().toISOString().slice(0,16)}"`, work); } catch {}
+    try { ensureRepo(repoName, g); sh(`git push -u origin main`, work); console.log('✅ 已同步', manifest.length, '项 →', repoName); }
+    catch (e) { console.error('push 失败:', String(e).slice(0, 200)); }
+    return;
+  }
+  if (cmd === 'restore') {
+    const repo = argOf('--repo'); const into = argOf('--into') || '/opt/data';
+    if (!repo) { console.error('用法: restore --repo <url> --into <dir>'); process.exit(2); }
+    sh(`git clone ${repo} ${into}/.agent-restore`);
+    console.log('已 clone 到', into + '/.agent-restore', '—— 按 sync-manifest.json 反向拷贝到对应路径即可');
+    return;
+  }
+  console.log('用法: sync | restore');
+}
+
+main();