@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/dist/index.js CHANGED
@@ -301,29 +301,20 @@ async function resolveAgentId(slug, config) {
301
301
  const deployCmd = program
302
302
  .command("deploy")
303
303
  .description("Faz o deploy de entidades para a plataforma GoClaw");
304
- deployCmd
305
- .command("skill <slug>")
306
- .description("Faz o build da skill e envia para a API do GoClaw")
307
- .action(async (slug) => {
308
- const config = await getConfig();
309
- if (!config.goclaw || !config.goclaw.token) {
310
- console.error("❌ Configure sua chave de API (token) no agentforge.json antes de fazer o deploy.");
311
- process.exit(1);
312
- }
313
- const basePath = getWorkspaceRoot();
304
+ async function deploySkill(slug, config, basePath) {
314
305
  const skillPath = path_1.default.join(basePath, "skills", slug);
315
306
  const exportsPath = path_1.default.join(basePath, "exports");
316
- const zipPath = path_1.default.join(exportsPath, `${slug}.zip`);
307
+ const safeSlug = slug.replace(/[\\\/]/g, '_');
308
+ const zipPath = path_1.default.join(exportsPath, `${safeSlug}.zip`);
317
309
  if (!(await fs_extra_1.default.pathExists(skillPath))) {
318
310
  console.error(`❌ A skill "${slug}" não foi encontrada em skills/${slug}.`);
319
- process.exit(1);
311
+ return;
320
312
  }
321
313
  await fs_extra_1.default.ensureDir(exportsPath);
322
314
  const zip = new adm_zip_1.default();
323
315
  zip.addLocalFolder(skillPath, "");
324
316
  zip.writeZip(zipPath);
325
- console.log(`✅ Build concluído: ${slug}.zip preparado para envio.`);
326
- console.log(`🚀 Fazendo upload para o GoClaw...`);
317
+ console.log(`🚀 Fazendo upload da skill "${slug}" para o GoClaw...`);
327
318
  const form = new form_data_1.default();
328
319
  form.append("file", fs_extra_1.default.createReadStream(zipPath));
329
320
  try {
@@ -336,11 +327,58 @@ deployCmd
336
327
  });
337
328
  const data = response.data;
338
329
  if (data && data.version) {
339
- console.log(`📌 Skill atualizada para a versão ${data.version}.`);
330
+ console.log(`✅ Arquivos da skill "${slug}" atualizados (versão ${data.version}).`);
331
+ }
332
+ else {
333
+ console.log(`✅ Arquivos da skill "${slug}" atualizados.`);
334
+ }
335
+ // Sincronizar metadados (visibility, description, tags, etc)
336
+ const skillsListRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills`, {
337
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
338
+ });
339
+ const remoteSkill = skillsListRes.data.skills?.find((s) => s.slug === slug);
340
+ if (remoteSkill) {
341
+ const metadataPath = path_1.default.join(skillPath, "metadata.json");
342
+ if (await fs_extra_1.default.pathExists(metadataPath)) {
343
+ console.log(`🚀 Sincronizando metadados da skill "${slug}"...`);
344
+ const metadata = await fs_extra_1.default.readJson(metadataPath);
345
+ // Remover campos que não devem ser enviados no PUT
346
+ const payload = { ...metadata };
347
+ delete payload.id;
348
+ delete payload.slug;
349
+ delete payload.name;
350
+ await axios_1.default.put(`${config.goclaw.api_url}/v1/skills/${remoteSkill.id}`, payload, {
351
+ headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
352
+ });
353
+ console.log(`✅ Metadados sincronizados com sucesso.`);
354
+ }
355
+ // Sincronizar permissões (grants)
356
+ const grantsPath = path_1.default.join(skillPath, "grants.jsonl");
357
+ if (await fs_extra_1.default.pathExists(grantsPath)) {
358
+ console.log(`🚀 Sincronizando permissões (grants) da skill "${slug}"...`);
359
+ const grantsContent = await fs_extra_1.default.readFile(grantsPath, 'utf8');
360
+ const lines = grantsContent.split('\n').filter(l => l.trim());
361
+ for (const line of lines) {
362
+ try {
363
+ const grant = JSON.parse(line);
364
+ if (grant.agent_key) {
365
+ const agentId = await resolveAgentId(grant.agent_key, config);
366
+ if (agentId) {
367
+ await axios_1.default.post(`${config.goclaw.api_url}/v1/skills/${remoteSkill.id}/grants/agent`, { agent_id: agentId, version: grant.pinned_version || null }, { headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" } });
368
+ console.log(` ➕ Permissão concedida ao agente: ${grant.agent_key}`);
369
+ }
370
+ }
371
+ }
372
+ catch (e) {
373
+ console.warn(` ⚠️ Falha ao conceder permissão: ${e.response?.data?.error || e.message}`);
374
+ }
375
+ }
376
+ console.log(`✅ Permissões sincronizadas.`);
377
+ }
340
378
  }
341
379
  }
342
380
  catch (error) {
343
- console.error("❌ Erro durante o deploy:");
381
+ console.error(`❌ Erro no deploy da skill "${slug}":`);
344
382
  if (error.response) {
345
383
  console.error(error.response.data);
346
384
  }
@@ -348,6 +386,18 @@ deployCmd
348
386
  console.error(error.message);
349
387
  }
350
388
  }
389
+ }
390
+ deployCmd
391
+ .command("skill <slug>")
392
+ .description("Faz build e upload automático de uma skill para o GoClaw")
393
+ .action(async (slug) => {
394
+ const config = await getConfig();
395
+ if (!config.goclaw || !config.goclaw.token) {
396
+ console.error("❌ Configure sua chave de API (token) no agentforge.json antes de fazer o deploy.");
397
+ process.exit(1);
398
+ }
399
+ const basePath = getWorkspaceRoot();
400
+ await deploySkill(slug, config, basePath);
351
401
  });
352
402
  async function deployContextFiles(slug, config, resolvedId) {
353
403
  const agentId = resolvedId || (await resolveAgentId(slug, config)) || slug;
@@ -357,53 +407,112 @@ async function deployContextFiles(slug, config, resolvedId) {
357
407
  throw new Error(`Agente não encontrado em agents/${slug}`);
358
408
  }
359
409
  const files = await fs_extra_1.default.readdir(agentPath);
360
- // Identificar ficheiros de contexto e de memória
361
- const memoryFileNames = ['MEMORY.md', 'memory.md'];
362
- const memoryDirName = 'memory';
363
- const contextFiles = files.filter(f => (f.endsWith('.md') || f.endsWith('.txt') || f.endsWith('.py')) &&
364
- f !== 'README.md' &&
365
- f !== 'agent.json' &&
366
- !memoryFileNames.includes(f));
367
- const hasMemoryDir = await fs_extra_1.default.pathExists(path_1.default.join(agentPath, memoryDirName));
368
- const memoryFilesFound = files.filter(f => memoryFileNames.includes(f));
369
- const hasMemory = hasMemoryDir || memoryFilesFound.length > 0;
370
- if (contextFiles.length === 0 && !hasMemory) {
410
+ const itemsToSync = files.filter(f => f !== 'agent.json' && f !== 'README.md');
411
+ if (itemsToSync.length === 0) {
371
412
  console.log(`Nenhum ficheiro de contexto ou memória encontrado para "${slug}".`);
372
413
  return;
373
414
  }
374
415
  const tempExportDir = path_1.default.join(basePath, `temp_export_${slug}`);
375
- const tempContextDir = path_1.default.join(tempExportDir, "context_files");
376
- const tempMemoryDir = path_1.default.join(tempExportDir, "memory");
377
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
+ }
378
437
  try {
379
- const sections = [];
380
- if (contextFiles.length > 0) {
381
- sections.push("context_files");
382
- await fs_extra_1.default.ensureDir(tempContextDir);
383
- for (const file of contextFiles) {
384
- 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));
385
463
  }
386
464
  }
387
- if (hasMemory) {
388
- sections.push("memory");
389
- await fs_extra_1.default.ensureDir(tempMemoryDir);
390
- // Copiar ficheiros de memória explícitos para a pasta memory no arquivo
391
- for (const file of memoryFilesFound) {
392
- 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/");
393
475
  }
394
- // Copiar conteúdo da pasta memory se existir
395
- if (hasMemoryDir) {
396
- await fs_extra_1.default.copy(path_1.default.join(agentPath, memoryDirName), tempMemoryDir);
476
+ else if (entry.startsWith("_system_")) {
477
+ finalEntry = entry.replace("_system_", "_system/");
397
478
  }
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
+ }
502
+ }
503
+ }
504
+ catch (e) {
505
+ console.warn("Aviso: Falha ao procurar fantasmas para o tarball.", e.message);
398
506
  }
399
507
  await tar.c({
400
508
  gzip: true,
401
509
  file: tarPath,
402
510
  cwd: tempExportDir
403
- }, sections);
511
+ }, sectionsArray);
404
512
  const form = new form_data_1.default();
405
513
  form.append("file", fs_extra_1.default.createReadStream(tarPath));
406
- 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(",")}`;
407
516
  await axios_1.default.post(url, form, {
408
517
  headers: {
409
518
  ...form.getHeaders(),
@@ -411,7 +520,73 @@ async function deployContextFiles(slug, config, resolvedId) {
411
520
  "X-GoClaw-User-Id": config.goclaw.username || "system"
412
521
  }
413
522
  });
414
- 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
+ }
415
590
  }
416
591
  finally {
417
592
  if (await fs_extra_1.default.pathExists(tempExportDir))
@@ -481,8 +656,44 @@ deployCmd
481
656
  }
482
657
  await deployAgent(slug, config);
483
658
  });
659
+ async function deployAllAgents(config, basePath) {
660
+ const agentsDir = path_1.default.join(basePath, "agents");
661
+ if (await fs_extra_1.default.pathExists(agentsDir)) {
662
+ const agents = await fs_extra_1.default.readdir(agentsDir);
663
+ console.log(`🚀 Iniciando deploy em lote de ${agents.length} agentes...`);
664
+ for (const slug of agents) {
665
+ const agentPath = path_1.default.join(agentsDir, slug);
666
+ if ((await fs_extra_1.default.stat(agentPath)).isDirectory()) {
667
+ await deployAgent(slug, config);
668
+ }
669
+ }
670
+ }
671
+ else {
672
+ console.log("Nenhum agente encontrado em agents/.");
673
+ }
674
+ }
675
+ async function deployAllSkills(config, basePath) {
676
+ const skillsDir = path_1.default.join(basePath, "skills");
677
+ if (await fs_extra_1.default.pathExists(skillsDir)) {
678
+ const skills = await fs_extra_1.default.readdir(skillsDir);
679
+ console.log(`🚀 Iniciando deploy em lote de skills...`);
680
+ for (const item of skills) {
681
+ const itemPath = path_1.default.join(skillsDir, item);
682
+ if ((await fs_extra_1.default.stat(itemPath)).isDirectory()) {
683
+ if (item === "system") {
684
+ console.log("⏩ Ignorando pasta 'system/' (skills nativas do GoClaw são apenas de leitura)");
685
+ continue;
686
+ }
687
+ await deploySkill(item, config, basePath);
688
+ }
689
+ }
690
+ }
691
+ else {
692
+ console.log("Nenhuma skill encontrada em skills/.");
693
+ }
694
+ }
484
695
  deployCmd
485
- .command("all")
696
+ .command("agents")
486
697
  .description("Faz deploy de todos os agentes do workspace")
487
698
  .action(async () => {
488
699
  const config = await getConfig();
@@ -491,24 +702,122 @@ deployCmd
491
702
  process.exit(1);
492
703
  }
493
704
  const basePath = getWorkspaceRoot();
494
- const agentsDir = path_1.default.join(basePath, "agents");
495
- if (!(await fs_extra_1.default.pathExists(agentsDir))) {
496
- console.log("Nenhum agente encontrado em agents/.");
497
- return;
705
+ await deployAllAgents(config, basePath);
706
+ console.log("🏁 Deploy de agentes concluído!");
707
+ });
708
+ deployCmd
709
+ .command("skills")
710
+ .description("Faz deploy de todas as skills do workspace")
711
+ .action(async () => {
712
+ const config = await getConfig();
713
+ if (!config.goclaw || !config.goclaw.token) {
714
+ console.error("❌ Configure sua chave de API (token) no agentforge.json.");
715
+ process.exit(1);
498
716
  }
499
- const agents = await fs_extra_1.default.readdir(agentsDir);
500
- console.log(`🚀 Iniciando deploy em lote de ${agents.length} agentes...`);
501
- for (const slug of agents) {
502
- const agentPath = path_1.default.join(agentsDir, slug);
503
- if ((await fs_extra_1.default.stat(agentPath)).isDirectory()) {
504
- await deployAgent(slug, config);
505
- }
717
+ const basePath = getWorkspaceRoot();
718
+ await deployAllSkills(config, basePath);
719
+ console.log("🏁 Deploy de skills concluído!");
720
+ });
721
+ deployCmd
722
+ .command("all")
723
+ .description("Faz deploy de todos os agentes e skills do workspace")
724
+ .action(async () => {
725
+ const config = await getConfig();
726
+ if (!config.goclaw || !config.goclaw.token) {
727
+ console.error("❌ Configure sua chave de API (token) no agentforge.json.");
728
+ process.exit(1);
506
729
  }
507
- console.log("🏁 Deploy em lote concluído!");
730
+ const basePath = getWorkspaceRoot();
731
+ await deployAllAgents(config, basePath);
732
+ await deployAllSkills(config, basePath);
733
+ console.log("🏁 Deploy completo (agentes e skills) concluído!");
508
734
  });
509
735
  const pullCmd = program
510
736
  .command("pull")
511
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
+ }
512
821
  pullCmd
513
822
  .command("skills")
514
823
  .description("Faz download do arquivo tar.gz de skills do GoClaw e extrai localmente")
@@ -522,100 +831,97 @@ pullCmd
522
831
  console.log("❌ Pull cancelado pelo utilizador.");
523
832
  return;
524
833
  }
525
- console.log("🧹 Limpando a pasta local de skills...");
526
- await fs_extra_1.default.emptyDir(path_1.default.join(getWorkspaceRoot(), "skills"));
527
- console.log("📥 Baixando skills do GoClaw...");
528
834
  try {
529
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
530
- const response = await axios_1.default.get(url, {
531
- headers: {
532
- Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system"
533
- },
534
- responseType: "stream"
535
- });
536
- 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 {
537
857
  const writer = fs_extra_1.default.createWriteStream(tempTarPath);
538
858
  response.data.pipe(writer);
539
859
  await new Promise((resolve, reject) => {
540
860
  writer.on("finish", resolve);
541
861
  writer.on("error", reject);
542
862
  });
543
- 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
+ });
544
881
  await tar.x({
545
882
  file: tempTarPath,
546
- cwd: getWorkspaceRoot()
547
- });
548
- await fs_extra_1.default.remove(tempTarPath);
549
- console.log("📥 Baixando ficheiros de código das skills...");
550
- const skillsListRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills`, {
551
- 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
+ }
552
888
  });
553
- const skills = skillsListRes.data.skills || [];
554
- for (const skill of skills) {
555
- try {
556
- const isSystem = skill.is_system === true;
557
- const targetFolder = isSystem ? path_1.default.join("system", skill.slug) : skill.slug;
558
- if (isSystem) {
559
- const originalPath = path_1.default.join(getWorkspaceRoot(), "skills", skill.slug);
560
- const newPath = path_1.default.join(getWorkspaceRoot(), "skills", targetFolder);
561
- if (await fs_extra_1.default.pathExists(originalPath)) {
562
- await fs_extra_1.default.ensureDir(path_1.default.dirname(newPath));
563
- 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;
564
901
  }
565
902
  }
566
- const filesRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
567
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
568
- });
569
- const files = filesRes.data.files || [];
570
- for (const file of files) {
571
- if (file.isDir)
572
- continue;
573
- const fileContentRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
574
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" }
575
- });
576
- const filePath = path_1.default.join(getWorkspaceRoot(), "skills", targetFolder, file.path);
577
- await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
578
- 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 });
579
907
  }
580
- }
581
- catch (fileErr) {
582
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
583
- }
584
- }
585
- // Remover quaisquer skills fantasmas que o tarball tenha extraído (skills apagadas mas ainda no export)
586
- const validSlugs = new Set(skills.map((s) => s.is_system === true ? path_1.default.join("system", s.slug) : s.slug));
587
- const skillsDir = path_1.default.join(getWorkspaceRoot(), "skills");
588
- if (await fs_extra_1.default.pathExists(skillsDir)) {
589
- const localItems = await fs_extra_1.default.readdir(skillsDir);
590
- for (const item of localItems) {
591
- if (item === "system") {
592
- const systemDir = path_1.default.join(skillsDir, "system");
593
- if (await fs_extra_1.default.pathExists(systemDir)) {
594
- const systemItems = await fs_extra_1.default.readdir(systemDir);
595
- for (const sysItem of systemItems) {
596
- if (!validSlugs.has(path_1.default.join("system", sysItem))) {
597
- await fs_extra_1.default.remove(path_1.default.join(systemDir, sysItem));
598
- }
599
- }
600
- }
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);
601
911
  }
602
- else if (!validSlugs.has(item)) {
603
- 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 });
604
914
  }
605
915
  }
916
+ await fs_extra_1.default.remove(contextDir);
606
917
  }
607
- console.log("✅ Pull concluído com sucesso! As skills foram atualizadas localmente.");
608
918
  }
609
- catch (error) {
610
- console.error("❌ Erro durante o pull das skills:");
611
- if (error.response) {
612
- console.error(`Status HTTP ${error.response.status}`);
613
- }
614
- else {
615
- console.error(error.message);
919
+ finally {
920
+ if (await fs_extra_1.default.pathExists(tempTarPath)) {
921
+ await fs_extra_1.default.remove(tempTarPath);
616
922
  }
617
923
  }
618
- });
924
+ }
619
925
  pullCmd
620
926
  .command("agents")
621
927
  .description("Faz download cirúrgico dos agentes (configuração e contexto)")
@@ -640,77 +946,7 @@ pullCmd
640
946
  console.log(`Encontrados ${agents.length} agentes. Sincronizando...`);
641
947
  for (const agent of agents) {
642
948
  const slug = agent.agent_key;
643
- console.log(`📦 Baixando agente: ${slug}...`);
644
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export?sections=config,context_files,memory`;
645
- const response = await axios_1.default.get(url, {
646
- headers: { Authorization: `Bearer ${config.goclaw.token}`, "X-GoClaw-User-Id": config.goclaw.username || "system" },
647
- responseType: "stream"
648
- });
649
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
650
- try {
651
- const writer = fs_extra_1.default.createWriteStream(tempTarPath);
652
- response.data.pipe(writer);
653
- await new Promise((resolve, reject) => {
654
- writer.on("finish", resolve);
655
- writer.on("error", reject);
656
- });
657
- const agentPath = path_1.default.join(getWorkspaceRoot(), "agents", slug);
658
- await fs_extra_1.default.ensureDir(agentPath);
659
- await tar.x({
660
- file: tempTarPath,
661
- cwd: agentPath,
662
- strip: 0,
663
- filter: (path) => {
664
- return path === 'agent.json' || path.startsWith('context_files/') || path.startsWith('memory/') || path === 'MEMORY.md' || path === 'memory.md';
665
- }
666
- });
667
- const contextDir = path_1.default.join(agentPath, "context_files");
668
- if (await fs_extra_1.default.pathExists(contextDir)) {
669
- const contextFiles = await fs_extra_1.default.readdir(contextDir);
670
- for (const f of contextFiles) {
671
- await fs_extra_1.default.move(path_1.default.join(contextDir, f), path_1.default.join(agentPath, f), { overwrite: true });
672
- }
673
- await fs_extra_1.default.remove(contextDir);
674
- }
675
- // Reconstruir ficheiros de memória a partir de JSONL
676
- const memoryDir = path_1.default.join(agentPath, "memory");
677
- if (await fs_extra_1.default.pathExists(memoryDir)) {
678
- const processJsonl = async (filePath) => {
679
- if (!(await fs_extra_1.default.pathExists(filePath)))
680
- return;
681
- const content = await fs_extra_1.default.readFile(filePath, 'utf8');
682
- const lines = content.split('\n').filter(l => l.trim());
683
- for (const line of lines) {
684
- try {
685
- const entry = JSON.parse(line);
686
- if (entry.path && entry.content) {
687
- const targetPath = path_1.default.join(agentPath, entry.path);
688
- await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath));
689
- await fs_extra_1.default.writeFile(targetPath, entry.content);
690
- }
691
- }
692
- catch (e) { }
693
- }
694
- await fs_extra_1.default.remove(filePath);
695
- };
696
- await processJsonl(path_1.default.join(memoryDir, "global.jsonl"));
697
- const usersDir = path_1.default.join(memoryDir, "users");
698
- if (await fs_extra_1.default.pathExists(usersDir)) {
699
- const userFiles = await fs_extra_1.default.readdir(usersDir);
700
- for (const uf of userFiles) {
701
- if (uf.endsWith(".jsonl")) {
702
- await processJsonl(path_1.default.join(usersDir, uf));
703
- }
704
- }
705
- await fs_extra_1.default.remove(usersDir);
706
- }
707
- }
708
- }
709
- finally {
710
- if (await fs_extra_1.default.pathExists(tempTarPath)) {
711
- await fs_extra_1.default.remove(tempTarPath);
712
- }
713
- }
949
+ await pullAgent(slug, agent.id, config);
714
950
  }
715
951
  console.log("✅ Pull de agentes concluído com sucesso!");
716
952
  }
@@ -740,55 +976,7 @@ pullCmd
740
976
  // PULL SKILLS INLINE
741
977
  console.log('\n--- [1/2] SKILLS ---');
742
978
  try {
743
- const url = `${config.goclaw.api_url}${config.goclaw.skills_export_endpoint || '/v1/skills/export'}`;
744
- const response = await axios_1.default.get(url, {
745
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
746
- responseType: 'stream'
747
- });
748
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), 'temp_skills.tar.gz');
749
- const writer = fs_extra_1.default.createWriteStream(tempTarPath);
750
- response.data.pipe(writer);
751
- await new Promise((resolve, reject) => {
752
- writer.on('finish', resolve);
753
- writer.on('error', reject);
754
- });
755
- await tar.x({ file: tempTarPath, cwd: getWorkspaceRoot() });
756
- await fs_extra_1.default.remove(tempTarPath);
757
- const skillsListRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills`, {
758
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
759
- });
760
- const skills = skillsListRes.data.skills || [];
761
- for (const skill of skills) {
762
- try {
763
- const isSystem = skill.is_system === true;
764
- const targetFolder = isSystem ? path_1.default.join('system', skill.slug) : skill.slug;
765
- if (isSystem) {
766
- const originalPath = path_1.default.join(getWorkspaceRoot(), 'skills', skill.slug);
767
- const newPath = path_1.default.join(getWorkspaceRoot(), 'skills', targetFolder);
768
- if (await fs_extra_1.default.pathExists(originalPath)) {
769
- await fs_extra_1.default.ensureDir(path_1.default.dirname(newPath));
770
- await fs_extra_1.default.move(originalPath, newPath, { overwrite: true });
771
- }
772
- }
773
- const filesRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files`, {
774
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
775
- });
776
- const files = filesRes.data.files || [];
777
- for (const file of files) {
778
- if (file.isDir)
779
- continue;
780
- const fileContentRes = await axios_1.default.get(`${config.goclaw.api_url}/v1/skills/${skill.id}/files/${file.path}`, {
781
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' }
782
- });
783
- const filePath = path_1.default.join(getWorkspaceRoot(), 'skills', targetFolder, file.path);
784
- await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
785
- await fs_extra_1.default.writeFile(filePath, fileContentRes.data.content || '');
786
- }
787
- }
788
- catch (fileErr) {
789
- console.warn(`⚠️ Não foi possível transferir os ficheiros da skill ${skill.slug}: ${fileErr.message}`);
790
- }
791
- }
979
+ await pullAllSkills(config);
792
980
  console.log('✅ Pull de skills concluído!');
793
981
  }
794
982
  catch (error) {
@@ -804,44 +992,7 @@ pullCmd
804
992
  console.log(`Encontrados ${agents.length} agentes. Sincronizando...`);
805
993
  for (const agent of agents) {
806
994
  const slug = agent.agent_key;
807
- console.log(`📦 Baixando agente: ${slug}...`);
808
- const url = `${config.goclaw.api_url}/v1/agents/${agent.id}/export`;
809
- const response = await axios_1.default.get(url, {
810
- headers: { Authorization: `Bearer ${config.goclaw.token}`, 'X-GoClaw-User-Id': config.goclaw.username || 'system' },
811
- responseType: 'stream'
812
- });
813
- const tempTarPath = path_1.default.join(getWorkspaceRoot(), `temp_agent_${slug}.tar.gz`);
814
- try {
815
- const writer = fs_extra_1.default.createWriteStream(tempTarPath);
816
- response.data.pipe(writer);
817
- await new Promise((resolve, reject) => {
818
- writer.on('finish', resolve);
819
- writer.on('error', reject);
820
- });
821
- const agentPath = path_1.default.join(getWorkspaceRoot(), 'agents', slug);
822
- await fs_extra_1.default.ensureDir(agentPath);
823
- await tar.x({
824
- file: tempTarPath,
825
- cwd: agentPath,
826
- strip: 0,
827
- filter: (path) => {
828
- return path === 'agent.json' || path.startsWith('context_files/');
829
- }
830
- });
831
- const contextDir = path_1.default.join(agentPath, 'context_files');
832
- if (await fs_extra_1.default.pathExists(contextDir)) {
833
- const contextFiles = await fs_extra_1.default.readdir(contextDir);
834
- for (const f of contextFiles) {
835
- await fs_extra_1.default.move(path_1.default.join(contextDir, f), path_1.default.join(agentPath, f), { overwrite: true });
836
- }
837
- await fs_extra_1.default.remove(contextDir);
838
- }
839
- }
840
- finally {
841
- if (await fs_extra_1.default.pathExists(tempTarPath)) {
842
- await fs_extra_1.default.remove(tempTarPath);
843
- }
844
- }
995
+ await pullAgent(slug, agent.id, config);
845
996
  }
846
997
  console.log('✅ Pull de agentes concluído!');
847
998
  }