@innominatum/agentforge-cli 1.1.3 → 1.1.22

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
@@ -314,9 +314,106 @@ const deployCmd = program
314
314
  .command("deploy")
315
315
  .description("Faz o deploy de entidades para a plataforma GoClaw");
316
316
 
317
+ async function deploySkill(slug: string, config: any, basePath: string) {
318
+ const skillPath = path.join(basePath, "skills", slug);
319
+ const exportsPath = path.join(basePath, "exports");
320
+ const safeSlug = slug.replace(/[\\\/]/g, '_');
321
+ const zipPath = path.join(exportsPath, `${safeSlug}.zip`);
322
+
323
+ if (!(await fs.pathExists(skillPath))) {
324
+ console.error(`❌ A skill "${slug}" não foi encontrada em skills/${slug}.`);
325
+ return;
326
+ }
327
+
328
+ await fs.ensureDir(exportsPath);
329
+ const zip = new AdmZip();
330
+ zip.addLocalFolder(skillPath, "");
331
+ zip.writeZip(zipPath);
332
+
333
+ console.log(`🚀 Fazendo upload da skill "${slug}" para o GoClaw...`);
334
+ const form = new FormData();
335
+ form.append("file", fs.createReadStream(zipPath));
336
+
337
+ try {
338
+ const url = `${config.goclaw.api_url}/v1/skills/upload`;
339
+ const response = await axios.post(url, form, {
340
+ headers: {
341
+ ...form.getHeaders(),
342
+ Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
343
+ }
344
+ });
345
+ const data = response.data;
346
+ if (data && data.version) {
347
+ console.log(`✅ Arquivos da skill "${slug}" atualizados (versão ${data.version}).`);
348
+ } else {
349
+ console.log(`✅ Arquivos da skill "${slug}" atualizados.`);
350
+ }
351
+
352
+ // Sincronizar metadados (visibility, description, tags, etc)
353
+ const skillsListRes = await axios.get(`${config.goclaw.api_url}/v1/skills`, {
354
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
355
+ });
356
+ const remoteSkill = skillsListRes.data.skills?.find((s: any) => s.slug === slug);
357
+
358
+ if (remoteSkill) {
359
+ const metadataPath = path.join(skillPath, "metadata.json");
360
+ if (await fs.pathExists(metadataPath)) {
361
+ console.log(`🚀 Sincronizando metadados da skill "${slug}"...`);
362
+ const metadata = await fs.readJson(metadataPath);
363
+
364
+ // Remover campos que não devem ser enviados no PUT
365
+ const payload = { ...metadata };
366
+ delete payload.id;
367
+ delete payload.slug;
368
+ delete payload.name;
369
+
370
+ await axios.put(`${config.goclaw.api_url}/v1/skills/${remoteSkill.id}`, payload, {
371
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
372
+ });
373
+ console.log(`✅ Metadados sincronizados com sucesso.`);
374
+ }
375
+
376
+ // Sincronizar permissões (grants)
377
+ const grantsPath = path.join(skillPath, "grants.jsonl");
378
+ if (await fs.pathExists(grantsPath)) {
379
+ console.log(`🚀 Sincronizando permissões (grants) da skill "${slug}"...`);
380
+ const grantsContent = await fs.readFile(grantsPath, 'utf8');
381
+ const lines = grantsContent.split('\n').filter(l => l.trim());
382
+
383
+ for (const line of lines) {
384
+ try {
385
+ const grant = JSON.parse(line);
386
+ if (grant.agent_key) {
387
+ const agentId = await resolveAgentId(grant.agent_key, config);
388
+ if (agentId) {
389
+ await axios.post(`${config.goclaw.api_url}/v1/skills/${remoteSkill.id}/grants/agent`,
390
+ { agent_id: agentId, version: grant.pinned_version || null },
391
+ { headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" } }
392
+ );
393
+ console.log(` ➕ Permissão concedida ao agente: ${grant.agent_key}`);
394
+ }
395
+ }
396
+ } catch (e: any) {
397
+ console.warn(` ⚠️ Falha ao conceder permissão: ${e.response?.data?.error || e.message}`);
398
+ }
399
+ }
400
+ console.log(`✅ Permissões sincronizadas.`);
401
+ }
402
+ }
403
+
404
+ } catch (error: any) {
405
+ console.error(`❌ Erro no deploy da skill "${slug}":`);
406
+ if (error.response) {
407
+ console.error(error.response.data);
408
+ } else {
409
+ console.error(error.message);
410
+ }
411
+ }
412
+ }
413
+
317
414
  deployCmd
318
415
  .command("skill <slug>")
319
- .description("Faz o build da skill e envia para a API do GoClaw")
416
+ .description("Faz build e upload automático de uma skill para o GoClaw")
320
417
  .action(async (slug: string) => {
321
418
  const config = await getConfig();
322
419
  if (!config.goclaw || !config.goclaw.token) {
@@ -325,46 +422,7 @@ deployCmd
325
422
  }
326
423
 
327
424
  const basePath = getWorkspaceRoot();
328
- const skillPath = path.join(basePath, "skills", slug);
329
- const exportsPath = path.join(basePath, "exports");
330
- const zipPath = path.join(exportsPath, `${slug}.zip`);
331
-
332
- if (!(await fs.pathExists(skillPath))) {
333
- console.error(`❌ A skill "${slug}" não foi encontrada em skills/${slug}.`);
334
- process.exit(1);
335
- }
336
-
337
- await fs.ensureDir(exportsPath);
338
- const zip = new AdmZip();
339
- zip.addLocalFolder(skillPath, "");
340
- zip.writeZip(zipPath);
341
-
342
- console.log(`✅ Build concluído: ${slug}.zip preparado para envio.`);
343
-
344
- console.log(`🚀 Fazendo upload para o GoClaw...`);
345
- const form = new FormData();
346
- form.append("file", fs.createReadStream(zipPath));
347
-
348
- try {
349
- const url = `${config.goclaw.api_url}/v1/skills/upload`;
350
- const response = await axios.post(url, form, {
351
- headers: {
352
- ...form.getHeaders(),
353
- Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
354
- }
355
- });
356
- const data = response.data;
357
- if (data && data.version) {
358
- console.log(`📌 Skill atualizada para a versão ${data.version}.`);
359
- }
360
- } catch (error: any) {
361
- console.error("❌ Erro durante o deploy:");
362
- if (error.response) {
363
- console.error(error.response.data);
364
- } else {
365
- console.error(error.message);
366
- }
367
- }
425
+ await deploySkill(slug, config, basePath);
368
426
  });
369
427
 
370
428
  async function deployContextFiles(slug: string, config: any, resolvedId?: string | null) {
@@ -376,66 +434,121 @@ async function deployContextFiles(slug: string, config: any, resolvedId?: string
376
434
  }
377
435
 
378
436
  const files = await fs.readdir(agentPath);
379
-
380
- // Identificar ficheiros de contexto e de memória
381
- const memoryFileNames = ['MEMORY.md', 'memory.md'];
382
- const memoryDirName = 'memory';
383
-
384
- const contextFiles = files.filter(f =>
385
- (f.endsWith('.md') || f.endsWith('.txt') || f.endsWith('.py')) &&
386
- f !== 'README.md' &&
387
- f !== 'agent.json' &&
388
- !memoryFileNames.includes(f)
389
- );
390
-
391
- const hasMemoryDir = await fs.pathExists(path.join(agentPath, memoryDirName));
392
- const memoryFilesFound = files.filter(f => memoryFileNames.includes(f));
393
- const hasMemory = hasMemoryDir || memoryFilesFound.length > 0;
437
+ const itemsToSync = files.filter(f => f !== 'agent.json' && f !== 'README.md');
394
438
 
395
- if (contextFiles.length === 0 && !hasMemory) {
439
+ if (itemsToSync.length === 0) {
396
440
  console.log(`Nenhum ficheiro de contexto ou memória encontrado para "${slug}".`);
397
441
  return;
398
442
  }
399
443
 
400
444
  const tempExportDir = path.join(basePath, `temp_export_${slug}`);
401
- const tempContextDir = path.join(tempExportDir, "context_files");
402
- const tempMemoryDir = path.join(tempExportDir, "memory");
403
445
  const tarPath = path.join(basePath, `temp_export_${slug}.tar.gz`);
404
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
+
405
466
  try {
406
- const sections: string[] = [];
467
+ const sections = new Set<string>();
407
468
 
408
- if (contextFiles.length > 0) {
409
- sections.push("context_files");
410
- await fs.ensureDir(tempContextDir);
411
- for (const file of contextFiles) {
412
- 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));
413
495
  }
414
496
  }
415
497
 
416
- if (hasMemory) {
417
- sections.push("memory");
418
- await fs.ensureDir(tempMemoryDir);
419
- // Copiar ficheiros de memória explícitos para a pasta memory no arquivo
420
- for (const file of memoryFilesFound) {
421
- 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/");
422
510
  }
423
- // Copiar conteúdo da pasta memory se existir
424
- if (hasMemoryDir) {
425
- 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
+ }
426
536
  }
537
+ } catch (e: any) {
538
+ console.warn("Aviso: Falha ao procurar fantasmas para o tarball.", e.message);
427
539
  }
428
540
 
429
541
  await tar.c({
430
542
  gzip: true,
431
543
  file: tarPath,
432
544
  cwd: tempExportDir
433
- }, sections);
545
+ }, sectionsArray);
434
546
 
435
547
  const form = new FormData();
436
548
  form.append("file", fs.createReadStream(tarPath));
437
549
 
438
- 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(",")}`;
439
552
  await axios.post(url, form, {
440
553
  headers: {
441
554
  ...form.getHeaders(),
@@ -444,7 +557,76 @@ async function deployContextFiles(slug: string, config: any, resolvedId?: string
444
557
  }
445
558
  });
446
559
 
447
- 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
+
448
630
  } finally {
449
631
  if (await fs.pathExists(tempExportDir)) await fs.remove(tempExportDir);
450
632
  if (await fs.pathExists(tarPath)) await fs.remove(tarPath);
@@ -519,8 +701,45 @@ deployCmd
519
701
  await deployAgent(slug, config);
520
702
  });
521
703
 
704
+
705
+ async function deployAllAgents(config: any, basePath: string) {
706
+ const agentsDir = path.join(basePath, "agents");
707
+ if (await fs.pathExists(agentsDir)) {
708
+ const agents = await fs.readdir(agentsDir);
709
+ console.log(`🚀 Iniciando deploy em lote de ${agents.length} agentes...`);
710
+ for (const slug of agents) {
711
+ const agentPath = path.join(agentsDir, slug);
712
+ if ((await fs.stat(agentPath)).isDirectory()) {
713
+ await deployAgent(slug, config);
714
+ }
715
+ }
716
+ } else {
717
+ console.log("Nenhum agente encontrado em agents/.");
718
+ }
719
+ }
720
+
721
+ async function deployAllSkills(config: any, basePath: string) {
722
+ const skillsDir = path.join(basePath, "skills");
723
+ if (await fs.pathExists(skillsDir)) {
724
+ const skills = await fs.readdir(skillsDir);
725
+ console.log(`🚀 Iniciando deploy em lote de skills...`);
726
+ for (const item of skills) {
727
+ const itemPath = path.join(skillsDir, item);
728
+ if ((await fs.stat(itemPath)).isDirectory()) {
729
+ if (item === "system") {
730
+ console.log("⏩ Ignorando pasta 'system/' (skills nativas do GoClaw são apenas de leitura)");
731
+ continue;
732
+ }
733
+ await deploySkill(item, config, basePath);
734
+ }
735
+ }
736
+ } else {
737
+ console.log("Nenhuma skill encontrada em skills/.");
738
+ }
739
+ }
740
+
522
741
  deployCmd
523
- .command("all")
742
+ .command("agents")
524
743
  .description("Faz deploy de todos os agentes do workspace")
525
744
  .action(async () => {
526
745
  const config = await getConfig();
@@ -528,32 +747,137 @@ deployCmd
528
747
  console.error("❌ Configure sua chave de API (token) no agentforge.json.");
529
748
  process.exit(1);
530
749
  }
531
-
532
750
  const basePath = getWorkspaceRoot();
533
- const agentsDir = path.join(basePath, "agents");
534
-
535
- if (!(await fs.pathExists(agentsDir))) {
536
- console.log("Nenhum agente encontrado em agents/.");
537
- return;
751
+ await deployAllAgents(config, basePath);
752
+ console.log("🏁 Deploy de agentes concluído!");
753
+ });
754
+
755
+ deployCmd
756
+ .command("skills")
757
+ .description("Faz deploy de todas as skills do workspace")
758
+ .action(async () => {
759
+ const config = await getConfig();
760
+ if (!config.goclaw || !config.goclaw.token) {
761
+ console.error("❌ Configure sua chave de API (token) no agentforge.json.");
762
+ process.exit(1);
538
763
  }
764
+ const basePath = getWorkspaceRoot();
765
+ await deployAllSkills(config, basePath);
766
+ console.log("🏁 Deploy de skills concluído!");
767
+ });
539
768
 
540
- const agents = await fs.readdir(agentsDir);
541
- console.log(`🚀 Iniciando deploy em lote de ${agents.length} agentes...`);
542
-
543
- for (const slug of agents) {
544
- const agentPath = path.join(agentsDir, slug);
545
- if ((await fs.stat(agentPath)).isDirectory()) {
546
- await deployAgent(slug, config);
547
- }
769
+ deployCmd
770
+ .command("all")
771
+ .description("Faz deploy de todos os agentes e skills do workspace")
772
+ .action(async () => {
773
+ const config = await getConfig();
774
+ if (!config.goclaw || !config.goclaw.token) {
775
+ console.error("❌ Configure sua chave de API (token) no agentforge.json.");
776
+ process.exit(1);
548
777
  }
778
+
779
+ const basePath = getWorkspaceRoot();
780
+ await deployAllAgents(config, basePath);
781
+ await deployAllSkills(config, basePath);
549
782
 
550
- console.log("🏁 Deploy em lote concluído!");
783
+ console.log("🏁 Deploy completo (agentes e skills) concluído!");
551
784
  });
552
785
 
553
786
  const pullCmd = program
554
787
  .command("pull")
555
788
  .description("Sincroniza entidades do GoClaw para o workspace local");
556
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
+
557
881
  pullCmd
558
882
  .command("skills")
559
883
  .description("Faz download do arquivo tar.gz de skills do GoClaw e extrai localmente")
@@ -569,106 +893,101 @@ pullCmd
569
893
  return;
570
894
  }
571
895
 
572
- console.log("🧹 Limpando a pasta local de skills...");
573
- await fs.emptyDir(path.join(getWorkspaceRoot(), "skills"));
574
-
575
- console.log("📥 Baixando skills do GoClaw...");
576
896
  try {
577
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
578
- const response = await axios.get(url, {
579
- headers: {
580
- Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
581
- },
582
- responseType: "stream"
583
- });
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
+ });
584
908
 
585
- const tempTarPath = path.join(getWorkspaceRoot(), "temp_skills.tar.gz");
586
- const writer = fs.createWriteStream(tempTarPath);
587
- 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
+ });
588
917
 
589
- await new Promise((resolve, reject) => {
590
- writer.on("finish", resolve);
591
- writer.on("error", reject);
592
- });
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);
593
923
 
594
- console.log("📦 Extraindo skills para a pasta local...");
595
- await tar.x({
596
- file: tempTarPath,
597
- cwd: getWorkspaceRoot()
598
- });
599
- await fs.remove(tempTarPath);
924
+ await new Promise((resolve, reject) => {
925
+ writer.on("finish", resolve);
926
+ writer.on("error", reject);
927
+ });
600
928
 
601
- console.log("📥 Baixando ficheiros de código das skills...");
602
- const skillsListRes = await axios.get(`${config.goclaw.api_url}/v1/skills`, {
603
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
604
- });
605
-
606
- const skills = skillsListRes.data.skills || [];
607
- for (const skill of skills) {
608
- try {
609
- const isSystem = skill.is_system === true;
610
- const targetFolder = isSystem ? path.join("system", skill.slug) : skill.slug;
611
-
612
- if (isSystem) {
613
- const originalPath = path.join(getWorkspaceRoot(), "skills", skill.slug);
614
- const newPath = path.join(getWorkspaceRoot(), "skills", targetFolder);
615
- if (await fs.pathExists(originalPath)) {
616
- await fs.ensureDir(path.dirname(newPath));
617
- await fs.move(originalPath, newPath, { overwrite: true });
618
- }
619
- }
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
+ }
620
935
 
621
- const filesRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
622
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
623
- });
624
-
625
- const files = filesRes.data.files || [];
626
- for (const file of files) {
627
- if (file.isDir) continue;
628
- const fileContentRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
629
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
630
- });
631
- const filePath = path.join(getWorkspaceRoot(), "skills", targetFolder, file.path);
632
- await fs.ensureDir(path.dirname(filePath));
633
- await fs.writeFile(filePath, fileContentRes.data.content || "");
634
- }
635
- } catch (fileErr: any) {
636
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
637
- }
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;
638
945
  }
946
+ });
639
947
 
640
- // Remover quaisquer skills fantasmas que o tarball tenha extraído (skills apagadas mas ainda no export)
641
- const validSlugs = new Set(skills.map((s: any) => s.is_system === true ? path.join("system", s.slug) : s.slug));
642
- const skillsDir = path.join(getWorkspaceRoot(), "skills");
643
- if (await fs.pathExists(skillsDir)) {
644
- const localItems = await fs.readdir(skillsDir);
645
- for (const item of localItems) {
646
- if (item === "system") {
647
- const systemDir = path.join(skillsDir, "system");
648
- if (await fs.pathExists(systemDir)) {
649
- const systemItems = await fs.readdir(systemDir);
650
- for (const sysItem of systemItems) {
651
- if (!validSlugs.has(path.join("system", sysItem))) {
652
- await fs.remove(path.join(systemDir, sysItem));
653
- }
654
- }
655
- }
656
- } else if (!validSlugs.has(item)) {
657
- 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;
658
969
  }
659
970
  }
660
- }
661
971
 
662
- console.log("✅ Pull concluído com sucesso! As skills foram atualizadas localmente.");
663
- } catch (error: any) {
664
- console.error("❌ Erro durante o pull das skills:");
665
- if (error.response) {
666
- console.error(`Status HTTP ${error.response.status}`);
667
- } else {
668
- 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
+ }
669
982
  }
983
+ await fs.remove(contextDir);
670
984
  }
671
- });
985
+ } finally {
986
+ if (await fs.pathExists(tempTarPath)) {
987
+ await fs.remove(tempTarPath);
988
+ }
989
+ }
990
+ }
672
991
 
673
992
  pullCmd
674
993
  .command("agents")
@@ -699,84 +1018,9 @@ pullCmd
699
1018
 
700
1019
  for (const agent of agents) {
701
1020
  const slug = agent.agent_key;
702
- console.log(`📦 Baixando agente: ${slug}...`);
703
-
704
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export?sections=config,context_files,memory`;
705
- const response = await axios.get(url, {
706
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" },
707
- responseType: "stream"
708
- });
709
-
710
- const tempTarPath = path.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
711
-
712
- try {
713
- const writer = fs.createWriteStream(tempTarPath);
714
- response.data.pipe(writer);
715
-
716
- await new Promise((resolve, reject) => {
717
- writer.on("finish", resolve);
718
- writer.on("error", reject);
719
- });
720
-
721
- const agentPath = path.join(getWorkspaceRoot(), "agents", slug);
722
- await fs.ensureDir(agentPath);
723
-
724
- await tar.x({
725
- file: tempTarPath,
726
- cwd: agentPath,
727
- strip: 0,
728
- filter: (path) => {
729
- return path === 'agent.json' || path.startsWith('context_files/') || path.startsWith('memory/') || path === 'MEMORY.md' || path === 'memory.md';
730
- }
731
- });
732
-
733
- const contextDir = path.join(agentPath, "context_files");
734
- if (await fs.pathExists(contextDir)) {
735
- const contextFiles = await fs.readdir(contextDir);
736
- for (const f of contextFiles) {
737
- await fs.move(path.join(contextDir, f), path.join(agentPath, f), { overwrite: true });
738
- }
739
- await fs.remove(contextDir);
740
- }
741
-
742
- // Reconstruir ficheiros de memória a partir de JSONL
743
- const memoryDir = path.join(agentPath, "memory");
744
- if (await fs.pathExists(memoryDir)) {
745
- const processJsonl = async (filePath: string) => {
746
- if (!(await fs.pathExists(filePath))) return;
747
- const content = await fs.readFile(filePath, 'utf8');
748
- const lines = content.split('\n').filter(l => l.trim());
749
- for (const line of lines) {
750
- try {
751
- const entry = JSON.parse(line);
752
- if (entry.path && entry.content) {
753
- const targetPath = path.join(agentPath, entry.path);
754
- await fs.ensureDir(path.dirname(targetPath));
755
- await fs.writeFile(targetPath, entry.content);
756
- }
757
- } catch (e) {}
758
- }
759
- await fs.remove(filePath);
760
- };
761
-
762
- await processJsonl(path.join(memoryDir, "global.jsonl"));
763
- const usersDir = path.join(memoryDir, "users");
764
- if (await fs.pathExists(usersDir)) {
765
- const userFiles = await fs.readdir(usersDir);
766
- for (const uf of userFiles) {
767
- if (uf.endsWith(".jsonl")) {
768
- await processJsonl(path.join(usersDir, uf));
769
- }
770
- }
771
- await fs.remove(usersDir);
772
- }
773
- }
774
- } finally {
775
- if (await fs.pathExists(tempTarPath)) {
776
- await fs.remove(tempTarPath);
777
- }
778
- }
1021
+ await pullAgent(slug, agent.id, config);
779
1022
  }
1023
+
780
1024
  console.log("✅ Pull de agentes concluído com sucesso!");
781
1025
  } catch (error: any) {
782
1026
  if (error.response && error.response.status) {
@@ -808,61 +1052,7 @@ pullCmd
808
1052
  // PULL SKILLS INLINE
809
1053
  console.log('\n--- [1/2] SKILLS ---');
810
1054
  try {
811
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
812
- const response = await axios.get(url, {
813
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
814
- responseType: 'stream'
815
- });
816
-
817
- const tempTarPath = path.join(getWorkspaceRoot(), 'temp_skills.tar.gz');
818
- const writer = fs.createWriteStream(tempTarPath);
819
- response.data.pipe(writer);
820
-
821
- await new Promise((resolve, reject) => {
822
- writer.on('finish', resolve);
823
- writer.on('error', reject);
824
- });
825
-
826
- await tar.x({ file: tempTarPath, cwd: getWorkspaceRoot() });
827
- await fs.remove(tempTarPath);
828
-
829
- const skillsListRes = await axios.get(`${config.goclaw.api_url}/v1/skills`, {
830
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
831
- });
832
-
833
- const skills = skillsListRes.data.skills || [];
834
- for (const skill of skills) {
835
- try {
836
- const isSystem = skill.is_system === true;
837
- const targetFolder = isSystem ? path.join('system', skill.slug) : skill.slug;
838
-
839
- if (isSystem) {
840
- const originalPath = path.join(getWorkspaceRoot(), 'skills', skill.slug);
841
- const newPath = path.join(getWorkspaceRoot(), 'skills', targetFolder);
842
- if (await fs.pathExists(originalPath)) {
843
- await fs.ensureDir(path.dirname(newPath));
844
- await fs.move(originalPath, newPath, { overwrite: true });
845
- }
846
- }
847
-
848
- const filesRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
849
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
850
- });
851
-
852
- const files = filesRes.data.files || [];
853
- for (const file of files) {
854
- if (file.isDir) continue;
855
- const fileContentRes = await axios.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
856
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
857
- });
858
- const filePath = path.join(getWorkspaceRoot(), 'skills', targetFolder, file.path);
859
- await fs.ensureDir(path.dirname(filePath));
860
- await fs.writeFile(filePath, fileContentRes.data.content || '');
861
- }
862
- } catch (fileErr: any) {
863
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
864
- }
865
- }
1055
+ await pullAllSkills(config);
866
1056
  console.log('✅ Pull de skills concluído!');
867
1057
  } catch (error: any) {
868
1058
  console.error('❌ Erro durante o pull das skills:', error.message);
@@ -880,50 +1070,7 @@ pullCmd
880
1070
 
881
1071
  for (const agent of agents) {
882
1072
  const slug = agent.agent_key;
883
- console.log(`📦 Baixando agente: ${slug}...`);
884
-
885
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export`;
886
- const response = await axios.get(url, {
887
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
888
- responseType: 'stream'
889
- });
890
-
891
- const tempTarPath = path.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
892
-
893
- try {
894
- const writer = fs.createWriteStream(tempTarPath);
895
- response.data.pipe(writer);
896
-
897
- await new Promise((resolve, reject) => {
898
- writer.on('finish', resolve);
899
- writer.on('error', reject);
900
- });
901
-
902
- const agentPath = path.join(getWorkspaceRoot(), 'agents', slug);
903
- await fs.ensureDir(agentPath);
904
-
905
- await tar.x({
906
- file: tempTarPath,
907
- cwd: agentPath,
908
- strip: 0,
909
- filter: (path) => {
910
- return path === 'agent.json' || path.startsWith('context_files/');
911
- }
912
- });
913
-
914
- const contextDir = path.join(agentPath, 'context_files');
915
- if (await fs.pathExists(contextDir)) {
916
- const contextFiles = await fs.readdir(contextDir);
917
- for (const f of contextFiles) {
918
- await fs.move(path.join(contextDir, f), path.join(agentPath, f), { overwrite: true });
919
- }
920
- await fs.remove(contextDir);
921
- }
922
- } finally {
923
- if (await fs.pathExists(tempTarPath)) {
924
- await fs.remove(tempTarPath);
925
- }
926
- }
1073
+ await pullAgent(slug, agent.id, config);
927
1074
  }
928
1075
  console.log('✅ Pull de agentes concluído!');
929
1076
  } catch (error: any) {