@ectplsm/relic 0.2.0 → 0.2.2

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.
@@ -13,41 +13,95 @@ export class Extract {
13
13
  constructor(repository) {
14
14
  this.repository = repository;
15
15
  }
16
+ async inspectPersona(agentName, options) {
17
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
18
+ if (!existsSync(sourcePath)) {
19
+ throw new WorkspaceNotFoundError(sourcePath);
20
+ }
21
+ const { files } = await this.readFiles(sourcePath);
22
+ const existing = await this.repository.get(agentName);
23
+ if (!existing) {
24
+ return {
25
+ engramId: agentName,
26
+ engramName: options?.name ?? agentName,
27
+ sourcePath,
28
+ existing: false,
29
+ name: "missing",
30
+ soul: "missing",
31
+ identity: "missing",
32
+ overwriteRequired: false,
33
+ };
34
+ }
35
+ const requestedName = options?.name ?? existing.meta.name;
36
+ const name = requestedName === existing.meta.name ? "same" : "different";
37
+ const soul = this.comparePersonaFile(existing.files.soul, files.soul);
38
+ const identity = this.comparePersonaFile(existing.files.identity, files.identity);
39
+ return {
40
+ engramId: agentName,
41
+ engramName: existing.meta.name,
42
+ sourcePath,
43
+ existing: true,
44
+ name,
45
+ soul,
46
+ identity,
47
+ overwriteRequired: name === "different" ||
48
+ soul === "different" ||
49
+ identity === "different",
50
+ };
51
+ }
16
52
  async execute(agentName, options) {
17
53
  const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
18
54
  if (!existsSync(sourcePath)) {
19
55
  throw new WorkspaceNotFoundError(sourcePath);
20
56
  }
21
- // 既存Engramがあればエラー — Relic側が真のデータソース
22
57
  const existing = await this.repository.get(agentName);
23
- if (existing) {
58
+ if (existing && !options?.force) {
24
59
  throw new AlreadyExtractedError(agentName);
25
60
  }
26
61
  const { files, filesRead } = await this.readFiles(sourcePath);
27
62
  if (filesRead.length === 0) {
28
63
  throw new WorkspaceEmptyError(sourcePath);
29
64
  }
30
- const engramName = options?.name ?? agentName;
31
65
  const now = new Date().toISOString();
32
- const engram = {
33
- meta: {
34
- id: agentName,
35
- name: engramName,
36
- description: `Extracted from OpenClaw workspace (${agentName})`,
37
- createdAt: now,
38
- updatedAt: now,
39
- tags: ["extracted", "openclaw"],
40
- },
41
- files,
42
- };
66
+ const engram = existing && options?.force
67
+ ? {
68
+ meta: {
69
+ ...existing.meta,
70
+ name: options?.name ?? existing.meta.name,
71
+ updatedAt: now,
72
+ },
73
+ // Force extract only replaces persona files from Claw.
74
+ files: {
75
+ ...existing.files,
76
+ soul: files.soul ?? existing.files.soul,
77
+ identity: files.identity ?? existing.files.identity,
78
+ },
79
+ }
80
+ : {
81
+ meta: {
82
+ id: agentName,
83
+ name: options?.name ?? agentName,
84
+ description: `Extracted from OpenClaw workspace (${agentName})`,
85
+ createdAt: now,
86
+ updatedAt: now,
87
+ tags: ["extracted", "openclaw"],
88
+ },
89
+ files,
90
+ };
43
91
  await this.repository.save(engram);
44
92
  return {
45
93
  engramId: agentName,
46
- engramName,
94
+ engramName: engram.meta.name,
47
95
  sourcePath,
48
96
  filesRead,
49
97
  };
50
98
  }
99
+ comparePersonaFile(currentContent, incomingContent) {
100
+ if (currentContent === undefined || incomingContent === undefined) {
101
+ return "missing";
102
+ }
103
+ return currentContent === incomingContent ? "same" : "different";
104
+ }
51
105
  async readFiles(sourcePath) {
52
106
  const files = {};
53
107
  const filesRead = [];
@@ -88,7 +142,7 @@ export class WorkspaceEmptyError extends Error {
88
142
  }
89
143
  export class AlreadyExtractedError extends Error {
90
144
  constructor(id) {
91
- super(`Engram "${id}" already exists. Relic is the source of truth use "relic claw inject" to push changes.`);
145
+ super(`Engram "${id}" already exists. Re-run with "--force" option to overwrite local persona files from the Claw workspace.`);
92
146
  this.name = "AlreadyExtractedError";
93
147
  }
94
148
  }
@@ -1,8 +1,10 @@
1
1
  export { Summon, EngramNotFoundError, type SummonResult } from "./summon.js";
2
2
  export { ListEngrams } from "./list-engrams.js";
3
3
  export { Init, type InitResult } from "./init.js";
4
- export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectResult, } from "./inject.js";
5
- export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractResult, } from "./extract.js";
4
+ export { MigrateEngrams, type MigrateEngramsResult, } from "./migrate-engrams.js";
5
+ export { RefreshSamples, type RefreshSamplesResult, } from "./refresh-samples.js";
6
+ export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectPersonaFileDiff, type InjectPersonaDiffResult, type InjectResult, } from "./inject.js";
7
+ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractPersonaFileDiff, type ExtractPersonaDiffResult, type ExtractResult, } from "./extract.js";
6
8
  export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
7
9
  export { Sync, SyncOpenclawDirNotFoundError, type SyncTarget, type SyncResult, type SyncInitialResult, } from "./sync.js";
8
10
  export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
@@ -1,6 +1,8 @@
1
1
  export { Summon, EngramNotFoundError } from "./summon.js";
2
2
  export { ListEngrams } from "./list-engrams.js";
3
3
  export { Init } from "./init.js";
4
+ export { MigrateEngrams, } from "./migrate-engrams.js";
5
+ export { RefreshSamples, } from "./refresh-samples.js";
4
6
  export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, } from "./inject.js";
5
7
  export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, } from "./extract.js";
6
8
  export { MemoryWrite, MemoryWriteEngramNotFoundError, } from "./memory-write.js";
@@ -1,4 +1,13 @@
1
1
  import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export type InjectPersonaFileDiff = "missing" | "same" | "different" | "skipped";
3
+ export interface InjectPersonaDiffResult {
4
+ engramId: string;
5
+ engramName: string;
6
+ targetPath: string;
7
+ soul: InjectPersonaFileDiff;
8
+ identity: InjectPersonaFileDiff;
9
+ overwriteRequired: boolean;
10
+ }
2
11
  export interface InjectResult {
3
12
  engramId: string;
4
13
  engramName: string;
@@ -15,11 +24,17 @@ export interface InjectResult {
15
24
  export declare class Inject {
16
25
  private readonly repository;
17
26
  constructor(repository: EngramRepository);
27
+ inspectPersona(engramId: string, options?: {
28
+ openclawDir?: string;
29
+ mergeIdentity?: boolean;
30
+ }): Promise<InjectPersonaDiffResult>;
18
31
  execute(engramId: string, options?: {
19
- to?: string;
20
32
  openclawDir?: string;
21
33
  mergeIdentity?: boolean;
22
34
  }): Promise<InjectResult>;
35
+ private loadInjectTarget;
36
+ private resolveInjectedContent;
37
+ private compareTargetFile;
23
38
  private writeFiles;
24
39
  }
25
40
  export declare class InjectEngramNotFoundError extends Error {
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { writeFile } from "node:fs/promises";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
4
  import { INJECT_FILE_MAP, resolveWorkspacePath } from "../../shared/openclaw.js";
5
5
  /**
6
6
  * Inject — EngramのファイルをOpenClawワークスペースに注入する
@@ -14,27 +14,65 @@ export class Inject {
14
14
  constructor(repository) {
15
15
  this.repository = repository;
16
16
  }
17
+ async inspectPersona(engramId, options) {
18
+ const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
19
+ const mergeIdentity = options?.mergeIdentity ?? false;
20
+ const soul = await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.soul), this.resolveInjectedContent("soul", engram.files, mergeIdentity));
21
+ const identity = mergeIdentity
22
+ ? "skipped"
23
+ : await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.identity), this.resolveInjectedContent("identity", engram.files, mergeIdentity));
24
+ return {
25
+ engramId: engram.meta.id,
26
+ engramName: engram.meta.name,
27
+ targetPath,
28
+ soul,
29
+ identity,
30
+ overwriteRequired: soul === "different" || identity === "different",
31
+ };
32
+ }
17
33
  async execute(engramId, options) {
34
+ const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
35
+ const filesWritten = await this.writeFiles(targetPath, engram.files, options?.mergeIdentity ?? false);
36
+ return {
37
+ engramId: engram.meta.id,
38
+ engramName: engram.meta.name,
39
+ targetPath,
40
+ filesWritten,
41
+ };
42
+ }
43
+ async loadInjectTarget(engramId, options) {
18
44
  const engram = await this.repository.get(engramId);
19
45
  if (!engram) {
20
46
  throw new InjectEngramNotFoundError(engramId);
21
47
  }
22
- // ベースディレクトリ(--dir)の存在チェック
23
48
  if (options?.openclawDir && !existsSync(options.openclawDir)) {
24
49
  throw new InjectClawDirNotFoundError(options.openclawDir);
25
50
  }
26
- const agentName = options?.to ?? engramId;
27
- const targetPath = resolveWorkspacePath(agentName, options?.openclawDir);
51
+ const targetPath = resolveWorkspacePath(engramId, options?.openclawDir);
28
52
  if (!existsSync(targetPath)) {
29
- throw new InjectWorkspaceNotFoundError(agentName);
53
+ throw new InjectWorkspaceNotFoundError(engramId);
30
54
  }
31
- const filesWritten = await this.writeFiles(targetPath, engram.files, options?.mergeIdentity ?? false);
32
- return {
33
- engramId: engram.meta.id,
34
- engramName: engram.meta.name,
35
- targetPath,
36
- filesWritten,
37
- };
55
+ return { engram, targetPath };
56
+ }
57
+ resolveInjectedContent(key, files, mergeIdentity) {
58
+ if (mergeIdentity && key === "identity") {
59
+ return undefined;
60
+ }
61
+ let content = files[key];
62
+ if (mergeIdentity && key === "soul" && files.identity) {
63
+ content = content + "\n" + files.identity;
64
+ }
65
+ return content;
66
+ }
67
+ async compareTargetFile(filePath, expectedContent) {
68
+ if (expectedContent === undefined) {
69
+ return "skipped";
70
+ }
71
+ if (!existsSync(filePath)) {
72
+ return "missing";
73
+ }
74
+ const currentContent = await readFile(filePath, "utf-8");
75
+ return currentContent === expectedContent ? "same" : "different";
38
76
  }
39
77
  async writeFiles(targetPath, files, mergeIdentity) {
40
78
  const written = [];
@@ -43,11 +81,7 @@ export class Inject {
43
81
  if (mergeIdentity && key === "identity") {
44
82
  continue;
45
83
  }
46
- let content = files[key];
47
- // --merge-identity: append IDENTITY.md content to SOUL.md
48
- if (mergeIdentity && key === "soul" && files.identity) {
49
- content = content + "\n" + files.identity;
50
- }
84
+ const content = this.resolveInjectedContent(key, files, mergeIdentity);
51
85
  if (content !== undefined) {
52
86
  await writeFile(join(targetPath, filename), content, "utf-8");
53
87
  written.push(filename);
@@ -0,0 +1,18 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export interface MigrateEngramsResult {
3
+ migrated: string[];
4
+ alreadyUpToDate: string[];
5
+ skipped: Array<{
6
+ id: string;
7
+ reason: string;
8
+ }>;
9
+ }
10
+ /**
11
+ * MigrateEngrams — 既存Engramを走査し、新形式の manifest.json へ前倒し移行する
12
+ */
13
+ export declare class MigrateEngrams {
14
+ private readonly repository;
15
+ private readonly basePath;
16
+ constructor(repository: EngramRepository, basePath: string);
17
+ execute(): Promise<MigrateEngramsResult>;
18
+ }
@@ -0,0 +1,49 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ /**
5
+ * MigrateEngrams — 既存Engramを走査し、新形式の manifest.json へ前倒し移行する
6
+ */
7
+ export class MigrateEngrams {
8
+ repository;
9
+ basePath;
10
+ constructor(repository, basePath) {
11
+ this.repository = repository;
12
+ this.basePath = basePath;
13
+ }
14
+ async execute() {
15
+ if (!existsSync(this.basePath)) {
16
+ return { migrated: [], alreadyUpToDate: [], skipped: [] };
17
+ }
18
+ const entries = await readdir(this.basePath, { withFileTypes: true });
19
+ const dirs = entries.filter((entry) => entry.isDirectory());
20
+ const result = {
21
+ migrated: [],
22
+ alreadyUpToDate: [],
23
+ skipped: [],
24
+ };
25
+ for (const dir of dirs) {
26
+ const id = dir.name;
27
+ const engramDir = join(this.basePath, id);
28
+ const profilePath = join(engramDir, "engram.json");
29
+ const manifestPath = join(engramDir, "manifest.json");
30
+ if (!existsSync(profilePath)) {
31
+ result.skipped.push({ id, reason: "engram.json not found" });
32
+ continue;
33
+ }
34
+ const hadManifest = existsSync(manifestPath);
35
+ const engram = await this.repository.get(id);
36
+ if (!engram) {
37
+ result.skipped.push({ id, reason: "failed to load engram metadata" });
38
+ continue;
39
+ }
40
+ if (!hadManifest && existsSync(manifestPath)) {
41
+ result.migrated.push(id);
42
+ }
43
+ else {
44
+ result.alreadyUpToDate.push(id);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export interface RefreshSamplesResult {
3
+ refreshed: string[];
4
+ skipped: Array<{
5
+ id: string;
6
+ reason: string;
7
+ }>;
8
+ }
9
+ /**
10
+ * RefreshSamples — サンプルEngramの人格ファイルを最新テンプレートで上書きする
11
+ *
12
+ * memory / USER / archive などの運用データは保持し、
13
+ * SOUL.md / IDENTITY.md と updatedAt のみを更新する。
14
+ */
15
+ export declare class RefreshSamples {
16
+ private readonly repository;
17
+ private readonly templatesDir;
18
+ constructor(repository: EngramRepository, templatesDir: string);
19
+ execute(targetIds?: string[]): Promise<RefreshSamplesResult>;
20
+ private listTemplateIds;
21
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ /**
5
+ * RefreshSamples — サンプルEngramの人格ファイルを最新テンプレートで上書きする
6
+ *
7
+ * memory / USER / archive などの運用データは保持し、
8
+ * SOUL.md / IDENTITY.md と updatedAt のみを更新する。
9
+ */
10
+ export class RefreshSamples {
11
+ repository;
12
+ templatesDir;
13
+ constructor(repository, templatesDir) {
14
+ this.repository = repository;
15
+ this.templatesDir = templatesDir;
16
+ }
17
+ async execute(targetIds) {
18
+ const ids = targetIds && targetIds.length > 0
19
+ ? targetIds
20
+ : await this.listTemplateIds();
21
+ const result = {
22
+ refreshed: [],
23
+ skipped: [],
24
+ };
25
+ for (const id of ids) {
26
+ const templateDir = join(this.templatesDir, id);
27
+ if (!existsSync(templateDir)) {
28
+ result.skipped.push({ id, reason: "sample template not found" });
29
+ continue;
30
+ }
31
+ const engram = await this.repository.get(id);
32
+ if (!engram) {
33
+ result.skipped.push({ id, reason: "local Engram not found" });
34
+ continue;
35
+ }
36
+ const soulPath = join(templateDir, "SOUL.md");
37
+ const identityPath = join(templateDir, "IDENTITY.md");
38
+ if (!existsSync(soulPath) || !existsSync(identityPath)) {
39
+ result.skipped.push({ id, reason: "template persona files are incomplete" });
40
+ continue;
41
+ }
42
+ engram.files.soul = await readFile(soulPath, "utf-8");
43
+ engram.files.identity = await readFile(identityPath, "utf-8");
44
+ engram.meta.updatedAt = new Date().toISOString();
45
+ await this.repository.save(engram);
46
+ result.refreshed.push(id);
47
+ }
48
+ return result;
49
+ }
50
+ async listTemplateIds() {
51
+ if (!existsSync(this.templatesDir)) {
52
+ return [];
53
+ }
54
+ const entries = await readdir(this.templatesDir, { withFileTypes: true });
55
+ return entries
56
+ .filter((entry) => entry.isDirectory())
57
+ .map((entry) => entry.name)
58
+ .sort();
59
+ }
60
+ }
@@ -1,3 +1,6 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { existsSync } from "node:fs";
1
4
  import { LocalEngramRepository } from "../../../adapters/local/index.js";
2
5
  import { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, Sync, SyncOpenclawDirNotFoundError, } from "../../../core/usecases/index.js";
3
6
  import { resolveEngramsPath, resolveClawPath } from "../../../shared/config.js";
@@ -11,9 +14,9 @@ export function registerClawCommand(program) {
11
14
  .command("inject")
12
15
  .description("Push an Engram into a Claw workspace")
13
16
  .requiredOption("-e, --engram <id>", "Engram ID to inject")
14
- .option("--to <agent>", "Inject into a different agent name")
15
17
  .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
16
18
  .option("--merge-identity", "Merge IDENTITY.md into SOUL.md (for non-OpenClaw Claw frameworks)")
19
+ .option("-y, --yes", "Skip persona overwrite confirmation")
17
20
  .option("--no-sync", "Skip automatic memory sync after inject")
18
21
  .option("-p, --path <dir>", "Override engrams directory path")
19
22
  .action(async (opts) => {
@@ -22,19 +25,34 @@ export function registerClawCommand(program) {
22
25
  const repo = new LocalEngramRepository(engramsPath);
23
26
  const inject = new Inject(repo);
24
27
  try {
25
- const result = await inject.execute(opts.engram, {
26
- to: opts.to,
28
+ const diff = await inject.inspectPersona(opts.engram, {
27
29
  openclawDir: clawDir,
28
30
  mergeIdentity: opts.mergeIdentity,
29
31
  });
30
- console.log(`Injected "${result.engramName}" into ${result.targetPath}`);
31
- console.log(` Files written: ${result.filesWritten.join(", ")}`);
32
+ const alreadyInjected = diff.soul === "same" &&
33
+ (diff.identity === "same" || diff.identity === "skipped");
34
+ if (diff.overwriteRequired &&
35
+ !opts.yes &&
36
+ !(await confirmOverwrite(`SOUL.md and/or IDENTITY.md already exist and differ in ${diff.targetPath}. Overwrite with local Relic version? [y/N] `))) {
37
+ console.error("Error: Persona files already exist and differ. Re-run with --yes to overwrite from local Relic Engram.");
38
+ process.exit(1);
39
+ }
40
+ if (alreadyInjected) {
41
+ console.log(` Already injected (${diff.targetPath})`);
42
+ }
43
+ else {
44
+ const result = await inject.execute(opts.engram, {
45
+ openclawDir: clawDir,
46
+ mergeIdentity: opts.mergeIdentity,
47
+ });
48
+ console.log(`Injected "${result.engramName}" into ${result.targetPath}`);
49
+ console.log(` Files written: ${result.filesWritten.join(", ")}`);
50
+ }
32
51
  if (!opts.sync)
33
52
  return;
34
53
  // Auto-sync memory after inject
35
54
  const sync = new Sync(repo, engramsPath);
36
- const agentName = opts.to ?? opts.engram;
37
- const workspacePath = resolveWorkspacePath(agentName, clawDir);
55
+ const workspacePath = resolveWorkspacePath(opts.engram, clawDir);
38
56
  const syncResult = await sync.syncPair({
39
57
  engramId: opts.engram,
40
58
  workspacePath,
@@ -73,21 +91,82 @@ export function registerClawCommand(program) {
73
91
  .option("-a, --agent <name>", "Agent name to extract from (default: main)")
74
92
  .option("--name <name>", "Engram display name (defaults to agent name)")
75
93
  .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
94
+ .option("-f, --force", "Allow overwriting local persona files from the Claw workspace")
95
+ .option("-y, --yes", "Skip persona overwrite confirmation")
96
+ .option("--no-sync", "Skip automatic memory sync after extract")
76
97
  .option("-p, --path <dir>", "Override engrams directory path")
77
98
  .action(async (opts) => {
78
99
  const engramsPath = await resolveEngramsPath(opts.path);
79
100
  const clawDir = await resolveClawPath(opts.dir);
80
101
  const repo = new LocalEngramRepository(engramsPath);
81
102
  const extract = new Extract(repo);
103
+ const sync = new Sync(repo, engramsPath);
82
104
  try {
83
105
  const agentName = opts.agent ?? "main";
84
- const result = await extract.execute(agentName, {
106
+ const diff = await extract.inspectPersona(agentName, {
85
107
  name: opts.name,
86
108
  openclawDir: clawDir,
87
109
  });
88
- console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
89
- console.log(` Files read: ${result.filesRead.join(", ")}`);
90
- console.log(` Saved as Engram: ${result.engramId}`);
110
+ if (diff.existing && !opts.force) {
111
+ throw new AlreadyExtractedError(agentName);
112
+ }
113
+ const alreadyExtracted = diff.existing &&
114
+ diff.name === "same" &&
115
+ diff.soul === "same" &&
116
+ diff.identity === "same";
117
+ if (diff.existing &&
118
+ diff.overwriteRequired &&
119
+ !opts.yes &&
120
+ !(await confirmOverwrite(`SOUL.md and/or IDENTITY.md already exist in local Engram "${agentName}" and differ from ${diff.sourcePath}. Overwrite with the Claw version? [y/N] `))) {
121
+ console.error("Error: Persona files already exist and differ. Re-run with --yes to overwrite from the Claw workspace.");
122
+ process.exit(1);
123
+ }
124
+ if (alreadyExtracted) {
125
+ console.log(opts.force
126
+ ? ` Already extracted and updated (${agentName})`
127
+ : ` Already extracted (${agentName})`);
128
+ }
129
+ else {
130
+ const result = await extract.execute(agentName, {
131
+ name: opts.name,
132
+ openclawDir: clawDir,
133
+ force: opts.force,
134
+ });
135
+ console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
136
+ if (diff.existing) {
137
+ console.log(` Files overwritten: SOUL.md, IDENTITY.md`);
138
+ if (diff.name === "different") {
139
+ console.log(` Metadata updated: engram.json (name)`);
140
+ }
141
+ console.log(` Saved as Engram: ${result.engramId}`);
142
+ }
143
+ else {
144
+ console.log(` Files extracted: ${result.filesRead.join(", ")}`);
145
+ console.log(` Saved as Engram: ${result.engramId}`);
146
+ }
147
+ }
148
+ if (!opts.sync)
149
+ return;
150
+ const syncResult = await sync.syncPair({
151
+ engramId: agentName,
152
+ workspacePath: diff.sourcePath,
153
+ });
154
+ const details = [];
155
+ if (syncResult.memoryFilesMerged > 0) {
156
+ details.push(`${syncResult.memoryFilesMerged} memory file(s)`);
157
+ }
158
+ if (syncResult.memoryIndexMerged) {
159
+ details.push("MEMORY.md");
160
+ }
161
+ if (syncResult.userMerged) {
162
+ details.push("USER.md");
163
+ }
164
+ if (details.length > 0) {
165
+ console.log(` Synced: ${details.join(", ")}`);
166
+ }
167
+ else {
168
+ console.log(` Already in sync`);
169
+ }
91
170
  }
92
171
  catch (err) {
93
172
  if (err instanceof WorkspaceNotFoundError ||
@@ -103,6 +182,7 @@ export function registerClawCommand(program) {
103
182
  claw
104
183
  .command("sync")
105
184
  .description("Bidirectional memory sync between Engrams and Claw workspaces")
185
+ .option("-t, --target <id>", "Sync one target only by shared Engram/agent name")
106
186
  .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
107
187
  .option("-p, --path <dir>", "Override engrams directory path")
108
188
  .action(async (opts) => {
@@ -111,6 +191,44 @@ export function registerClawCommand(program) {
111
191
  const repo = new LocalEngramRepository(engramsPath);
112
192
  const sync = new Sync(repo, engramsPath);
113
193
  try {
194
+ if (opts.target) {
195
+ const targetId = opts.target.trim();
196
+ if (!targetId) {
197
+ console.error(`Error: Invalid sync target "${opts.target}".`);
198
+ process.exit(1);
199
+ }
200
+ const engram = await repo.get(targetId);
201
+ if (!engram) {
202
+ console.error(`Error: Engram "${targetId}" not found.`);
203
+ process.exit(1);
204
+ }
205
+ const workspacePath = resolveWorkspacePath(targetId, clawDir);
206
+ if (!existsSync(workspacePath)) {
207
+ console.error(`Error: Claw agent "${targetId}" workspace not found.`);
208
+ process.exit(1);
209
+ }
210
+ const result = await sync.syncPair({
211
+ engramId: targetId,
212
+ workspacePath,
213
+ });
214
+ const details = [];
215
+ if (result.memoryFilesMerged > 0) {
216
+ details.push(`${result.memoryFilesMerged} memory file(s)`);
217
+ }
218
+ if (result.memoryIndexMerged) {
219
+ details.push("MEMORY.md");
220
+ }
221
+ if (result.userMerged) {
222
+ details.push("USER.md");
223
+ }
224
+ if (details.length > 0) {
225
+ console.log(` ${targetId}: merged ${details.join(", ")}`);
226
+ }
227
+ else {
228
+ console.log(` Already in sync (${targetId})`);
229
+ }
230
+ return;
231
+ }
114
232
  const result = await sync.execute(clawDir);
115
233
  if (result.synced.length === 0 && result.skipped.length === 0) {
116
234
  console.log("No Claw workspaces found.");
@@ -131,7 +249,7 @@ export function registerClawCommand(program) {
131
249
  console.log(` ${s.engramId}: merged ${details.join(", ")}`);
132
250
  }
133
251
  else {
134
- console.log(` ${s.engramId}: already in sync`);
252
+ console.log(` Already in sync (${s.engramId})`);
135
253
  }
136
254
  }
137
255
  if (result.skipped.length > 0) {
@@ -147,3 +265,16 @@ export function registerClawCommand(program) {
147
265
  }
148
266
  });
149
267
  }
268
+ async function confirmOverwrite(message) {
269
+ if (!input.isTTY || !output.isTTY) {
270
+ return false;
271
+ }
272
+ const rl = createInterface({ input, output });
273
+ try {
274
+ const answer = await rl.question(message);
275
+ return /^(y|yes)$/i.test(answer.trim());
276
+ }
277
+ finally {
278
+ rl.close();
279
+ }
280
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerMigrateCommand(program: Command): void;