@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/dist/index.js CHANGED
@@ -407,53 +407,112 @@ async function deployContextFiles(slug, config, resolvedId) {
407
407
  throw new Error(`Agente não encontrado em agents/${slug}`);
408
408
  }
409
409
  const files = await fs_extra_1.default.readdir(agentPath);
410
- // Identificar ficheiros de contexto e de memória
411
- const memoryFileNames = ['MEMORY.md', 'memory.md'];
412
- const memoryDirName = 'memory';
413
- const contextFiles = files.filter(f => (f.endsWith('.md') || f.endsWith('.txt') || f.endsWith('.py')) &&
414
- f !== 'README.md' &&
415
- f !== 'agent.json' &&
416
- !memoryFileNames.includes(f));
417
- const hasMemoryDir = await fs_extra_1.default.pathExists(path_1.default.join(agentPath, memoryDirName));
418
- const memoryFilesFound = files.filter(f => memoryFileNames.includes(f));
419
- const hasMemory = hasMemoryDir || memoryFilesFound.length > 0;
420
- if (contextFiles.length === 0 && !hasMemory) {
410
+ const itemsToSync = files.filter(f => f !== 'agent.json' && f !== 'README.md');
411
+ if (itemsToSync.length === 0) {
421
412
  console.log(`Nenhum ficheiro de contexto ou memória encontrado para "${slug}".`);
422
413
  return;
423
414
  }
424
415
  const tempExportDir = path_1.default.join(basePath, `temp_export_${slug}`);
425
- const tempContextDir = path_1.default.join(tempExportDir, "context_files");
426
- const tempMemoryDir = path_1.default.join(tempExportDir, "memory");
427
416
  const tarPath = path_1.default.join(basePath, `temp_export_${slug}.tar.gz`);
417
+ // Guardar lista de ficheiros locais para pruning
418
+ const localFilePaths = [];
419
+ async function collectFilesRecursive(dir, baseDir) {
420
+ const results = [];
421
+ if (!(await fs_extra_1.default.pathExists(dir)))
422
+ return results;
423
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
424
+ for (const entry of entries) {
425
+ const fullPath = path_1.default.join(dir, entry.name);
426
+ const relativePath = path_1.default.relative(baseDir, fullPath).replace(/\\/g, '/');
427
+ if (entry.isDirectory()) {
428
+ const subResults = await collectFilesRecursive(fullPath, baseDir);
429
+ results.push(...subResults);
430
+ }
431
+ else {
432
+ results.push(relativePath);
433
+ }
434
+ }
435
+ return results;
436
+ }
428
437
  try {
429
- const sections = [];
430
- if (contextFiles.length > 0) {
431
- sections.push("context_files");
432
- await fs_extra_1.default.ensureDir(tempContextDir);
433
- for (const file of contextFiles) {
434
- await fs_extra_1.default.copy(path_1.default.join(agentPath, file), path_1.default.join(tempContextDir, file));
438
+ const sections = new Set();
439
+ // Processar ficheiros/pastas da raiz do agente
440
+ for (const item of itemsToSync) {
441
+ const itemPath = path_1.default.join(agentPath, item);
442
+ const isDir = (await fs_extra_1.default.stat(itemPath)).isDirectory();
443
+ const section = "context_files";
444
+ sections.add(section);
445
+ const targetDir = path_1.default.join(tempExportDir, section);
446
+ await fs_extra_1.default.ensureDir(targetDir);
447
+ if (isDir) {
448
+ // Obter todos os ficheiros da pasta (ex: memory, _system)
449
+ // e achatá-os (flatten) com o nome da pasta como prefixo (ex: memory_arquivo.md)
450
+ // O GoClaw espera que os ficheiros de contexto não tenham pastas, mas sim prefixos achatados!
451
+ const subFiles = await fs_extra_1.default.readdir(itemPath);
452
+ for (const sub of subFiles) {
453
+ const subPath = path_1.default.join(itemPath, sub);
454
+ const isSubDir = (await fs_extra_1.default.stat(subPath)).isDirectory();
455
+ if (!isSubDir) {
456
+ const flatName = `${item}_${sub}`;
457
+ await fs_extra_1.default.copy(subPath, path_1.default.join(targetDir, flatName));
458
+ }
459
+ }
460
+ }
461
+ else {
462
+ await fs_extra_1.default.copy(itemPath, path_1.default.join(targetDir, item));
435
463
  }
436
464
  }
437
- if (hasMemory) {
438
- sections.push("memory");
439
- await fs_extra_1.default.ensureDir(tempMemoryDir);
440
- // Copiar ficheiros de memória explícitos para a pasta memory no arquivo
441
- for (const file of memoryFilesFound) {
442
- await fs_extra_1.default.copy(path_1.default.join(agentPath, file), path_1.default.join(tempMemoryDir, file));
465
+ // Coletar ficheiros para pruning
466
+ const sectionDir = path_1.default.join(tempExportDir, "context_files");
467
+ const sectionEntries = await collectFilesRecursive(sectionDir, sectionDir);
468
+ for (const entry of sectionEntries) {
469
+ // O GoClaw retorna os caminhos das memórias com barras (memory/arquivo.md)
470
+ // Nós achatamos para o upload (memory_arquivo.md). Para o pruning não apagar ficheiros
471
+ // válidos acidentalmente, temos que re-mapear o nome achatado para a versão com barra.
472
+ let finalEntry = entry;
473
+ if (entry.startsWith("memory_")) {
474
+ finalEntry = entry.replace("memory_", "memory/");
475
+ }
476
+ else if (entry.startsWith("_system_")) {
477
+ finalEntry = entry.replace("_system_", "_system/");
443
478
  }
444
- // Copiar conteúdo da pasta memory se existir
445
- if (hasMemoryDir) {
446
- await fs_extra_1.default.copy(path_1.default.join(agentPath, memoryDirName), tempMemoryDir);
479
+ localFilePaths.push(finalEntry);
480
+ }
481
+ const sectionsArray = Array.from(sections);
482
+ // --- HACK PARA APAGAR FICHEIROS FÍSICOS DO GOCLAW ---
483
+ // O GoClaw não apaga ficheiros do disco (context_files) quando os apagamos da DB (memory_documents).
484
+ // Isto faz com que os ficheiros voltem como "fantasmas" durante o pull.
485
+ // Solução: Injetar ficheiros vazios no tarball para os órfãos, forçando o /import a esmagá-los com 0 bytes!
486
+ try {
487
+ const docsUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents`;
488
+ const preDocsRes = await axios_1.default.get(docsUrl, {
489
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
490
+ });
491
+ const preDocs = preDocsRes.data || [];
492
+ for (const pDoc of preDocs) {
493
+ if (pDoc.path && !localFilePaths.includes(pDoc.path)) {
494
+ const flatGhost = pDoc.path.replace(/[\/\\]/g, '_');
495
+ const ghostPath = path_1.default.join(tempExportDir, "context_files", flatGhost);
496
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(ghostPath));
497
+ await fs_extra_1.default.writeFile(ghostPath, " "); // 1 byte soft-delete payload
498
+ if (!sectionsArray.includes("context_files")) {
499
+ sectionsArray.push("context_files");
500
+ }
501
+ }
447
502
  }
448
503
  }
504
+ catch (e) {
505
+ console.warn("Aviso: Falha ao procurar fantasmas para o tarball.", e.message);
506
+ }
449
507
  await tar.c({
450
508
  gzip: true,
451
509
  file: tarPath,
452
510
  cwd: tempExportDir
453
- }, sections);
511
+ }, sectionsArray);
454
512
  const form = new form_data_1.default();
455
513
  form.append("file", fs_extra_1.default.createReadStream(tarPath));
456
- const url = `${config.goclaw.api_url}/v1/agents/${agentId}/import?include=${sections.join(",")}`;
514
+ // Upload dos ficheiros (aditivo)
515
+ const url = `${config.goclaw.api_url}/v1/agents/${agentId}/import?include=${sectionsArray.join(",")}`;
457
516
  await axios_1.default.post(url, form, {
458
517
  headers: {
459
518
  ...form.getHeaders(),
@@ -461,7 +520,73 @@ async function deployContextFiles(slug, config, resolvedId) {
461
520
  "X-GoClaw-User-Id": config.goclaw.username || "system"
462
521
  }
463
522
  });
464
- console.log(`✅ Upload cirúrgico de ${contextFiles.length} ficheiros de contexto e ${hasMemory ? 'dados de memória' : 'sem memória'} concluído com sucesso!`);
523
+ console.log(`✅ Upload de ficheiros e subpastas de contexto concluído com sucesso!`);
524
+ // Atualização forçada de memórias editadas (bypassa a proteção de overwrite do /import)
525
+ for (const localPath of localFilePaths) {
526
+ if (localPath.startsWith('memory/') && localPath.endsWith('.md')) {
527
+ try {
528
+ // O ficheiro físico no tempExportDir está achatado (memory_arquivo.md)
529
+ const flatFileName = localPath.replace("memory/", "memory_");
530
+ const content = await fs_extra_1.default.readFile(path_1.default.join(sectionDir, flatFileName), 'utf8');
531
+ // O endpoint usa {path...} portanto não podemos fazer URL encode das barras
532
+ const putUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents/${localPath}`;
533
+ await axios_1.default.put(putUrl, { content }, {
534
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
535
+ });
536
+ console.log(`✅ Edição de memória forçada com sucesso: ${localPath}`);
537
+ }
538
+ catch (putErr) {
539
+ console.warn(`⚠️ Aviso na edição de ${localPath}: O conteúdo pode não ter sido alterado. (${putErr.message})`);
540
+ }
541
+ }
542
+ }
543
+ // --- Início do Pruning (Remover ficheiros órfãos do servidor) ---
544
+ try {
545
+ const documentsUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents`;
546
+ const docsResponse = await axios_1.default.get(documentsUrl, {
547
+ headers: {
548
+ Authorization: `Bearer ${config.goclaw.token}`,
549
+ "X-GoClaw-User-Id": config.goclaw.username || "system"
550
+ }
551
+ });
552
+ const remoteDocs = docsResponse.data || [];
553
+ let deletedCount = 0;
554
+ for (const doc of remoteDocs) {
555
+ if (!doc.path)
556
+ continue;
557
+ // Verifica se o ficheiro remoto existe na nossa lista de ficheiros locais
558
+ if (!localFilePaths.includes(doc.path)) {
559
+ console.log(`🧹 Removendo memória órfã no servidor: ${doc.path}`);
560
+ try {
561
+ // O endpoint do GoClaw espera o caminho exato sem URL encode (route genérica {path...})
562
+ // E é mandatório passar o user_id dono do documento, senão dá erro 500.
563
+ const deleteUrl = `${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents/${doc.path}`;
564
+ await axios_1.default.delete(deleteUrl, {
565
+ headers: {
566
+ Authorization: `Bearer ${config.goclaw.token}`,
567
+ "X-GoClaw-User-Id": doc.user_id || config.goclaw.username || "system"
568
+ }
569
+ });
570
+ deletedCount++;
571
+ }
572
+ catch (delErr) {
573
+ const errorData = delErr.response?.data?.error || "";
574
+ if (delErr.response?.status === 500 && errorData.includes("not found")) {
575
+ console.log(`✅ ${doc.path} já estava removido da base de dados.`);
576
+ }
577
+ else {
578
+ console.warn(`⚠️ Não foi possível apagar ${doc.path}: ${delErr.message}`);
579
+ }
580
+ }
581
+ }
582
+ }
583
+ if (deletedCount > 0) {
584
+ console.log(`✅ Pruning concluído: ${deletedCount} ficheiro(s) apagado(s) do GoClaw.`);
585
+ }
586
+ }
587
+ catch (pruneErr) {
588
+ console.warn(`⚠️ Aviso: Falha ao fazer pruning das memórias: ${pruneErr.message}`);
589
+ }
465
590
  }
466
591
  finally {
467
592
  if (await fs_extra_1.default.pathExists(tempExportDir))
@@ -610,6 +735,89 @@ deployCmd
610
735
  const pullCmd = program
611
736
  .command("pull")
612
737
  .description("Sincroniza entidades do GoClaw para o workspace local");
738
+ async function pullAllSkills(config) {
739
+ console.log("🧹 Limpando a pasta local de skills...");
740
+ await fs_extra_1.default.emptyDir(path_1.default.join(getWorkspaceRoot(), "skills"));
741
+ console.log("📥 Baixando skills do GoClaw...");
742
+ const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
743
+ const response = await axios_1.default.get(url, {
744
+ headers: {
745
+ Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
746
+ },
747
+ responseType: "stream"
748
+ });
749
+ const tempTarPath = path_1.default.join(getWorkspaceRoot(), "temp_skills.tar.gz");
750
+ const writer = fs_extra_1.default.createWriteStream(tempTarPath);
751
+ response.data.pipe(writer);
752
+ await new Promise((resolve, reject) => {
753
+ writer.on("finish", resolve);
754
+ writer.on("error", reject);
755
+ });
756
+ console.log("📦 Extraindo skills para a pasta local...");
757
+ await tar.x({
758
+ file: tempTarPath,
759
+ cwd: getWorkspaceRoot()
760
+ });
761
+ await fs_extra_1.default.remove(tempTarPath);
762
+ console.log("📥 Baixando ficheiros de código das skills...");
763
+ const skillsListRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills`, {
764
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
765
+ });
766
+ const skills = skillsListRes.data.skills || [];
767
+ for (const skill of skills) {
768
+ try {
769
+ const isSystem = skill.is_system === true;
770
+ const targetFolder = isSystem ? path_1.default.join("system", skill.slug) : skill.slug;
771
+ if (isSystem) {
772
+ const originalPath = path_1.default.join(getWorkspaceRoot(), "skills", skill.slug);
773
+ const newPath = path_1.default.join(getWorkspaceRoot(), "skills", targetFolder);
774
+ if (await fs_extra_1.default.pathExists(originalPath)) {
775
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(newPath));
776
+ await fs_extra_1.default.move(originalPath, newPath, { overwrite: true });
777
+ }
778
+ }
779
+ const filesRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
780
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
781
+ });
782
+ const files = filesRes.data.files || [];
783
+ for (const file of files) {
784
+ if (file.isDir)
785
+ continue;
786
+ const fileContentRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
787
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
788
+ });
789
+ const filePath = path_1.default.join(getWorkspaceRoot(), "skills", targetFolder, file.path);
790
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
791
+ await fs_extra_1.default.writeFile(filePath, fileContentRes.data.content || "");
792
+ }
793
+ }
794
+ catch (fileErr) {
795
+ console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
796
+ }
797
+ }
798
+ // Remover quaisquer skills fantasmas que o tarball tenha extraído
799
+ const validSlugs = new Set(skills.map((s) => s.is_system === true ? path_1.default.join("system", s.slug) : s.slug));
800
+ const skillsDir = path_1.default.join(getWorkspaceRoot(), "skills");
801
+ if (await fs_extra_1.default.pathExists(skillsDir)) {
802
+ const localItems = await fs_extra_1.default.readdir(skillsDir);
803
+ for (const item of localItems) {
804
+ if (item === "system") {
805
+ const systemDir = path_1.default.join(skillsDir, "system");
806
+ if (await fs_extra_1.default.pathExists(systemDir)) {
807
+ const systemItems = await fs_extra_1.default.readdir(systemDir);
808
+ for (const sysItem of systemItems) {
809
+ if (!validSlugs.has(path_1.default.join("system", sysItem))) {
810
+ await fs_extra_1.default.remove(path_1.default.join(systemDir, sysItem));
811
+ }
812
+ }
813
+ }
814
+ }
815
+ else if (!validSlugs.has(item)) {
816
+ await fs_extra_1.default.remove(path_1.default.join(skillsDir, item));
817
+ }
818
+ }
819
+ }
820
+ }
613
821
  pullCmd
614
822
  .command("skills")
615
823
  .description("Faz download do arquivo tar.gz de skills do GoClaw e extrai localmente")
@@ -623,100 +831,97 @@ pullCmd
623
831
  console.log("❌ Pull cancelado pelo utilizador.");
624
832
  return;
625
833
  }
626
- console.log("🧹 Limpando a pasta local de skills...");
627
- await fs_extra_1.default.emptyDir(path_1.default.join(getWorkspaceRoot(), "skills"));
628
- console.log("📥 Baixando skills do GoClaw...");
629
834
  try {
630
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
631
- const response = await axios_1.default.get(url, {
632
- headers: {
633
- Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
634
- },
635
- responseType: "stream"
636
- });
637
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), "temp_skills.tar.gz");
835
+ await pullAllSkills(config);
836
+ console.log("✅ Pull de skills concluído com sucesso! As skills foram atualizadas localmente.");
837
+ }
838
+ catch (error) {
839
+ console.error("❌ Erro durante o pull das skills:");
840
+ if (error.response) {
841
+ console.error(`Status HTTP ${error.response.status}`);
842
+ }
843
+ else {
844
+ console.error(error.message);
845
+ }
846
+ }
847
+ });
848
+ async function pullAgent(slug, agentId, config) {
849
+ console.log(`📦 Baixando agente: ${slug}...`);
850
+ const url = `${config.goclaw.api_url}/v1/agents/${agentId}/export`;
851
+ const response = await axios_1.default.get(url, {
852
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" },
853
+ responseType: "stream"
854
+ });
855
+ const tempTarPath = path_1.default.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
856
+ try {
638
857
  const writer = fs_extra_1.default.createWriteStream(tempTarPath);
639
858
  response.data.pipe(writer);
640
859
  await new Promise((resolve, reject) => {
641
860
  writer.on("finish", resolve);
642
861
  writer.on("error", reject);
643
862
  });
644
- console.log("📦 Extraindo skills para a pasta local...");
863
+ const agentPath = path_1.default.join(getWorkspaceRoot(), "agents", slug);
864
+ if (await fs_extra_1.default.pathExists(agentPath)) {
865
+ await fs_extra_1.default.emptyDir(agentPath);
866
+ }
867
+ else {
868
+ await fs_extra_1.default.ensureDir(agentPath);
869
+ }
870
+ // Obter os caminhos reais (com barras) da API para reverter o flattening do export
871
+ const docsRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/agents/${agentId}/memory/documents`, {
872
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
873
+ });
874
+ const pathMap = {};
875
+ (docsRes.data || []).forEach((d) => {
876
+ if (d.path) {
877
+ const flat = d.path.replace(/[\/\\]/g, '_');
878
+ pathMap[flat] = d.path;
879
+ }
880
+ });
645
881
  await tar.x({
646
882
  file: tempTarPath,
647
- cwd: getWorkspaceRoot()
648
- });
649
- await fs_extra_1.default.remove(tempTarPath);
650
- console.log("📥 Baixando ficheiros de código das skills...");
651
- const skillsListRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills`, {
652
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
883
+ cwd: agentPath,
884
+ strip: 0,
885
+ filter: (path) => {
886
+ return path === 'agent.json' || path.startsWith('context_files/');
887
+ }
653
888
  });
654
- const skills = skillsListRes.data.skills || [];
655
- for (const skill of skills) {
656
- try {
657
- const isSystem = skill.is_system === true;
658
- const targetFolder = isSystem ? path_1.default.join("system", skill.slug) : skill.slug;
659
- if (isSystem) {
660
- const originalPath = path_1.default.join(getWorkspaceRoot(), "skills", skill.slug);
661
- const newPath = path_1.default.join(getWorkspaceRoot(), "skills", targetFolder);
662
- if (await fs_extra_1.default.pathExists(originalPath)) {
663
- await fs_extra_1.default.ensureDir(path_1.default.dirname(newPath));
664
- await fs_extra_1.default.move(originalPath, newPath, { overwrite: true });
889
+ const contextDir = path_1.default.join(agentPath, "context_files");
890
+ if (await fs_extra_1.default.pathExists(contextDir)) {
891
+ const contextFiles = await fs_extra_1.default.readdir(contextDir);
892
+ for (const f of contextFiles) {
893
+ const filePath = path_1.default.join(contextDir, f);
894
+ const stat = await fs_extra_1.default.stat(filePath);
895
+ if (stat.isFile()) {
896
+ const content = await fs_extra_1.default.readFile(filePath, 'utf8');
897
+ if (content === " " || content === "") {
898
+ // Fantasma neutralizado pelo nosso script de deploy! Deita fora!
899
+ await fs_extra_1.default.remove(filePath);
900
+ continue;
665
901
  }
666
902
  }
667
- const filesRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
668
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
669
- });
670
- const files = filesRes.data.files || [];
671
- for (const file of files) {
672
- if (file.isDir)
673
- continue;
674
- const fileContentRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
675
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
676
- });
677
- const filePath = path_1.default.join(getWorkspaceRoot(), "skills", targetFolder, file.path);
678
- await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
679
- await fs_extra_1.default.writeFile(filePath, fileContentRes.data.content || "");
903
+ if (pathMap[f]) {
904
+ const targetPath = path_1.default.join(agentPath, pathMap[f]);
905
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath));
906
+ await fs_extra_1.default.move(filePath, targetPath, { overwrite: true });
680
907
  }
681
- }
682
- catch (fileErr) {
683
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
684
- }
685
- }
686
- // Remover quaisquer skills fantasmas que o tarball tenha extraído (skills apagadas mas ainda no export)
687
- const validSlugs = new Set(skills.map((s) => s.is_system === true ? path_1.default.join("system", s.slug) : s.slug));
688
- const skillsDir = path_1.default.join(getWorkspaceRoot(), "skills");
689
- if (await fs_extra_1.default.pathExists(skillsDir)) {
690
- const localItems = await fs_extra_1.default.readdir(skillsDir);
691
- for (const item of localItems) {
692
- if (item === "system") {
693
- const systemDir = path_1.default.join(skillsDir, "system");
694
- if (await fs_extra_1.default.pathExists(systemDir)) {
695
- const systemItems = await fs_extra_1.default.readdir(systemDir);
696
- for (const sysItem of systemItems) {
697
- if (!validSlugs.has(path_1.default.join("system", sysItem))) {
698
- await fs_extra_1.default.remove(path_1.default.join(systemDir, sysItem));
699
- }
700
- }
701
- }
908
+ else if (f.startsWith('_system_') || f.startsWith('memory_')) {
909
+ // É um stub de diretório do export do GoClaw (ex: _system_dreaming_) ou um órfão esmagado. Ignoramos.
910
+ await fs_extra_1.default.remove(filePath);
702
911
  }
703
- else if (!validSlugs.has(item)) {
704
- await fs_extra_1.default.remove(path_1.default.join(skillsDir, item));
912
+ else {
913
+ await fs_extra_1.default.move(filePath, path_1.default.join(agentPath, f), { overwrite: true });
705
914
  }
706
915
  }
916
+ await fs_extra_1.default.remove(contextDir);
707
917
  }
708
- console.log("✅ Pull concluído com sucesso! As skills foram atualizadas localmente.");
709
918
  }
710
- catch (error) {
711
- console.error("❌ Erro durante o pull das skills:");
712
- if (error.response) {
713
- console.error(`Status HTTP ${error.response.status}`);
714
- }
715
- else {
716
- console.error(error.message);
919
+ finally {
920
+ if (await fs_extra_1.default.pathExists(tempTarPath)) {
921
+ await fs_extra_1.default.remove(tempTarPath);
717
922
  }
718
923
  }
719
- });
924
+ }
720
925
  pullCmd
721
926
  .command("agents")
722
927
  .description("Faz download cirúrgico dos agentes (configuração e contexto)")
@@ -741,77 +946,7 @@ pullCmd
741
946
  console.log(`Encontrados ${agents.length} agentes. Sincronizando...`);
742
947
  for (const agent of agents) {
743
948
  const slug = agent.agent_key;
744
- console.log(`📦 Baixando agente: ${slug}...`);
745
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export?sections=config,context_files,memory`;
746
- const response = await axios_1.default.get(url, {
747
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" },
748
- responseType: "stream"
749
- });
750
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
751
- try {
752
- const writer = fs_extra_1.default.createWriteStream(tempTarPath);
753
- response.data.pipe(writer);
754
- await new Promise((resolve, reject) => {
755
- writer.on("finish", resolve);
756
- writer.on("error", reject);
757
- });
758
- const agentPath = path_1.default.join(getWorkspaceRoot(), "agents", slug);
759
- await fs_extra_1.default.ensureDir(agentPath);
760
- await tar.x({
761
- file: tempTarPath,
762
- cwd: agentPath,
763
- strip: 0,
764
- filter: (path) => {
765
- return path === 'agent.json' || path.startsWith('context_files/') || path.startsWith('memory/') || path === 'MEMORY.md' || path === 'memory.md';
766
- }
767
- });
768
- const contextDir = path_1.default.join(agentPath, "context_files");
769
- if (await fs_extra_1.default.pathExists(contextDir)) {
770
- const contextFiles = await fs_extra_1.default.readdir(contextDir);
771
- for (const f of contextFiles) {
772
- await fs_extra_1.default.move(path_1.default.join(contextDir, f), path_1.default.join(agentPath, f), { overwrite: true });
773
- }
774
- await fs_extra_1.default.remove(contextDir);
775
- }
776
- // Reconstruir ficheiros de memória a partir de JSONL
777
- const memoryDir = path_1.default.join(agentPath, "memory");
778
- if (await fs_extra_1.default.pathExists(memoryDir)) {
779
- const processJsonl = async (filePath) => {
780
- if (!(await fs_extra_1.default.pathExists(filePath)))
781
- return;
782
- const content = await fs_extra_1.default.readFile(filePath, 'utf8');
783
- const lines = content.split('\n').filter(l => l.trim());
784
- for (const line of lines) {
785
- try {
786
- const entry = JSON.parse(line);
787
- if (entry.path && entry.content) {
788
- const targetPath = path_1.default.join(agentPath, entry.path);
789
- await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath));
790
- await fs_extra_1.default.writeFile(targetPath, entry.content);
791
- }
792
- }
793
- catch (e) { }
794
- }
795
- await fs_extra_1.default.remove(filePath);
796
- };
797
- await processJsonl(path_1.default.join(memoryDir, "global.jsonl"));
798
- const usersDir = path_1.default.join(memoryDir, "users");
799
- if (await fs_extra_1.default.pathExists(usersDir)) {
800
- const userFiles = await fs_extra_1.default.readdir(usersDir);
801
- for (const uf of userFiles) {
802
- if (uf.endsWith(".jsonl")) {
803
- await processJsonl(path_1.default.join(usersDir, uf));
804
- }
805
- }
806
- await fs_extra_1.default.remove(usersDir);
807
- }
808
- }
809
- }
810
- finally {
811
- if (await fs_extra_1.default.pathExists(tempTarPath)) {
812
- await fs_extra_1.default.remove(tempTarPath);
813
- }
814
- }
949
+ await pullAgent(slug, agent.id, config);
815
950
  }
816
951
  console.log("✅ Pull de agentes concluído com sucesso!");
817
952
  }
@@ -841,55 +976,7 @@ pullCmd
841
976
  // PULL SKILLS INLINE
842
977
  console.log('\n--- [1/2] SKILLS ---');
843
978
  try {
844
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
845
- const response = await axios_1.default.get(url, {
846
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
847
- responseType: 'stream'
848
- });
849
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), 'temp_skills.tar.gz');
850
- const writer = fs_extra_1.default.createWriteStream(tempTarPath);
851
- response.data.pipe(writer);
852
- await new Promise((resolve, reject) => {
853
- writer.on('finish', resolve);
854
- writer.on('error', reject);
855
- });
856
- await tar.x({ file: tempTarPath, cwd: getWorkspaceRoot() });
857
- await fs_extra_1.default.remove(tempTarPath);
858
- const skillsListRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills`, {
859
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
860
- });
861
- const skills = skillsListRes.data.skills || [];
862
- for (const skill of skills) {
863
- try {
864
- const isSystem = skill.is_system === true;
865
- const targetFolder = isSystem ? path_1.default.join('system', skill.slug) : skill.slug;
866
- if (isSystem) {
867
- const originalPath = path_1.default.join(getWorkspaceRoot(), 'skills', skill.slug);
868
- const newPath = path_1.default.join(getWorkspaceRoot(), 'skills', targetFolder);
869
- if (await fs_extra_1.default.pathExists(originalPath)) {
870
- await fs_extra_1.default.ensureDir(path_1.default.dirname(newPath));
871
- await fs_extra_1.default.move(originalPath, newPath, { overwrite: true });
872
- }
873
- }
874
- const filesRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
875
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
876
- });
877
- const files = filesRes.data.files || [];
878
- for (const file of files) {
879
- if (file.isDir)
880
- continue;
881
- const fileContentRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
882
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
883
- });
884
- const filePath = path_1.default.join(getWorkspaceRoot(), 'skills', targetFolder, file.path);
885
- await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
886
- await fs_extra_1.default.writeFile(filePath, fileContentRes.data.content || '');
887
- }
888
- }
889
- catch (fileErr) {
890
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
891
- }
892
- }
979
+ await pullAllSkills(config);
893
980
  console.log('✅ Pull de skills concluído!');
894
981
  }
895
982
  catch (error) {
@@ -905,44 +992,7 @@ pullCmd
905
992
  console.log(`Encontrados ${agents.length} agentes. Sincronizando...`);
906
993
  for (const agent of agents) {
907
994
  const slug = agent.agent_key;
908
- console.log(`📦 Baixando agente: ${slug}...`);
909
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export`;
910
- const response = await axios_1.default.get(url, {
911
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
912
- responseType: 'stream'
913
- });
914
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
915
- try {
916
- const writer = fs_extra_1.default.createWriteStream(tempTarPath);
917
- response.data.pipe(writer);
918
- await new Promise((resolve, reject) => {
919
- writer.on('finish', resolve);
920
- writer.on('error', reject);
921
- });
922
- const agentPath = path_1.default.join(getWorkspaceRoot(), 'agents', slug);
923
- await fs_extra_1.default.ensureDir(agentPath);
924
- await tar.x({
925
- file: tempTarPath,
926
- cwd: agentPath,
927
- strip: 0,
928
- filter: (path) => {
929
- return path === 'agent.json' || path.startsWith('context_files/');
930
- }
931
- });
932
- const contextDir = path_1.default.join(agentPath, 'context_files');
933
- if (await fs_extra_1.default.pathExists(contextDir)) {
934
- const contextFiles = await fs_extra_1.default.readdir(contextDir);
935
- for (const f of contextFiles) {
936
- await fs_extra_1.default.move(path_1.default.join(contextDir, f), path_1.default.join(agentPath, f), { overwrite: true });
937
- }
938
- await fs_extra_1.default.remove(contextDir);
939
- }
940
- }
941
- finally {
942
- if (await fs_extra_1.default.pathExists(tempTarPath)) {
943
- await fs_extra_1.default.remove(tempTarPath);
944
- }
945
- }
995
+ await pullAgent(slug, agent.id, config);
946
996
  }
947
997
  console.log('✅ Pull de agentes concluído!');
948
998
  }