@innominatum/agentforge-cli 1.1.9 → 1.1.23

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/src/index.ts CHANGED
@@ -434,66 +434,121 @@ async function deployContextFiles(slug: string, config: any, resolvedId?: string
434
434
  }
435
435
 
436
436
  const files = await fs.readdir(agentPath);
437
-
438
- // Identificar ficheiros de contexto e de memória
439
- const memoryFileNames = ['MEMORY.md', 'memory.md'];
440
- const memoryDirName = 'memory';
441
-
442
- const contextFiles = files.filter(f =>
443
- (f.endsWith('.md') || f.endsWith('.txt') || f.endsWith('.py')) &&
444
- f !== 'README.md' &&
445
- f !== 'agent.json' &&
446
- !memoryFileNames.includes(f)
447
- );
448
-
449
- const hasMemoryDir = await fs.pathExists(path.join(agentPath, memoryDirName));
450
- const memoryFilesFound = files.filter(f => memoryFileNames.includes(f));
451
- const hasMemory = hasMemoryDir || memoryFilesFound.length > 0;
437
+ const itemsToSync = files.filter(f => f !== 'agent.json' && f !== 'README.md');
452
438
 
453
- if (contextFiles.length === 0 && !hasMemory) {
439
+ if (itemsToSync.length === 0) {
454
440
  console.log(`Nenhum ficheiro de contexto ou memória encontrado para "${slug}".`);
455
441
  return;
456
442
  }
457
443
 
458
444
  const tempExportDir = path.join(basePath, `temp_export_${slug}`);
459
- const tempContextDir = path.join(tempExportDir, "context_files");
460
- const tempMemoryDir = path.join(tempExportDir, "memory");
461
445
  const tarPath = path.join(basePath, `temp_export_${slug}.tar.gz`);
462
446
 
447
+ // Guardar lista de ficheiros locais para pruning
448
+ const localFilePaths: string[] = [];
449
+ async function collectFilesRecursive(dir: string, baseDir: string): Promise<string[]> {
450
+ const results: string[] = [];
451
+ if (!(await fs.pathExists(dir))) return results;
452
+ const entries = await fs.readdir(dir, { withFileTypes: true });
453
+ for (const entry of entries) {
454
+ const fullPath = path.join(dir, entry.name);
455
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
456
+ if (entry.isDirectory()) {
457
+ const subResults = await collectFilesRecursive(fullPath, baseDir);
458
+ results.push(...subResults);
459
+ } else {
460
+ results.push(relativePath);
461
+ }
462
+ }
463
+ return results;
464
+ }
465
+
463
466
  try {
464
- const sections: string[] = [];
467
+ const sections = new Set<string>();
465
468
 
466
- if (contextFiles.length > 0) {
467
- sections.push("context_files");
468
- await fs.ensureDir(tempContextDir);
469
- for (const file of contextFiles) {
470
- await fs.copy(path.join(agentPath, file), path.join(tempContextDir, file));
469
+ // Processar ficheiros/pastas da raiz do agente
470
+ for (const item of itemsToSync) {
471
+ const itemPath = path.join(agentPath, item);
472
+ const isDir = (await fs.stat(itemPath)).isDirectory();
473
+
474
+ const section = "context_files";
475
+ sections.add(section);
476
+
477
+ const targetDir = path.join(tempExportDir, section);
478
+ await fs.ensureDir(targetDir);
479
+
480
+ if (isDir) {
481
+ // Obter todos os ficheiros da pasta (ex: memory, _system)
482
+ // e achatá-os (flatten) com o nome da pasta como prefixo (ex: memory_arquivo.md)
483
+ // O GoClaw espera que os ficheiros de contexto não tenham pastas, mas sim prefixos achatados!
484
+ const subFiles = await fs.readdir(itemPath);
485
+ for (const sub of subFiles) {
486
+ const subPath = path.join(itemPath, sub);
487
+ const isSubDir = (await fs.stat(subPath)).isDirectory();
488
+ if (!isSubDir) {
489
+ const flatName = `${item}_${sub}`;
490
+ await fs.copy(subPath, path.join(targetDir, flatName));
491
+ }
492
+ }
493
+ } else {
494
+ await fs.copy(itemPath, path.join(targetDir, item));
471
495
  }
472
496
  }
473
497
 
474
- if (hasMemory) {
475
- sections.push("memory");
476
- await fs.ensureDir(tempMemoryDir);
477
- // Copiar ficheiros de memória explícitos para a pasta memory no arquivo
478
- for (const file of memoryFilesFound) {
479
- await fs.copy(path.join(agentPath, file), path.join(tempMemoryDir, file));
498
+ // Coletar ficheiros para pruning
499
+ const sectionDir = path.join(tempExportDir, "context_files");
500
+ const sectionEntries = await collectFilesRecursive(sectionDir, sectionDir);
501
+ for (const entry of sectionEntries) {
502
+ // O GoClaw retorna os caminhos das memórias com barras (memory/arquivo.md)
503
+ // Nós achatamos para o upload (memory_arquivo.md). Para o pruning não apagar ficheiros
504
+ // válidos acidentalmente, temos que re-mapear o nome achatado para a versão com barra.
505
+ let finalEntry = entry;
506
+ if (entry.startsWith("memory_")) {
507
+ finalEntry = entry.replace("memory_", "memory/");
508
+ } else if (entry.startsWith("_system_")) {
509
+ finalEntry = entry.replace("_system_", "_system/");
480
510
  }
481
- // Copiar conteúdo da pasta memory se existir
482
- if (hasMemoryDir) {
483
- await fs.copy(path.join(agentPath, memoryDirName), tempMemoryDir);
511
+ localFilePaths.push(finalEntry);
512
+ }
513
+
514
+ const sectionsArray = Array.from(sections);
515
+
516
+ // --- HACK PARA APAGAR FICHEIROS FÍSICOS DO GOCLAW ---
517
+ // O GoClaw não apaga ficheiros do disco (context_files) quando os apagamos da DB (memory_documents).
518
+ // Isto faz com que os ficheiros voltem como "fantasmas" durante o pull.
519
+ // Solução: Injetar ficheiros vazios no tarball para os órfãos, forçando o /import a esmagá-los com 0 bytes!
520
+ try {
521
+ const docsUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents`;
522
+ const preDocsRes = await axios.get(docsUrl, {
523
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
524
+ });
525
+ const preDocs = preDocsRes.data || [];
526
+ for (const pDoc of preDocs) {
527
+ if (pDoc.path && !localFilePaths.includes(pDoc.path)) {
528
+ const flatGhost = pDoc.path.replace(/[\/\\]/g, '_');
529
+ const ghostPath = path.join(tempExportDir, "context_files", flatGhost);
530
+ await fs.ensureDir(path.dirname(ghostPath));
531
+ await fs.writeFile(ghostPath, " "); // 1 byte soft-delete payload
532
+ if (!sectionsArray.includes("context_files")) {
533
+ sectionsArray.push("context_files");
534
+ }
535
+ }
484
536
  }
537
+ } catch (e: any) {
538
+ console.warn("Aviso: Falha ao procurar fantasmas para o tarball.", e.message);
485
539
  }
486
540
 
487
541
  await tar.c({
488
542
  gzip: true,
489
543
  file: tarPath,
490
544
  cwd: tempExportDir
491
- }, sections);
545
+ }, sectionsArray);
492
546
 
493
547
  const form = new FormData();
494
548
  form.append("file", fs.createReadStream(tarPath));
495
549
 
496
- const url = `${config.goclaw.api_url}/v1/agents/${agentId}/import?include=${sections.join(",")}`;
550
+ // Upload dos ficheiros (aditivo)
551
+ const url = `${config.goclaw.api_url}/v1/agents/${agentId}/import?include=${sectionsArray.join(",")}`;
497
552
  await axios.post(url, form, {
498
553
  headers: {
499
554
  ...form.getHeaders(),
@@ -502,7 +557,76 @@ async function deployContextFiles(slug: string, config: any, resolvedId?: string
502
557
  }
503
558
  });
504
559
 
505
- console.log(`✅ Upload cirúrgico de ${contextFiles.length} ficheiros de contexto e ${hasMemory ? 'dados de memória' : 'sem memória'} concluído com sucesso!`);
560
+ console.log(`✅ Upload de ficheiros e subpastas de contexto concluído com sucesso!`);
561
+
562
+ // Atualização forçada de memórias editadas (bypassa a proteção de overwrite do /import)
563
+ for (const localPath of localFilePaths) {
564
+ if (localPath.startsWith('memory/') && localPath.endsWith('.md')) {
565
+ try {
566
+ // O ficheiro físico no tempExportDir está achatado (memory_arquivo.md)
567
+ const flatFileName = localPath.replace("memory/", "memory_");
568
+ const content = await fs.readFile(path.join(sectionDir, flatFileName), 'utf8');
569
+
570
+ // O endpoint usa {path...} portanto não podemos fazer URL encode das barras
571
+ const putUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents/${localPath}`;
572
+ await axios.put(putUrl, { content }, {
573
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
574
+ });
575
+ console.log(`✅ Edição de memória forçada com sucesso: ${localPath}`);
576
+ } catch (putErr: any) {
577
+ console.warn(`⚠️ Aviso na edição de ${localPath}: O conteúdo pode não ter sido alterado. (${putErr.message})`);
578
+ }
579
+ }
580
+ }
581
+
582
+ // --- Início do Pruning (Remover ficheiros órfãos do servidor) ---
583
+ try {
584
+ const documentsUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents`;
585
+ const docsResponse = await axios.get(documentsUrl, {
586
+ headers: {
587
+ Authorization: `Bearer ${config.goclaw.token}`,
588
+ "X-GoClaw-User-Id": config.goclaw.username || "system"
589
+ }
590
+ });
591
+
592
+ const remoteDocs = docsResponse.data || [];
593
+ let deletedCount = 0;
594
+
595
+ for (const doc of remoteDocs) {
596
+ if (!doc.path) continue;
597
+
598
+ // Verifica se o ficheiro remoto existe na nossa lista de ficheiros locais
599
+ if (!localFilePaths.includes(doc.path)) {
600
+ console.log(`🧹 Removendo memória órfã no servidor: ${doc.path}`);
601
+
602
+ try {
603
+ // O endpoint do GoClaw espera o caminho exato sem URL encode (route genérica {path...})
604
+ // E é mandatório passar o user_id dono do documento, senão dá erro 500.
605
+ const deleteUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents/${doc.path}`;
606
+ await axios.delete(deleteUrl, {
607
+ headers: {
608
+ Authorization: `Bearer ${config.goclaw.token}`,
609
+ "X-GoClaw-User-Id": doc.user_id || config.goclaw.username || "system"
610
+ }
611
+ });
612
+ deletedCount++;
613
+ } catch (delErr: any) {
614
+ const errorData = delErr.response?.data?.error || "";
615
+ if (delErr.response?.status === 500 && errorData.includes("not found")) {
616
+ console.log(`✅ ${doc.path} já estava removido da base de dados.`);
617
+ } else {
618
+ console.warn(`⚠️ Não foi possível apagar ${doc.path}: ${delErr.message}`);
619
+ }
620
+ }
621
+ }
622
+ }
623
+ if (deletedCount > 0) {
624
+ console.log(`✅ Pruning concluído: ${deletedCount} ficheiro(s) apagado(s) do GoClaw.`);
625
+ }
626
+ } catch (pruneErr: any) {
627
+ console.warn(`⚠️ Aviso: Falha ao fazer pruning das memórias: ${pruneErr.message}`);
628
+ }
629
+
506
630
  } finally {
507
631
  if (await fs.pathExists(tempExportDir)) await fs.remove(tempExportDir);
508
632
  if (await fs.pathExists(tarPath)) await fs.remove(tarPath);
@@ -663,6 +787,97 @@ const pullCmd = program
663
787
  .command("pull")
664
788
  .description("Sincroniza entidades do GoClaw para o workspace local");
665
789
 
790
+ async function pullAllSkills(config: any) {
791
+ console.log("🧹 Limpando a pasta local de skills...");
792
+ await fs.emptyDir(path.join(getWorkspaceRoot(), "skills"));
793
+
794
+ console.log("📥 Baixando skills do GoClaw...");
795
+ const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
796
+ const response = await axios.get(url, {
797
+ headers: {
798
+ Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
799
+ },
800
+ responseType: "stream"
801
+ });
802
+
803
+ const tempTarPath = path.join(getWorkspaceRoot(), "temp_skills.tar.gz");
804
+ const writer = fs.createWriteStream(tempTarPath);
805
+ response.data.pipe(writer);
806
+
807
+ await new Promise((resolve, reject) => {
808
+ writer.on("finish", resolve);
809
+ writer.on("error", reject);
810
+ });
811
+
812
+ console.log("📦 Extraindo skills para a pasta local...");
813
+ await tar.x({
814
+ file: tempTarPath,
815
+ cwd: getWorkspaceRoot()
816
+ });
817
+ await fs.remove(tempTarPath);
818
+
819
+ console.log("📥 Baixando ficheiros de código das skills...");
820
+ const skillsListRes = await axios.get(`${config.goclaw.api_url}/v1/skills`, {
821
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
822
+ });
823
+
824
+ const skills = skillsListRes.data.skills || [];
825
+ for (const skill of skills) {
826
+ try {
827
+ const isSystem = skill.is_system === true;
828
+ const targetFolder = isSystem ? path.join("system", skill.slug) : skill.slug;
829
+
830
+ if (isSystem) {
831
+ const originalPath = path.join(getWorkspaceRoot(), "skills", skill.slug);
832
+ const newPath = path.join(getWorkspaceRoot(), "skills", targetFolder);
833
+ if (await fs.pathExists(originalPath)) {
834
+ await fs.ensureDir(path.dirname(newPath));
835
+ await fs.move(originalPath, newPath, { overwrite: true });
836
+ }
837
+ }
838
+
839
+ const filesRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
840
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
841
+ });
842
+
843
+ const files = filesRes.data.files || [];
844
+ for (const file of files) {
845
+ if (file.isDir) continue;
846
+ const fileContentRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
847
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
848
+ });
849
+ const filePath = path.join(getWorkspaceRoot(), "skills", targetFolder, file.path);
850
+ await fs.ensureDir(path.dirname(filePath));
851
+ await fs.writeFile(filePath, fileContentRes.data.content || "");
852
+ }
853
+ } catch (fileErr: any) {
854
+ console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
855
+ }
856
+ }
857
+
858
+ // Remover quaisquer skills fantasmas que o tarball tenha extraído
859
+ const validSlugs = new Set(skills.map((s: any) => s.is_system === true ? path.join("system", s.slug) : s.slug));
860
+ const skillsDir = path.join(getWorkspaceRoot(), "skills");
861
+ if (await fs.pathExists(skillsDir)) {
862
+ const localItems = await fs.readdir(skillsDir);
863
+ for (const item of localItems) {
864
+ if (item === "system") {
865
+ const systemDir = path.join(skillsDir, "system");
866
+ if (await fs.pathExists(systemDir)) {
867
+ const systemItems = await fs.readdir(systemDir);
868
+ for (const sysItem of systemItems) {
869
+ if (!validSlugs.has(path.join("system", sysItem))) {
870
+ await fs.remove(path.join(systemDir, sysItem));
871
+ }
872
+ }
873
+ }
874
+ } else if (!validSlugs.has(item)) {
875
+ await fs.remove(path.join(skillsDir, item));
876
+ }
877
+ }
878
+ }
879
+ }
880
+
666
881
  pullCmd
667
882
  .command("skills")
668
883
  .description("Faz download do arquivo tar.gz de skills do GoClaw e extrai localmente")
@@ -678,106 +893,101 @@ pullCmd
678
893
  return;
679
894
  }
680
895
 
681
- console.log("🧹 Limpando a pasta local de skills...");
682
- await fs.emptyDir(path.join(getWorkspaceRoot(), "skills"));
683
-
684
- console.log("📥 Baixando skills do GoClaw...");
685
896
  try {
686
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
687
- const response = await axios.get(url, {
688
- headers: {
689
- Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
690
- },
691
- responseType: "stream"
692
- });
897
+ await pullAllSkills(config);
898
+ console.log("✅ Pull de skills concluído com sucesso! As skills foram atualizadas localmente.");
899
+ } catch (error: any) {
900
+ console.error("❌ Erro durante o pull das skills:");
901
+ if (error.response) {
902
+ console.error(`Status HTTP ${error.response.status}`);
903
+ } else {
904
+ console.error(error.message);
905
+ }
906
+ }
907
+ });
693
908
 
694
- const tempTarPath = path.join(getWorkspaceRoot(), "temp_skills.tar.gz");
695
- const writer = fs.createWriteStream(tempTarPath);
696
- response.data.pipe(writer);
909
+ async function pullAgent(slug: string, agentId: string, config: any) {
910
+ console.log(`📦 Baixando agente: ${slug}...`);
911
+
912
+ const url = `${config.goclaw.api_url}/v1/agents/${agentId}/export`;
913
+ const response = await axios.get(url, {
914
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" },
915
+ responseType: "stream"
916
+ });
697
917
 
698
- await new Promise((resolve, reject) => {
699
- writer.on("finish", resolve);
700
- writer.on("error", reject);
701
- });
918
+ const tempTarPath = path.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
919
+
920
+ try {
921
+ const writer = fs.createWriteStream(tempTarPath);
922
+ response.data.pipe(writer);
702
923
 
703
- console.log("📦 Extraindo skills para a pasta local...");
704
- await tar.x({
705
- file: tempTarPath,
706
- cwd: getWorkspaceRoot()
707
- });
708
- await fs.remove(tempTarPath);
924
+ await new Promise((resolve, reject) => {
925
+ writer.on("finish", resolve);
926
+ writer.on("error", reject);
927
+ });
709
928
 
710
- console.log("📥 Baixando ficheiros de código das skills...");
711
- const skillsListRes = await axios.get(`${config.goclaw.api_url}/v1/skills`, {
712
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
713
- });
714
-
715
- const skills = skillsListRes.data.skills || [];
716
- for (const skill of skills) {
717
- try {
718
- const isSystem = skill.is_system === true;
719
- const targetFolder = isSystem ? path.join("system", skill.slug) : skill.slug;
720
-
721
- if (isSystem) {
722
- const originalPath = path.join(getWorkspaceRoot(), "skills", skill.slug);
723
- const newPath = path.join(getWorkspaceRoot(), "skills", targetFolder);
724
- if (await fs.pathExists(originalPath)) {
725
- await fs.ensureDir(path.dirname(newPath));
726
- await fs.move(originalPath, newPath, { overwrite: true });
727
- }
728
- }
929
+ const agentPath = path.join(getWorkspaceRoot(), "agents", slug);
930
+ if (await fs.pathExists(agentPath)) {
931
+ await fs.emptyDir(agentPath);
932
+ } else {
933
+ await fs.ensureDir(agentPath);
934
+ }
729
935
 
730
- const filesRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
731
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
732
- });
733
-
734
- const files = filesRes.data.files || [];
735
- for (const file of files) {
736
- if (file.isDir) continue;
737
- const fileContentRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
738
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
739
- });
740
- const filePath = path.join(getWorkspaceRoot(), "skills", targetFolder, file.path);
741
- await fs.ensureDir(path.dirname(filePath));
742
- await fs.writeFile(filePath, fileContentRes.data.content || "");
743
- }
744
- } catch (fileErr: any) {
745
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
746
- }
936
+ // Obter os caminhos reais (com barras) da API para reverter o flattening do export
937
+ const docsRes = await axios.get(`${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents`, {
938
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
939
+ });
940
+ const pathMap: Record<string, string> = {};
941
+ (docsRes.data || []).forEach((d: any) => {
942
+ if (d.path) {
943
+ const flat = d.path.replace(/[\/\\]/g, '_');
944
+ pathMap[flat] = d.path;
747
945
  }
946
+ });
748
947
 
749
- // Remover quaisquer skills fantasmas que o tarball tenha extraído (skills apagadas mas ainda no export)
750
- const validSlugs = new Set(skills.map((s: any) => s.is_system === true ? path.join("system", s.slug) : s.slug));
751
- const skillsDir = path.join(getWorkspaceRoot(), "skills");
752
- if (await fs.pathExists(skillsDir)) {
753
- const localItems = await fs.readdir(skillsDir);
754
- for (const item of localItems) {
755
- if (item === "system") {
756
- const systemDir = path.join(skillsDir, "system");
757
- if (await fs.pathExists(systemDir)) {
758
- const systemItems = await fs.readdir(systemDir);
759
- for (const sysItem of systemItems) {
760
- if (!validSlugs.has(path.join("system", sysItem))) {
761
- await fs.remove(path.join(systemDir, sysItem));
762
- }
763
- }
764
- }
765
- } else if (!validSlugs.has(item)) {
766
- await fs.remove(path.join(skillsDir, item));
948
+ await tar.x({
949
+ file: tempTarPath,
950
+ cwd: agentPath,
951
+ strip: 0,
952
+ filter: (path) => {
953
+ return path === 'agent.json' || path.startsWith('context_files/');
954
+ }
955
+ });
956
+
957
+ const contextDir = path.join(agentPath, "context_files");
958
+ if (await fs.pathExists(contextDir)) {
959
+ const contextFiles = await fs.readdir(contextDir);
960
+ for (const f of contextFiles) {
961
+ const filePath = path.join(contextDir, f);
962
+ const stat = await fs.stat(filePath);
963
+ if (stat.isFile()) {
964
+ const content = await fs.readFile(filePath, 'utf8');
965
+ if (content === " " || content === "") {
966
+ // Fantasma neutralizado pelo nosso script de deploy! Deita fora!
967
+ await fs.remove(filePath);
968
+ continue;
767
969
  }
768
970
  }
769
- }
770
971
 
771
- console.log("✅ Pull concluído com sucesso! As skills foram atualizadas localmente.");
772
- } catch (error: any) {
773
- console.error("❌ Erro durante o pull das skills:");
774
- if (error.response) {
775
- console.error(`Status HTTP ${error.response.status}`);
776
- } else {
777
- console.error(error.message);
972
+ if (pathMap[f]) {
973
+ const targetPath = path.join(agentPath, pathMap[f]);
974
+ await fs.ensureDir(path.dirname(targetPath));
975
+ await fs.move(filePath, targetPath, { overwrite: true });
976
+ } else if (f.startsWith('_system_') || f.startsWith('memory_')) {
977
+ // É um stub de diretório do export do GoClaw (ex: _system_dreaming_) ou um órfão esmagado. Ignoramos.
978
+ await fs.remove(filePath);
979
+ } else {
980
+ await fs.move(filePath, path.join(agentPath, f), { overwrite: true });
981
+ }
778
982
  }
983
+ await fs.remove(contextDir);
779
984
  }
780
- });
985
+ } finally {
986
+ if (await fs.pathExists(tempTarPath)) {
987
+ await fs.remove(tempTarPath);
988
+ }
989
+ }
990
+ }
781
991
 
782
992
  pullCmd
783
993
  .command("agents")
@@ -808,84 +1018,9 @@ pullCmd
808
1018
 
809
1019
  for (const agent of agents) {
810
1020
  const slug = agent.agent_key;
811
- console.log(`📦 Baixando agente: ${slug}...`);
812
-
813
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export?sections=config,context_files,memory`;
814
- const response = await axios.get(url, {
815
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" },
816
- responseType: "stream"
817
- });
818
-
819
- const tempTarPath = path.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
820
-
821
- try {
822
- const writer = fs.createWriteStream(tempTarPath);
823
- response.data.pipe(writer);
824
-
825
- await new Promise((resolve, reject) => {
826
- writer.on("finish", resolve);
827
- writer.on("error", reject);
828
- });
829
-
830
- const agentPath = path.join(getWorkspaceRoot(), "agents", slug);
831
- await fs.ensureDir(agentPath);
832
-
833
- await tar.x({
834
- file: tempTarPath,
835
- cwd: agentPath,
836
- strip: 0,
837
- filter: (path) => {
838
- return path === 'agent.json' || path.startsWith('context_files/') || path.startsWith('memory/') || path === 'MEMORY.md' || path === 'memory.md';
839
- }
840
- });
841
-
842
- const contextDir = path.join(agentPath, "context_files");
843
- if (await fs.pathExists(contextDir)) {
844
- const contextFiles = await fs.readdir(contextDir);
845
- for (const f of contextFiles) {
846
- await fs.move(path.join(contextDir, f), path.join(agentPath, f), { overwrite: true });
847
- }
848
- await fs.remove(contextDir);
849
- }
850
-
851
- // Reconstruir ficheiros de memória a partir de JSONL
852
- const memoryDir = path.join(agentPath, "memory");
853
- if (await fs.pathExists(memoryDir)) {
854
- const processJsonl = async (filePath: string) => {
855
- if (!(await fs.pathExists(filePath))) return;
856
- const content = await fs.readFile(filePath, 'utf8');
857
- const lines = content.split('\n').filter(l => l.trim());
858
- for (const line of lines) {
859
- try {
860
- const entry = JSON.parse(line);
861
- if (entry.path && entry.content) {
862
- const targetPath = path.join(agentPath, entry.path);
863
- await fs.ensureDir(path.dirname(targetPath));
864
- await fs.writeFile(targetPath, entry.content);
865
- }
866
- } catch (e) {}
867
- }
868
- await fs.remove(filePath);
869
- };
870
-
871
- await processJsonl(path.join(memoryDir, "global.jsonl"));
872
- const usersDir = path.join(memoryDir, "users");
873
- if (await fs.pathExists(usersDir)) {
874
- const userFiles = await fs.readdir(usersDir);
875
- for (const uf of userFiles) {
876
- if (uf.endsWith(".jsonl")) {
877
- await processJsonl(path.join(usersDir, uf));
878
- }
879
- }
880
- await fs.remove(usersDir);
881
- }
882
- }
883
- } finally {
884
- if (await fs.pathExists(tempTarPath)) {
885
- await fs.remove(tempTarPath);
886
- }
887
- }
1021
+ await pullAgent(slug, agent.id, config);
888
1022
  }
1023
+
889
1024
  console.log("✅ Pull de agentes concluído com sucesso!");
890
1025
  } catch (error: any) {
891
1026
  if (error.response && error.response.status) {
@@ -917,61 +1052,7 @@ pullCmd
917
1052
  // PULL SKILLS INLINE
918
1053
  console.log('\n--- [1/2] SKILLS ---');
919
1054
  try {
920
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
921
- const response = await axios.get(url, {
922
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
923
- responseType: 'stream'
924
- });
925
-
926
- const tempTarPath = path.join(getWorkspaceRoot(), 'temp_skills.tar.gz');
927
- const writer = fs.createWriteStream(tempTarPath);
928
- response.data.pipe(writer);
929
-
930
- await new Promise((resolve, reject) => {
931
- writer.on('finish', resolve);
932
- writer.on('error', reject);
933
- });
934
-
935
- await tar.x({ file: tempTarPath, cwd: getWorkspaceRoot() });
936
- await fs.remove(tempTarPath);
937
-
938
- const skillsListRes = await axios.get(`${config.goclaw.api_url}/v1/skills`, {
939
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
940
- });
941
-
942
- const skills = skillsListRes.data.skills || [];
943
- for (const skill of skills) {
944
- try {
945
- const isSystem = skill.is_system === true;
946
- const targetFolder = isSystem ? path.join('system', skill.slug) : skill.slug;
947
-
948
- if (isSystem) {
949
- const originalPath = path.join(getWorkspaceRoot(), 'skills', skill.slug);
950
- const newPath = path.join(getWorkspaceRoot(), 'skills', targetFolder);
951
- if (await fs.pathExists(originalPath)) {
952
- await fs.ensureDir(path.dirname(newPath));
953
- await fs.move(originalPath, newPath, { overwrite: true });
954
- }
955
- }
956
-
957
- const filesRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
958
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
959
- });
960
-
961
- const files = filesRes.data.files || [];
962
- for (const file of files) {
963
- if (file.isDir) continue;
964
- const fileContentRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
965
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
966
- });
967
- const filePath = path.join(getWorkspaceRoot(), 'skills', targetFolder, file.path);
968
- await fs.ensureDir(path.dirname(filePath));
969
- await fs.writeFile(filePath, fileContentRes.data.content || '');
970
- }
971
- } catch (fileErr: any) {
972
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
973
- }
974
- }
1055
+ await pullAllSkills(config);
975
1056
  console.log('✅ Pull de skills concluído!');
976
1057
  } catch (error: any) {
977
1058
  console.error('❌ Erro durante o pull das skills:', error.message);
@@ -989,50 +1070,7 @@ pullCmd
989
1070
 
990
1071
  for (const agent of agents) {
991
1072
  const slug = agent.agent_key;
992
- console.log(`📦 Baixando agente: ${slug}...`);
993
-
994
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export`;
995
- const response = await axios.get(url, {
996
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
997
- responseType: 'stream'
998
- });
999
-
1000
- const tempTarPath = path.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
1001
-
1002
- try {
1003
- const writer = fs.createWriteStream(tempTarPath);
1004
- response.data.pipe(writer);
1005
-
1006
- await new Promise((resolve, reject) => {
1007
- writer.on('finish', resolve);
1008
- writer.on('error', reject);
1009
- });
1010
-
1011
- const agentPath = path.join(getWorkspaceRoot(), 'agents', slug);
1012
- await fs.ensureDir(agentPath);
1013
-
1014
- await tar.x({
1015
- file: tempTarPath,
1016
- cwd: agentPath,
1017
- strip: 0,
1018
- filter: (path) => {
1019
- return path === 'agent.json' || path.startsWith('context_files/');
1020
- }
1021
- });
1022
-
1023
- const contextDir = path.join(agentPath, 'context_files');
1024
- if (await fs.pathExists(contextDir)) {
1025
- const contextFiles = await fs.readdir(contextDir);
1026
- for (const f of contextFiles) {
1027
- await fs.move(path.join(contextDir, f), path.join(agentPath, f), { overwrite: true });
1028
- }
1029
- await fs.remove(contextDir);
1030
- }
1031
- } finally {
1032
- if (await fs.pathExists(tempTarPath)) {
1033
- await fs.remove(tempTarPath);
1034
- }
1035
- }
1073
+ await pullAgent(slug, agent.id, config);
1036
1074
  }
1037
1075
  console.log('✅ Pull de agentes concluído!');
1038
1076
  } catch (error: any) {