@nahisaho/satori 0.28.0 → 0.28.1

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.
Files changed (2) hide show
  1. package/bin/satori.js +314 -13
  2. package/package.json +1 -1
package/bin/satori.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('node:fs');
4
+ const os = require('node:os');
4
5
  const path = require('node:path');
5
6
 
6
7
  const COMMAND = process.argv[2];
@@ -9,6 +10,7 @@ const FLAGS = process.argv.slice(3);
9
10
 
10
11
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
11
12
  const SOURCE_DIR = path.join(PACKAGE_ROOT, 'src', '.github');
13
+ const CUSTOM_PIPELINES_PATH = path.join(process.env.HOME || os.homedir(), '.satori', 'custom-pipelines.json');
12
14
 
13
15
  function copyDirSync(src, dest) {
14
16
  fs.mkdirSync(dest, { recursive: true });
@@ -77,6 +79,7 @@ Usage:
77
79
  satori pipeline suggest Interactive pipeline recommendation
78
80
  satori pipeline list List all available pipelines
79
81
  satori pipeline custom <action> Manage custom pipelines
82
+ satori docs generate [--preview] Generate docs/ and docs/qiita/ files
80
83
  satori validate [--verbose] Validate all SKILL.md files
81
84
  satori stats Show skill/TU coverage statistics
82
85
  satori help Show this help message
@@ -86,6 +89,7 @@ Options:
86
89
  --force Overwrite existing .github/ directory
87
90
  --dry-run Preview what would be installed without making changes
88
91
  --verbose Show detailed validation output
92
+ --preview Show docs generation summary without writing files
89
93
 
90
94
  Custom Pipelines:
91
95
  satori pipeline custom list List custom pipelines
@@ -480,6 +484,54 @@ const PIPELINES = [
480
484
  },
481
485
  ];
482
486
 
487
+ // ── Synonym Dictionary ──
488
+ const SYNONYM_DICT = {
489
+ // 機械学習・AI
490
+ ml: ['machine learning', '機械学習', 'ML'],
491
+ ai: ['artificial intelligence', '人工知能', 'AI', 'AI'],
492
+ dl: ['deep learning', '深層学習', 'DL'],
493
+ 'neural network': ['NN', 'ニューラルネットワーク'],
494
+
495
+ // バイオインフォマティクス
496
+ bioinfo: ['バイオインフォマティクス', 'bioinformatics'],
497
+ genomics: ['ゲノミクス', 'ゲノム', 'genomics'],
498
+ seq: ['シーケンシング', 'sequencing'],
499
+ rna: ['RNA', 'RNA-seq', 'トランスクリプトーム'],
500
+ protein: ['プロテイン', 'タンパク質'],
501
+
502
+ // 創薬・化学
503
+ 'drug discovery': ['創薬', '創薬', 'drug-discovery'],
504
+ admet: ['ADMET', '薬物動態'],
505
+ docking: ['ドッキング', 'molecular docking'],
506
+ cheminformatics: ['ケモインフォマティクス'],
507
+
508
+ // データ分析
509
+ 'data analysis': ['データ解析', 'data analysis'],
510
+ statistics: ['統計', '統計学'],
511
+ visualization: ['可視化', 'ビジュアル'],
512
+ pipeline: ['パイプライン'],
513
+
514
+ // 医療・臨床
515
+ clinical: ['臨床', 'クリニカル'],
516
+ precision: ['精密医療', '精密'],
517
+ oncology: ['腫瘍学', 'がん'],
518
+ };
519
+
520
+ function normalizeKeyword(keyword) {
521
+ const lower = keyword.toLowerCase().trim();
522
+
523
+ // 同義語チェック
524
+ for (const [key, synonyms] of Object.entries(SYNONYM_DICT)) {
525
+ for (const syn of synonyms) {
526
+ if (lower.includes(syn.toLowerCase()) || syn.toLowerCase().includes(lower)) {
527
+ return key;
528
+ }
529
+ }
530
+ }
531
+
532
+ return lower;
533
+ }
534
+
483
535
  function pipelineSuggest() {
484
536
  const readline = require('node:readline');
485
537
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -493,15 +545,20 @@ function pipelineSuggest() {
493
545
 
494
546
  const input = await ask('何を解析しますか? キーワードや研究テーマを入力してください:\n> ');
495
547
  const query = input.toLowerCase();
548
+ const normalizedQuery = normalizeKeyword(query);
496
549
 
497
- // Score each pipeline by keyword match
550
+ // Score each pipeline by keyword match (with synonym support)
498
551
  const scored = PIPELINES.map((p) => {
499
552
  let score = 0;
500
553
  for (const kw of p.keywords) {
554
+ const normalizedKw = normalizeKeyword(kw);
555
+ // 完全一致:2点、同義語マッチ:1.5点、部分一致:1点
501
556
  if (query.includes(kw.toLowerCase())) score += 2;
557
+ else if (normalizedQuery === normalizedKw) score += 1.5;
558
+ else if (normalizedKw.includes(normalizedQuery) || query.includes(normalizedKw)) score += 1;
502
559
  }
503
560
  // Partial match on name
504
- if (query.includes(p.name.toLowerCase()) || p.name.toLowerCase().includes(query)) score += 1;
561
+ if (query.includes(p.name.toLowerCase()) || p.name.toLowerCase().includes(query)) score += 0.5;
505
562
  return { ...p, score };
506
563
  })
507
564
  .filter((p) => p.score > 0)
@@ -663,7 +720,7 @@ function validate() {
663
720
 
664
721
  // ── Stats ──
665
722
 
666
- function stats() {
723
+ function collectStats() {
667
724
  const skillsDir = path.join(SOURCE_DIR, 'skills');
668
725
 
669
726
  if (!fs.existsSync(skillsDir)) {
@@ -697,21 +754,142 @@ function stats() {
697
754
  }
698
755
  }
699
756
 
700
- const coverage = ((tuLinked / totalSkills) * 100).toFixed(1);
757
+ const pipelineBreakdown = {
758
+ domain: PIPELINES.filter((p) => typeof p.id === 'number').length,
759
+ cross: PIPELINES.filter((p) => p.domain === 'cross-domain').length,
760
+ industry: PIPELINES.filter((p) => p.domain === 'industry').length,
761
+ methodology: PIPELINES.filter((p) => p.domain === 'methodology').length,
762
+ };
763
+
701
764
  const pkg = require(path.join(PACKAGE_ROOT, 'package.json'));
765
+ const date = new Date().toISOString().slice(0, 10);
766
+ const coverage = ((tuLinked / totalSkills) * 100).toFixed(1);
767
+
768
+ return {
769
+ version: pkg.version,
770
+ date,
771
+ totalSkills,
772
+ pipelinesCount: PIPELINES.length,
773
+ pipelineBreakdown,
774
+ tuLinked,
775
+ tuKeysCount: allTuKeys.size,
776
+ totalCodeBlocks,
777
+ coverage,
778
+ };
779
+ }
780
+
781
+ function stats() {
782
+ const summary = collectStats();
702
783
 
703
784
  console.log(`
704
- 📊 SATORI v${pkg.version} — 統計
705
-
706
- スキル総数: ${totalSkills}
707
- パイプライン数: ${PIPELINES.length}
708
- TU 連携スキル: ${tuLinked} (${coverage}%)
709
- TU 未連携: ${totalSkills - tuLinked}
710
- ユニーク TU キー: ${allTuKeys.size}
711
- コードブロック総数: ${totalCodeBlocks}
785
+ 📊 SATORI v${summary.version} — 統計
786
+
787
+ スキル総数: ${summary.totalSkills}
788
+ パイプライン数: ${summary.pipelinesCount}
789
+ TU 連携スキル: ${summary.tuLinked} (${summary.coverage}%)
790
+ TU 未連携: ${summary.totalSkills - summary.tuLinked}
791
+ ユニーク TU キー: ${summary.tuKeysCount}
792
+ コードブロック総数: ${summary.totalCodeBlocks}
712
793
  `);
713
794
  }
714
795
 
796
+ // ── Docs Generate ──
797
+
798
+ function updateReverseIndexDoc(content, summary) {
799
+ const pipelineLine = `| パイプライン数 | **${summary.pipelinesCount}** (ドメイン ${summary.pipelineBreakdown.domain} + クロスドメイン ${summary.pipelineBreakdown.cross} + インダストリー ${summary.pipelineBreakdown.industry} + メソドロジー ${summary.pipelineBreakdown.methodology}) |`;
800
+
801
+ return content
802
+ .replace(/\n\| SATORI バージョン \| \*\*v[^*]+\*\* \|/g, `\n| SATORI バージョン | **v${summary.version}** |`)
803
+ .replace(/\n\| 生成日 \| .* \|/g, `\n| 生成日 | ${summary.date} |`)
804
+ .replace(/\n\| スキル数 \| \*\*\d+\*\* \|/g, `\n| スキル数 | **${summary.totalSkills}** |`)
805
+ .replace(/\n\| パイプライン数 \| .* \|/g, `\n${pipelineLine}`)
806
+ .replace(
807
+ /\n\| ToolUniverse 連携スキル数 \| \*\*\d+\*\* \|/g,
808
+ `\n| ToolUniverse 連携スキル数 | **${summary.tuLinked}** |`,
809
+ )
810
+ .replace(
811
+ /\n\| ToolUniverse キー数(ユニーク) \| \*\*\d+\*\* \|/g,
812
+ `\n| ToolUniverse キー数(ユニーク) | **${summary.tuKeysCount}** |`,
813
+ );
814
+ }
815
+
816
+ function updatePipelineExamplesDoc(content, summary) {
817
+ const pipelineLine = `| 掲載パイプライン数 | ${summary.pipelineBreakdown.domain} ドメイン + ${summary.pipelineBreakdown.cross} クロスドメイン + ${summary.pipelineBreakdown.industry} インダストリー + ${summary.pipelineBreakdown.methodology} メソドロジー = **${summary.pipelinesCount}** |`;
818
+
819
+ return content
820
+ .replace(
821
+ /^> \*\*SATORI v[^*]+\*\* — .*$/m,
822
+ `> **SATORI v${summary.version}** — ${summary.totalSkills} スキル + ${summary.pipelinesCount} パイプラインの連携レシピ集`,
823
+ )
824
+ .replace(/\n\| 生成日 \| .* \|/g, `\n| 生成日 | ${summary.date} |`)
825
+ .replace(/\n\| 対象バージョン \| .* \|/g, `\n| 対象バージョン | v${summary.version} |`)
826
+ .replace(/\n\| 掲載パイプライン数 \| .* \|/g, `\n${pipelineLine}`)
827
+ .replace(
828
+ /\n\| スキル総数 \| \d+ \(.*\) \|/g,
829
+ `\n| スキル総数 | ${summary.totalSkills} (\`src/.github/skills/scientific-*/SKILL.md\`) |`,
830
+ )
831
+ .replace(/\n\| ToolUniverse キー数 \| .* \|/g, `\n| ToolUniverse キー数 | ${summary.tuKeysCount} (ユニーク) |`);
832
+ }
833
+
834
+ function updateQiitaReverseIndexDoc(content, summary) {
835
+ const title = `title: 【SATORI v${summary.version}】${summary.totalSkills}スキル×${summary.pipelinesCount}パイプライン逆引き辞書 完全索引`;
836
+ const intro = `**[SATORI](https://github.com/nahisaho/satori)** は **GitHub Copilot** 上で動作する、${summary.totalSkills} の専門スキルと ${summary.pipelinesCount} の統合パイプライン(${summary.pipelineBreakdown.domain} ドメイン + ${summary.pipelineBreakdown.cross} クロスドメイン + ${summary.pipelineBreakdown.industry} インダストリー + ${summary.pipelineBreakdown.methodology} メソドロジー)により、仮説構築から論文出版まで、あらゆる科学研究ワークフローを自動化するフレームワークです。`;
837
+
838
+ return content.replace(/^title: .+$/m, title).replace(/^\*\*\[SATORI\][\s\S]*?フレームワークです。$/m, intro);
839
+ }
840
+
841
+ function updateQiitaPipelineExamplesDoc(content, summary) {
842
+ const title = `title: 【SATORI v${summary.version}】${summary.totalSkills}スキル×${summary.pipelinesCount}パイプラインで実現する科学研究自動化 完全ガイド`;
843
+ const intro = `**[SATORI](https://github.com/nahisaho/satori)** は **GitHub Copilot** 上で動作する、${summary.totalSkills} の専門スキルを組み合わせて構築した ${summary.pipelinesCount} 個のパイプライン(${summary.pipelineBreakdown.domain} ドメイン + ${summary.pipelineBreakdown.cross} クロスドメイン + ${summary.pipelineBreakdown.industry} インダストリー + ${summary.pipelineBreakdown.methodology} メソドロジー)により、仮説構築から論文出版まで、あらゆる科学研究ワークフローを自動化するフレームワークです。`;
844
+
845
+ return content.replace(/^title: .+$/m, title).replace(/^\*\*\[SATORI\][\s\S]*?フレームワークです。$/m, intro);
846
+ }
847
+
848
+ function applyDocUpdate(filePath, updater, summary, preview) {
849
+ if (!fs.existsSync(filePath)) {
850
+ console.error(`Error: ドキュメントが見つかりません: ${filePath}`);
851
+ return false;
852
+ }
853
+ const content = fs.readFileSync(filePath, 'utf-8');
854
+ const updated = updater(content, summary);
855
+ if (updated !== content && !preview) {
856
+ fs.writeFileSync(filePath, updated);
857
+ }
858
+ return updated !== content;
859
+ }
860
+
861
+ function docsGenerate() {
862
+ const summary = collectStats();
863
+ const docsFlags = process.argv.slice(4);
864
+ const preview = docsFlags.includes('--preview');
865
+
866
+ const targets = [
867
+ { path: path.join(PACKAGE_ROOT, 'docs', 'SATORI_REVERSE_INDEX.md'), update: updateReverseIndexDoc },
868
+ { path: path.join(PACKAGE_ROOT, 'docs', 'SATORI_PIPELINE_EXAMPLES.md'), update: updatePipelineExamplesDoc },
869
+ {
870
+ path: path.join(PACKAGE_ROOT, 'docs', 'qiita', 'SATORI_REVERSE_INDEX_QIITA.md'),
871
+ update: updateQiitaReverseIndexDoc,
872
+ },
873
+ {
874
+ path: path.join(PACKAGE_ROOT, 'docs', 'qiita', 'SATORI_PIPELINE_EXAMPLES_QIITA.md'),
875
+ update: updateQiitaPipelineExamplesDoc,
876
+ },
877
+ ];
878
+
879
+ let updatedCount = 0;
880
+ for (const target of targets) {
881
+ if (applyDocUpdate(target.path, target.update, summary, preview)) {
882
+ updatedCount++;
883
+ }
884
+ }
885
+
886
+ if (preview) {
887
+ console.log(`✔ docs generate (preview) 完了: ${updatedCount} 件更新予定`);
888
+ } else {
889
+ console.log(`✔ docs generate 完了: ${updatedCount} 件更新`);
890
+ }
891
+ }
892
+
715
893
  // ── Skill Search / Info ──
716
894
 
717
895
  function loadAllSkills() {
@@ -962,6 +1140,118 @@ function skillRecommend() {
962
1140
  }
963
1141
  }
964
1142
 
1143
+ // ── Custom Pipeline Management ──
1144
+
1145
+ function loadCustomPipelines() {
1146
+ if (!fs.existsSync(CUSTOM_PIPELINES_PATH)) {
1147
+ return [];
1148
+ }
1149
+ try {
1150
+ const content = fs.readFileSync(CUSTOM_PIPELINES_PATH, 'utf-8');
1151
+ const data = JSON.parse(content);
1152
+ return data.customPipelines || [];
1153
+ } catch (err) {
1154
+ console.error('Warning: Failed to load custom pipelines:', err.message);
1155
+ return [];
1156
+ }
1157
+ }
1158
+
1159
+ function saveCustomPipelines(pipelines) {
1160
+ const dir = path.dirname(CUSTOM_PIPELINES_PATH);
1161
+ fs.mkdirSync(dir, { recursive: true });
1162
+ fs.writeFileSync(CUSTOM_PIPELINES_PATH, JSON.stringify({ customPipelines: pipelines }, null, 2));
1163
+ }
1164
+
1165
+ function pipelineCustom() {
1166
+ const action = process.argv[4];
1167
+ const customPipelines = loadCustomPipelines();
1168
+
1169
+ if (action === 'list') {
1170
+ listCustomPipelines(customPipelines);
1171
+ } else if (action === 'add') {
1172
+ addCustomPipeline(customPipelines);
1173
+ } else if (action === 'remove') {
1174
+ removeCustomPipeline(customPipelines);
1175
+ } else {
1176
+ console.error(`Unknown custom pipeline action: ${action || '(none)'}`);
1177
+ console.log('Usage: satori pipeline custom list | add <file> | remove <id>');
1178
+ process.exit(1);
1179
+ }
1180
+ }
1181
+
1182
+ function listCustomPipelines(pipelines) {
1183
+ if (pipelines.length === 0) {
1184
+ console.log('\n📋 カスタムパイプライン: 0 件\n');
1185
+ console.log('新しいカスタムパイプラインを追加するには:');
1186
+ console.log(' satori pipeline custom add <file>');
1187
+ return;
1188
+ }
1189
+
1190
+ console.log(`\n📋 カスタムパイプライン一覧 (${pipelines.length} 件)\n`);
1191
+ for (const p of pipelines) {
1192
+ console.log(` 🔧 [${p.id}] ${p.name}`);
1193
+ console.log(` スキル連鎖: ${p.skills}`);
1194
+ console.log('');
1195
+ }
1196
+ }
1197
+
1198
+ function addCustomPipeline(pipelines) {
1199
+ const filePath = process.argv[5];
1200
+ if (!filePath) {
1201
+ console.error('Error: ファイルパスを指定してください。');
1202
+ console.log('Usage: satori pipeline custom add <file>');
1203
+ process.exit(1);
1204
+ }
1205
+
1206
+ if (!fs.existsSync(filePath)) {
1207
+ console.error(`Error: ファイルが見つかりません: ${filePath}`);
1208
+ process.exit(1);
1209
+ }
1210
+
1211
+ try {
1212
+ const content = fs.readFileSync(filePath, 'utf-8');
1213
+ const pipelineData = JSON.parse(content);
1214
+
1215
+ // バリデーション
1216
+ if (!pipelineData.id || !pipelineData.name || !pipelineData.skills) {
1217
+ console.error('Error: パイプラインは id, name, skills を含む必要があります。');
1218
+ process.exit(1);
1219
+ }
1220
+
1221
+ // 重複チェック
1222
+ if (pipelines.some((p) => p.id === pipelineData.id)) {
1223
+ console.error(`Error: ID "${pipelineData.id}" は既に存在します。`);
1224
+ process.exit(1);
1225
+ }
1226
+
1227
+ pipelines.push(pipelineData);
1228
+ saveCustomPipelines(pipelines);
1229
+ console.log(`✔ カスタムパイプライン "${pipelineData.name}" を追加しました。`);
1230
+ } catch (err) {
1231
+ console.error('Error: パイプラインファイルの解析に失敗しました:', err.message);
1232
+ process.exit(1);
1233
+ }
1234
+ }
1235
+
1236
+ function removeCustomPipeline(pipelines) {
1237
+ const id = process.argv[5];
1238
+ if (!id) {
1239
+ console.error('Error: パイプライン ID を指定してください。');
1240
+ console.log('Usage: satori pipeline custom remove <id>');
1241
+ process.exit(1);
1242
+ }
1243
+
1244
+ const index = pipelines.findIndex((p) => p.id === id);
1245
+ if (index === -1) {
1246
+ console.error(`Error: パイプライン "${id}" が見つかりません。`);
1247
+ process.exit(1);
1248
+ }
1249
+
1250
+ const removed = pipelines.splice(index, 1)[0];
1251
+ saveCustomPipelines(pipelines);
1252
+ console.log(`✔ カスタムパイプライン "${removed.name}" を削除しました。`);
1253
+ }
1254
+
965
1255
  switch (COMMAND) {
966
1256
  case 'init':
967
1257
  init();
@@ -984,9 +1274,20 @@ switch (COMMAND) {
984
1274
  pipelineSuggest();
985
1275
  } else if (SUBCOMMAND === 'list') {
986
1276
  pipelineList();
1277
+ } else if (SUBCOMMAND === 'custom') {
1278
+ pipelineCustom();
987
1279
  } else {
988
1280
  console.error(`Unknown pipeline subcommand: ${SUBCOMMAND || '(none)'}`);
989
- console.log('Usage: satori pipeline suggest | satori pipeline list');
1281
+ console.log('Usage: satori pipeline suggest | satori pipeline list | satori pipeline custom list|add|remove');
1282
+ process.exit(1);
1283
+ }
1284
+ break;
1285
+ case 'docs':
1286
+ if (SUBCOMMAND === 'generate') {
1287
+ docsGenerate();
1288
+ } else {
1289
+ console.error(`Unknown docs subcommand: ${SUBCOMMAND || '(none)'}`);
1290
+ console.log('Usage: satori docs generate [--preview]');
990
1291
  process.exit(1);
991
1292
  }
992
1293
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nahisaho/satori",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "description": "SATORI — Agent Skills for Science. GitHub Copilot Agent Skills collection for scientific data analysis.",
5
5
  "main": "index.js",
6
6
  "bin": {