jkpark 2.3.1 → 2.3.3

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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # jkpark
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run src/index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/dist/index.js CHANGED
@@ -22714,7 +22714,7 @@ var {
22714
22714
  } = import__.default;
22715
22715
 
22716
22716
  // src/index.ts
22717
- import path5 from "path";
22717
+ import path6 from "path";
22718
22718
  import { fileURLToPath } from "url";
22719
22719
 
22720
22720
  // node_modules/@inquirer/core/dist/lib/key.js
@@ -25687,99 +25687,58 @@ var dist_default14 = inquirer;
25687
25687
  // src/commands/install.ts
25688
25688
  var import_fs_extra = __toESM(require_lib4(), 1);
25689
25689
  import path4 from "path";
25690
- import fs3 from "fs";
25690
+ import fs2 from "fs";
25691
25691
 
25692
25692
  // src/core/path-manager.ts
25693
25693
  import path2 from "path";
25694
25694
  import os2 from "os";
25695
- import fs from "fs";
25696
25695
 
25697
25696
  class PathManager {
25698
- static getOpenClawRoot() {
25699
- return path2.join(os2.homedir(), ".openclaw");
25697
+ static getOpenClawWorkspaceRoot() {
25698
+ return path2.join(os2.homedir(), ".openclaw", "workspace", "skills");
25700
25699
  }
25701
- static getClaudeRoot() {
25702
- return path2.join(os2.homedir(), ".claude");
25700
+ static getAntigravityRoot(cwd) {
25701
+ return path2.join(cwd, ".agent", "skills");
25703
25702
  }
25704
- static getGitHubRoot() {
25705
- return path2.join(os2.homedir(), ".config", "gh");
25706
- }
25707
- static getWorkspaces(root) {
25708
- if (!fs.existsSync(root))
25709
- return [];
25710
- let entries;
25711
- try {
25712
- entries = fs.readdirSync(root);
25713
- } catch (e) {
25714
- return [];
25715
- }
25716
- return entries.filter((f) => {
25717
- if (f.startsWith("."))
25718
- return false;
25719
- const fullPath = path2.join(root, f);
25720
- try {
25721
- return fs.statSync(fullPath).isDirectory();
25722
- } catch {
25723
- return false;
25724
- }
25725
- });
25703
+ static getJkparkSkillsRoot() {
25704
+ return path2.join(os2.homedir(), ".jkpark", "skills");
25726
25705
  }
25727
25706
  static resolveFinalPath(baseDir, relativeOrAbsolute) {
25728
25707
  return path2.isAbsolute(relativeOrAbsolute) ? relativeOrAbsolute : path2.resolve(baseDir, relativeOrAbsolute);
25729
25708
  }
25730
25709
  }
25731
25710
 
25732
- // src/core/plugin-manager.ts
25733
- import fs2 from "fs";
25711
+ // src/core/skill-manager.ts
25712
+ import fs from "fs";
25734
25713
  import path3 from "path";
25735
25714
 
25736
- class PluginManager {
25737
- pluginsDir;
25715
+ class SkillManager {
25716
+ skillsDir;
25738
25717
  constructor(baseDir) {
25739
- this.pluginsDir = path3.join(baseDir, "plugins");
25718
+ this.skillsDir = path3.join(baseDir, "skills");
25740
25719
  }
25741
- async getCategories() {
25742
- if (!fs2.existsSync(this.pluginsDir))
25720
+ async getAllSkills() {
25721
+ if (!fs.existsSync(this.skillsDir))
25743
25722
  return [];
25744
- const dirs = fs2.readdirSync(this.pluginsDir).filter((f) => fs2.statSync(path3.join(this.pluginsDir, f)).isDirectory());
25745
- return dirs.map((dir) => {
25746
- const pluginJsonPath = path3.join(this.pluginsDir, dir, "plugin.json");
25747
- let config = { name: dir, description: "No description provided" };
25748
- if (fs2.existsSync(pluginJsonPath)) {
25749
- try {
25750
- config = { ...config, ...JSON.parse(fs2.readFileSync(pluginJsonPath, "utf8")) };
25751
- } catch (e) {
25752
- console.warn(`Failed to parse plugin config at ${pluginJsonPath}:`, e);
25753
- }
25754
- }
25755
- return { ...config, value: dir };
25756
- });
25757
- }
25758
- async getSkills(category) {
25759
- const skillsDir = path3.join(this.pluginsDir, category, "skills");
25760
- if (!fs2.existsSync(skillsDir))
25761
- return [];
25762
- const skills = fs2.readdirSync(skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
25723
+ const skills = fs.readdirSync(this.skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
25763
25724
  return skills.map((skill) => {
25764
- const skillPath = path3.join(skillsDir, skill, "SKILL.md");
25725
+ const skillPath = path3.join(this.skillsDir, skill, "SKILL.md");
25765
25726
  let description = "No description provided";
25766
- if (fs2.existsSync(skillPath)) {
25767
- const content = fs2.readFileSync(skillPath, "utf8");
25727
+ if (fs.existsSync(skillPath)) {
25728
+ const content = fs.readFileSync(skillPath, "utf8");
25768
25729
  const match = content.match(/^description:\s*(.*)$/m);
25769
25730
  if (match && match[1]) {
25770
- description = match[1].trim();
25731
+ description = match[1].replace(/^["']|["']$/g, "").trim();
25771
25732
  }
25772
25733
  }
25773
25734
  return {
25774
25735
  name: skill,
25775
25736
  description,
25776
- value: skill
25737
+ value: skill,
25738
+ sourcePath: path3.join(this.skillsDir, skill)
25777
25739
  };
25778
25740
  });
25779
25741
  }
25780
- getSkillSourcePath(category, skill) {
25781
- return path3.join(this.pluginsDir, category, "skills", skill);
25782
- }
25783
25742
  }
25784
25743
 
25785
25744
  // src/commands/install.ts
@@ -25787,39 +25746,66 @@ async function runInstallWizard(projectRoot) {
25787
25746
  console.log(`
25788
25747
  \uD83D\uDC3E jkpark 설치 마법사에 오신 걸 환영합니다!
25789
25748
  `);
25790
- const pluginManager = new PluginManager(projectRoot);
25791
- const categoryChoices = await pluginManager.getCategories();
25792
- if (categoryChoices.length === 0) {
25793
- console.log("❌ 설치 가능한 플러그인이 없습니다. plugins 폴더를 확인해 주세요.");
25794
- return;
25795
- }
25749
+ const cwd = process.cwd();
25796
25750
  const { targetType } = await dist_default14.prompt([
25797
25751
  {
25798
- type: "list",
25752
+ type: "select",
25799
25753
  name: "targetType",
25800
25754
  message: "설치할 서비스(Target)를 선택하세요:",
25801
25755
  choices: [
25802
- { name: "\uD83C\uDFD7️ OpenClaw".padEnd(15) + " - OpenClaw Agents & Shared Skills", value: "openclaw" },
25803
- { name: "\uD83E\uDD16 Claude".padEnd(15) + " - Claude Code CLI & Global Skills", value: "claude" },
25804
- { name: "\uD83D\uDC19 GitHub".padEnd(15) + " - GitHub CLI Extensions (gh-extension)", value: "github" }
25756
+ { name: "openclaw (workspace)", value: "openclaw" },
25757
+ { name: "antigravity (workspace)", value: "antigravity" },
25758
+ { name: "custom path", value: "custom" }
25805
25759
  ]
25806
25760
  }
25807
25761
  ]);
25808
- const { selectedCategory } = await dist_default14.prompt([
25762
+ let targetPath = "";
25763
+ if (targetType === "openclaw") {
25764
+ targetPath = PathManager.getOpenClawWorkspaceRoot();
25765
+ } else if (targetType === "antigravity") {
25766
+ targetPath = PathManager.getAntigravityRoot(cwd);
25767
+ } else {
25768
+ const { customPath } = await dist_default14.prompt([
25769
+ {
25770
+ type: "input",
25771
+ name: "customPath",
25772
+ message: "설치 경로를 입력하세요:",
25773
+ validate: (input) => input.trim() !== "" ? true : "경로를 입력해야 합니다."
25774
+ }
25775
+ ]);
25776
+ targetPath = PathManager.resolveFinalPath(cwd, customPath);
25777
+ }
25778
+ console.log(`
25779
+ \uD83D\uDCCD 설정된 Target Path: ${targetPath}`);
25780
+ const { pathConfirm } = await dist_default14.prompt([
25809
25781
  {
25810
- type: "list",
25811
- name: "selectedCategory",
25812
- message: "설치할 플러그인 카테고리를 선택하세요:",
25813
- choices: categoryChoices.map((c) => ({
25814
- name: `${c.name.padEnd(15)} - ${c.description}`,
25815
- value: c.value
25816
- }))
25782
+ type: "confirm",
25783
+ name: "pathConfirm",
25784
+ message: " 경로에 설치하시겠습니까?",
25785
+ default: true
25817
25786
  }
25818
25787
  ]);
25819
- const skills = await pluginManager.getSkills(selectedCategory);
25820
- if (skills.length === 0) {
25788
+ if (!pathConfirm) {
25821
25789
  console.log(`
25822
- ⚠️ ${selectedCategory} 카테고리에 설치 가능한 스킬이 없습니다.`);
25790
+ 설치가 취소되었습니다.`);
25791
+ return;
25792
+ }
25793
+ const { installOption } = await dist_default14.prompt([
25794
+ {
25795
+ type: "select",
25796
+ name: "installOption",
25797
+ message: "설치 옵션을 선택하세요:",
25798
+ choices: [
25799
+ { name: "Option 1: 직접 설치 (타겟 폴더에 직접 복사)", value: "direct" },
25800
+ { name: "Option 2: 심볼릭 링크로 설치 (~/.jkpark/skills 에 설치 후 링크 생성)", value: "symlink" }
25801
+ ]
25802
+ }
25803
+ ]);
25804
+ const skillManager = new SkillManager(projectRoot);
25805
+ const allSkills = await skillManager.getAllSkills();
25806
+ if (allSkills.length === 0) {
25807
+ console.log(`
25808
+ ⚠️ 설치 가능한 스킬이 없습니다.`);
25823
25809
  return;
25824
25810
  }
25825
25811
  const { selectedSkills } = await dist_default14.prompt([
@@ -25827,119 +25813,174 @@ async function runInstallWizard(projectRoot) {
25827
25813
  type: "checkbox",
25828
25814
  name: "selectedSkills",
25829
25815
  message: "설치할 스킬들을 선택하세요 (Space로 선택, Enter로 완료):",
25830
- choices: skills.map((s) => ({
25831
- name: `${s.name.padEnd(25)} - ${s.description}`,
25832
- value: s.value
25833
- })),
25816
+ choices: allSkills.map((s) => {
25817
+ const desc = s.description.length > 65 ? s.description.substring(0, 65) + "..." : s.description;
25818
+ return {
25819
+ name: `${s.value.padEnd(25)} - ${desc}`,
25820
+ value: s.value
25821
+ };
25822
+ }),
25823
+ loop: false,
25834
25824
  validate: (answer) => answer.length > 0 ? true : "최소 하나 이상의 스킬을 선택해야 합니다."
25835
25825
  }
25836
25826
  ]);
25837
- let rootPath;
25838
- if (targetType === "openclaw") {
25839
- rootPath = PathManager.getOpenClawRoot();
25840
- } else if (targetType === "claude") {
25841
- rootPath = PathManager.getClaudeRoot();
25842
- } else {
25843
- rootPath = PathManager.getGitHubRoot();
25844
- }
25845
- const workspaces = PathManager.getWorkspaces(rootPath);
25846
- const scopeChoices = [
25847
- { name: "Current Directory (현재 프로젝트)", value: process.cwd() }
25848
- ];
25849
- if (targetType === "openclaw") {
25850
- scopeChoices.push({ name: `Shared Skills (모든 에이전트 공유: ${path4.join(rootPath, "skills")})`, value: path4.join(rootPath, "skills") });
25851
- } else if (targetType === "claude") {
25852
- scopeChoices.push({ name: `Global Skills (~/.claude/skills)`, value: path4.join(rootPath, "skills") });
25853
- } else if (targetType === "github") {
25854
- scopeChoices.push({ name: `GitHub Extensions (~/.config/gh/extensions)`, value: path4.join(rootPath, "extensions") });
25855
- }
25856
- scopeChoices.push(...workspaces.map((ws) => ({ name: `Workspace: ${ws}`, value: path4.join(rootPath, ws) })));
25857
- scopeChoices.push({ name: "Custom Path (직접 입력)", value: "custom" });
25858
- const { scope } = await dist_default14.prompt([
25859
- {
25860
- type: "list",
25861
- name: "scope",
25862
- message: `${targetType} 설치 범위를 선택하세요 (Default: Current Directory):`,
25863
- choices: scopeChoices,
25864
- default: 0
25865
- }
25866
- ]);
25867
- let finalTargetDir;
25868
- if (scope === "custom") {
25869
- const { customPath } = await dist_default14.prompt([
25870
- {
25871
- type: "input",
25872
- name: "customPath",
25873
- message: "설치 경로를 입력하세요:",
25874
- validate: (input) => input.trim() !== "" ? true : "경로를 입력해야 합니다."
25875
- }
25876
- ]);
25877
- finalTargetDir = PathManager.resolveFinalPath(process.cwd(), customPath);
25878
- } else {
25879
- finalTargetDir = scope;
25880
- }
25881
- const skillsBaseDir = targetType === "github" && scope.endsWith("extensions") ? finalTargetDir : path4.join(finalTargetDir, "skills");
25882
25827
  console.log(`
25883
- \uD83D\uDCCD Base Target Path: ${finalTargetDir}`);
25884
- console.log(`\uD83D\uDEE0️ Selected Skills: ${selectedSkills.join(", ")}`);
25885
- console.log(`\uD83D\uDE80 Installation Path: ${skillsBaseDir}/{skill_name}
25886
- `);
25887
- const { proceed } = await dist_default14.prompt([
25828
+ \uD83D\uDEE0️ 선택된 스킬 목록:`);
25829
+ selectedSkills.forEach((s) => console.log(` - ${s}`));
25830
+ const { skillConfirm } = await dist_default14.prompt([
25888
25831
  {
25889
25832
  type: "confirm",
25890
- name: "proceed",
25891
- message: "위 설정대로 설치를 진행할까요?",
25833
+ name: "skillConfirm",
25834
+ message: "위 스킬들을 설치하시겠습니까?",
25892
25835
  default: true
25893
25836
  }
25894
25837
  ]);
25895
- if (proceed) {
25838
+ if (!skillConfirm) {
25896
25839
  console.log(`
25840
+ ❌ 설치가 취소되었습니다.`);
25841
+ return;
25842
+ }
25843
+ console.log(`
25897
25844
  \uD83D\uDE80 설치를 시작합니다...`);
25898
- if (!fs3.existsSync(skillsBaseDir)) {
25899
- fs3.mkdirSync(skillsBaseDir, { recursive: true });
25845
+ const jkparkSkillsRoot = PathManager.getJkparkSkillsRoot();
25846
+ if (installOption === "direct") {
25847
+ if (!fs2.existsSync(targetPath)) {
25848
+ fs2.mkdirSync(targetPath, { recursive: true });
25849
+ }
25850
+ } else {
25851
+ if (!fs2.existsSync(jkparkSkillsRoot)) {
25852
+ fs2.mkdirSync(jkparkSkillsRoot, { recursive: true });
25853
+ }
25854
+ if (!fs2.existsSync(targetPath)) {
25855
+ fs2.mkdirSync(targetPath, { recursive: true });
25900
25856
  }
25901
- for (const skill of selectedSkills) {
25902
- const srcDir = pluginManager.getSkillSourcePath(selectedCategory, skill);
25903
- const destDir = path4.join(skillsBaseDir, skill);
25857
+ }
25858
+ for (const skillValue of selectedSkills) {
25859
+ const skillObj = allSkills.find((s) => s.value === skillValue);
25860
+ if (!skillObj || !skillObj.sourcePath)
25861
+ continue;
25862
+ if (installOption === "direct") {
25863
+ const destDir = path4.join(targetPath, skillObj.name);
25864
+ try {
25865
+ console.log(`- [${skillValue}] 직접 복사 중...`);
25866
+ await import_fs_extra.default.copy(skillObj.sourcePath, destDir);
25867
+ console.log(` ✅ [${skillValue}] 복사 완료`);
25868
+ } catch (err) {
25869
+ console.error(` ❌ [${skillValue}] 복사 실패:`, err.message);
25870
+ }
25871
+ } else {
25872
+ const baseDestDir = path4.join(jkparkSkillsRoot, skillObj.name);
25873
+ const symlinkDestDir = path4.join(targetPath, skillObj.name);
25904
25874
  try {
25905
- console.log(`- [${skill}] 복사 중...`);
25906
- await import_fs_extra.default.copy(srcDir, destDir);
25907
- console.log(` ✅ [${skill}] 설치 완료`);
25875
+ console.log(`- [${skillValue}] ~/.jkpark/skills에 복사 중...`);
25876
+ await import_fs_extra.default.copy(skillObj.sourcePath, baseDestDir);
25877
+ console.log(`- [${skillValue}] 심볼릭 링크 생성 중...`);
25878
+ if (fs2.existsSync(symlinkDestDir)) {
25879
+ fs2.rmSync(symlinkDestDir, { recursive: true, force: true });
25880
+ }
25881
+ const symlinkType = process.platform === "win32" ? "junction" : "dir";
25882
+ fs2.symlinkSync(baseDestDir, symlinkDestDir, symlinkType);
25883
+ console.log(` ✅ [${skillValue}] 링크 설치 완료 (${symlinkType} 방식)`);
25908
25884
  } catch (err) {
25909
- console.error(` ❌ [${skill}] 설치 실패:`, err.message);
25885
+ console.error(` ❌ [${skillValue}] 설치 실패:`, err.message);
25910
25886
  }
25911
25887
  }
25912
- console.log(`
25913
- ✅ 모든 작업이 완료되었습니다. 형, 설치가 끝났어! \uD83D\uDC3E`);
25914
- } else {
25915
- console.log(`
25916
- ❌ 설치가 취소되었습니다.`);
25917
25888
  }
25889
+ console.log(`
25890
+ ✅ 모든 작업이 완료되었습니다! \uD83D\uDC3E`);
25918
25891
  }
25919
25892
  async function runListCommand(projectRoot) {
25920
- const pluginManager = new PluginManager(projectRoot);
25921
- const categories = await pluginManager.getCategories();
25893
+ const skillManager = new SkillManager(projectRoot);
25894
+ const allSkills = await skillManager.getAllSkills();
25922
25895
  console.log(`
25923
- \uD83D\uDCE6 사용 가능한 플러그인 목록:
25896
+ \uD83D\uDCE6 사용 가능한 스킬 목록:
25924
25897
  `);
25925
- for (const cat of categories) {
25926
- console.log(`\uD83D\uDCC2 ${cat.name} (${cat.description})`);
25927
- const skills = await pluginManager.getSkills(cat.value);
25928
- for (const skill of skills) {
25929
- console.log(` - ${skill.name}: ${skill.description}`);
25898
+ if (allSkills.length === 0) {
25899
+ console.log(" ⚠️ 설치 가능한 스킬이 없습니다.");
25900
+ return;
25901
+ }
25902
+ for (const skill of allSkills) {
25903
+ const desc = skill.description.length > 65 ? skill.description.substring(0, 65) + "..." : skill.description;
25904
+ console.log(` - ${skill.name}: ${desc}`);
25905
+ }
25906
+ console.log("");
25907
+ }
25908
+
25909
+ // src/commands/clean.ts
25910
+ import fs3 from "fs";
25911
+ import path5 from "path";
25912
+ var import_fs_extra2 = __toESM(require_lib4(), 1);
25913
+ async function runCleanCommand() {
25914
+ console.log(`
25915
+ \uD83E\uDDF9 jkpark 캐시 정리 마법사에 오신 걸 환영합니다!
25916
+ `);
25917
+ const jkparkSkillsRoot = PathManager.getJkparkSkillsRoot();
25918
+ const openClawTarget = PathManager.getOpenClawWorkspaceRoot();
25919
+ const antigravityTarget = PathManager.getAntigravityRoot(process.cwd());
25920
+ console.log(`\uD83D\uDCCD 캐시 폴더 경로: ${jkparkSkillsRoot}`);
25921
+ console.log(`\uD83D\uDCCD 타겟 폴더 (openclaw): ${openClawTarget}`);
25922
+ console.log(`\uD83D\uDCCD 타겟 폴더 (antigravity): ${antigravityTarget}`);
25923
+ const { confirmClean } = await dist_default14.prompt([
25924
+ {
25925
+ type: "confirm",
25926
+ name: "confirmClean",
25927
+ message: `위 경로들의 스킬 캐시 공간을 비우고 각 타겟 폴더에 생성된 모든 심볼릭 링크(스킬 연결)를 삭제하시겠습니까?
25928
+ (이 작업은 되돌릴 수 없으며 설치된 스킬 연결이 해제됩니다.)`,
25929
+ default: false
25930
25930
  }
25931
- console.log("");
25931
+ ]);
25932
+ if (!confirmClean) {
25933
+ console.log(`
25934
+ ❌ 캐시 비우기가 취소되었습니다.`);
25935
+ return;
25936
+ }
25937
+ try {
25938
+ console.log(`
25939
+ \uD83D\uDDD1️ 원본 캐시 삭제 중...`);
25940
+ if (fs3.existsSync(jkparkSkillsRoot)) {
25941
+ import_fs_extra2.default.emptyDirSync(jkparkSkillsRoot);
25942
+ console.log(` ✨ [${jkparkSkillsRoot}] 내부 비우기 완료`);
25943
+ } else {
25944
+ console.log(` ✅ 캐시 폴더가 이미 비어있거나 없습니다.`);
25945
+ }
25946
+ console.log(`
25947
+ \uD83D\uDDD1️ 타겟 심볼릭 링크 삭제 중...`);
25948
+ const targets = [openClawTarget, antigravityTarget];
25949
+ for (const target of targets) {
25950
+ if (fs3.existsSync(target)) {
25951
+ const items = fs3.readdirSync(target);
25952
+ for (const item of items) {
25953
+ const itemPath = path5.join(target, item);
25954
+ try {
25955
+ const stat = fs3.lstatSync(itemPath);
25956
+ if (stat.isSymbolicLink()) {
25957
+ fs3.rmSync(itemPath, { recursive: true, force: true });
25958
+ console.log(` \uD83D\uDD17 링크 삭제됨: ${itemPath}`);
25959
+ }
25960
+ } catch (e) {
25961
+ console.log(` ❌ 링크 삭제 실패: ${itemPath} (${e.message})`);
25962
+ }
25963
+ }
25964
+ }
25965
+ }
25966
+ console.log(`
25967
+ ✨ 캐시 정리 및 링크 해제 작업이 깔끔하게 완료되었습니다!`);
25968
+ } catch (error) {
25969
+ console.error(`
25970
+ ❌ 삭제 프로세스 중 에러가 발생했습니다:`, error.message);
25932
25971
  }
25933
25972
  }
25934
25973
 
25935
25974
  // src/index.ts
25936
25975
  var __filename2 = fileURLToPath(import.meta.url);
25937
- var __dirname2 = path5.dirname(__filename2);
25938
- var projectRoot = process.env.JKPARK_CLI_ROOT || path5.join(__dirname2, "..");
25976
+ var __dirname2 = path6.dirname(__filename2);
25977
+ var projectRoot = process.env.JKPARK_CLI_ROOT || path6.join(__dirname2, "..");
25939
25978
  var program2 = new Command;
25940
- program2.name("jkpark").description("JK Park의 개인용 패키지 관리 도구").version("2.3.0");
25979
+ var VERSION = "2.3.2";
25980
+ program2.name("jkpark").description("JK Park의 개인용 패키지 관리 도구").version(VERSION);
25941
25981
  program2.command("install").description("패키지 설치 마법사를 실행합니다").action(() => runInstallWizard(projectRoot));
25942
25982
  program2.command("list").description("사용 가능한 모든 플러그인과 스킬을 나열합니다").action(() => runListCommand(projectRoot));
25983
+ program2.command("clean").description("설치된 로컬 스킬 캐시(~/.jkpark/skills)를 모두 삭제하여 초기화합니다").action(() => runCleanCommand());
25943
25984
  if (!process.argv.slice(2).length) {
25944
25985
  program2.outputHelp();
25945
25986
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jkpark",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "JK Park's Personal Package Manager (Bun Powered)",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1 +0,0 @@
1
- {"name": "GCP Tools", "description": "Google Cloud Platform automation skills"}
@@ -1,124 +0,0 @@
1
- ---
2
- name: gcloud-compute-automation
3
- description: "Automates Google Cloud Compute Engine instance management including setup, instance creation, and SSH connection. Use this skill when the user needs to: (1) Install or configure the gcloud CLI, (2) Create new VM instances, or (3) Connect to existing instances via SSH."
4
- ---
5
-
6
- # GCloud Compute Automation
7
-
8
- This skill provides a consolidated guide for managing Google Cloud Compute Engine instances directly using the `gcloud` CLI.
9
-
10
- ## 1. Setup Guide
11
-
12
- If `gcloud` is not yet installed or configured, follow these steps:
13
-
14
- ### Installation
15
- - **Debian/Ubuntu:**
16
- ```bash
17
- # Add Package Source
18
- sudo apt-get install -y apt-transport-https ca-certificates gnupg curl
19
- curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
20
- echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
21
-
22
- # Install
23
- sudo apt-get update && sudo apt-get install -y google-cloud-cli
24
- ```
25
- - **Other Linux Distributions:**
26
- ```bash
27
- curl https://sdk.cloud.google.com | bash
28
- exec -l $SHELL
29
- ```
30
- - **macOS:**
31
- ```bash
32
- brew install --cask google-cloud-sdk
33
- ```
34
-
35
- ### Initialization & Authentication
36
- 1. **Initialize gcloud**:
37
- ```bash
38
- gcloud init
39
- ```
40
- 2. **Web-free Login**: If you are in a remote environment or prefer no browser, run:
41
- ```bash
42
- gcloud auth login --no-launch-browser
43
- ```
44
- - Copy the provided URL into your local browser.
45
- - Authorize access and copy the verification code.
46
- - Paste the code back into your terminal.
47
-
48
- ## 2. Project Management
49
-
50
- After logging in, you must select or create a Google Cloud Project.
51
-
52
- ### A. List Existing Projects
53
- Check if you already have a project to use:
54
- ```bash
55
- gcloud projects list
56
- ```
57
-
58
- ### B. Create a New Project
59
- If you don't have a project, or want a new one for this VM, follow these steps:
60
- 1. **Create the project**:
61
- ```bash
62
- gcloud projects create [PROJECT_ID] --name="[PROJECT_NAME]"
63
- ```
64
- *Note: [PROJECT_ID] must be unique across all of Google Cloud.*
65
- 2. **Set as active**:
66
- ```bash
67
- gcloud config set project [PROJECT_ID]
68
- ```
69
- 3. **Enable Compute Engine API**:
70
- ```bash
71
- gcloud services enable compute.googleapis.com
72
- ```
73
- *Note: This requires billing to be enabled for the project.*
74
-
75
- ---
76
-
77
- ## 3. VM Management Workflow
78
-
79
- Follow this workflow to create and connect to a VM.
80
-
81
- ### Step A: Evaluate Machine Type and Cost
82
- Before creating an instance, check the available machine types and their specifications to estimate costs.
83
-
84
- 1. **List Machine Types**: Display available machine types and their core/memory specs in your zone.
85
- ```bash
86
- gcloud compute machine-types list --zones=[ZONE]
87
- ```
88
- *Note: Costs vary by machine type. For example, `e2-micro` is generally the cheapest, while `e2-standard-4` provides more resources at a higher price.*
89
-
90
- 2. **Estimate Cost**: Use the [Google Cloud Pricing Calculator](https://cloud.google.com/products/calculator) for precise estimates based on your selected machine type and region.
91
-
92
- ### Step B: Create an Instance
93
- **Mandatory**: You must provide a unique name for your instance.
94
-
95
- ```bash
96
- gcloud compute instances create [MANDATORY_INSTANCE_NAME] \
97
- --zone=[ZONE] \
98
- --machine-type=[MACHINE_TYPE] \
99
- --image-family=debian-12 \
100
- --image-project=debian-cloud
101
- ```
102
- *Tip: Common zones include `us-central1-a`, `asia-northeast3-a` (Seoul). Recommended machine types: `e2-micro` (development), `e2-small` (standard).*
103
-
104
- ### Step C: Connect via SSH
105
- Once the instance is running, connect using:
106
- ```bash
107
- gcloud compute ssh [INSTANCE_NAME] --zone=[ZONE]
108
- ```
109
-
110
- ---
111
-
112
- ## 4. Quick Reference
113
-
114
- | Action | Command |
115
- | :--- | :--- |
116
- | **List Instances** | `gcloud compute instances list` |
117
- | **Stop Instance** | `gcloud compute instances stop [NAME]` |
118
- | **Start Instance** | `gcloud compute instances start [NAME]` |
119
- | **Delete Instance** | `gcloud compute instances delete [NAME]` |
120
- | **Check Config** | `gcloud config list` |
121
-
122
- ## Tips for Efficiency
123
- - **Preemptible VMs:** Add `--preemptible` to the create command to save up to 80% on costs for short-lived tasks.
124
- - **Static External IP:** If you need a constant IP, use `--address=[IP_NAME]`.
@@ -1 +0,0 @@
1
- {"name": "Trading Bot", "description": "Binance trading automation skills"}
@@ -1,48 +0,0 @@
1
- ---
2
- name: binance-trader
3
- description: Interface with the Binance API using python-binance. Use for automated trading, account management, market data retrieval, and real-time socket streaming. Contains examples for orders, historical data, and async operations.
4
- ---
5
-
6
- # Binance Trader Skill
7
-
8
- This skill provides a structured way to interact with the Binance exchange using the `python-binance` Python wrapper.
9
-
10
- ## Core Workflows
11
-
12
- ### 1. Market Data Retrieval
13
- Fetch current prices, order books, or historical K-line (candle) data.
14
- - **Reference**: See [references/api_usage.md](references/api_usage.md)
15
- - **Example Script**: [scripts/historical_data.py](scripts/historical_data.py)
16
-
17
- ### 2. Account & Portfolio Management
18
- Check balances and manage account settings.
19
- - **Example Script**: [scripts/basic_ops.py](scripts/basic_ops.py)
20
-
21
- ### 3. Order Execution
22
- Place Market, Limit, or OCO (One-Cancels-the-Other) orders.
23
- - **Reference**: See [references/api_usage.md](references/api_usage.md)
24
- - **Example Script**: [scripts/order_examples.py](scripts/order_examples.py)
25
-
26
- ### 4. Real-time Streaming (WebSockets)
27
- Stream ticker updates, trade data, or account updates using `asyncio`.
28
- - **Example Script**: [scripts/async_sockets.py](scripts/async_sockets.py)
29
-
30
- ## Best Practices
31
-
32
- - **Security**: Always use environment variables (`BINANCE_API_KEY`, `BINANCE_API_SECRET` or `BINANCE_SECRET_KEY`) instead of hardcoding keys.
33
- - **.env file (required)**: Place your API keys in a `.env` file at the project root. If `.env` does not exist, create it from the provided template:
34
-
35
- ```bash
36
- cp skills/binance-trader/assets/.env.example .env
37
- # then edit .env and fill in your keys
38
- ```
39
-
40
- Scripts shipped with this skill will automatically look for `.env` in the script directory, the `test/` folder, and the project root.
41
-
42
- - **Error Handling**: Wrap API calls in `try-except` blocks to handle `BinanceAPIException`.
43
- - **Rate Limits**: Be mindful of Binance's rate limits (weight-based).
44
- - **Testnet**: Use `testnet=True` during development to avoid losing real funds.
45
-
46
- ## Resources
47
- - **Repository**: [sammchardy/python-binance](https://github.com/sammchardy/python-binance)
48
- - **Official Docs**: [python-binance.readthedocs.io](https://python-binance.readthedocs.io/en/latest/)
@@ -1,9 +0,0 @@
1
- # Binance API Credentials
2
- BINANCE_API_KEY=your_api_key_here
3
- BINANCE_API_SECRET=your_api_secret_here
4
-
5
- # Optional: Binance TLD (e.g., 'us' for binance.us)
6
- # BINANCE_TLD=com
7
-
8
- # Optional: Testnet mode (True/False)
9
- # BINANCE_TESTNET=False
@@ -1,65 +0,0 @@
1
- # Binance API Guide (python-binance)
2
-
3
- This guide covers common tasks when using the `python-binance` library.
4
-
5
- ## Setup
6
-
7
- Install the library:
8
- ```bash
9
- pip install python-binance python-dotenv
10
- ```
11
-
12
- ## Credentials
13
-
14
- Create a `.env` file based on `assets/.env.example`:
15
- ```bash
16
- cp skills/binance-trader/assets/.env.example .env
17
- ```
18
- Edit `.env` and add your Binance API keys (required variables: `BINANCE_API_KEY` and `BINANCE_API_SECRET` — the scripts also support `BINANCE_SECRET_KEY`).
19
-
20
- If `.env` is missing, the scripts will try to find it in the script directory, the `test/` folder, and then the project root; if you prefer, place `.env` at the project root for consistency.
21
-
22
- ## Client Initialization
23
-
24
- ```python
25
- from binance import Client
26
- client = Client(api_key, api_secret)
27
- # For Testnet
28
- # client = Client(api_key, api_secret, testnet=True)
29
- ```
30
-
31
- ## Common REST API Methods
32
-
33
- | Method | Description |
34
- |--------|-------------|
35
- | `get_account()` | Fetch account information and balances |
36
- | `get_asset_balance(asset='BTC')` | Get balance for a specific asset |
37
- | `get_symbol_ticker(symbol='BTCUSDT')` | Get latest price |
38
- | `get_order_book(symbol='BTCUSDT')` | Get order book depth |
39
- | `get_historical_klines(symbol, interval, start_str, end_str=None)` | Get historical OHLCV data |
40
-
41
- ## Order Types
42
-
43
- ### Market Order
44
- ```python
45
- client.create_order(symbol='BTCUSDT', side='BUY', type='MARKET', quantity=0.001)
46
- ```
47
-
48
- ### Limit Order
49
- ```python
50
- client.create_order(symbol='BTCUSDT', side='SELL', type='LIMIT', timeInForce='GTC', quantity=0.001, price='70000')
51
- ```
52
-
53
- ### OCO Order
54
- ```python
55
- client.create_oco_order(symbol='BTCUSDT', side='SELL', quantity=0.001, price='75000', stopPrice='60000', stopLimitPrice='59900', stopLimitTimeInForce='GTC')
56
- ```
57
-
58
- ## WebSockets (Async)
59
-
60
- Use `BinanceSocketManager` for real-time data.
61
-
62
- ```python
63
- from binance import AsyncClient, BinanceSocketManager
64
- # See scripts/async_sockets.py for example
65
- ```
@@ -1,29 +0,0 @@
1
- import asyncio
2
- from binance import AsyncClient, BinanceSocketManager
3
- import os
4
-
5
- # Manual .env loading if dotenv is missing
6
- if os.path.exists('.env'):
7
- with open('.env', 'r') as f:
8
- for line in f:
9
- if '=' in line and not line.startswith('#'):
10
- key, value = line.strip().split('=', 1)
11
- os.environ[key] = value
12
-
13
- async def main():
14
- client = await AsyncClient.create(
15
- api_key=os.getenv('BINANCE_API_KEY'),
16
- api_secret=os.getenv('BINANCE_API_SECRET') or os.getenv('BINANCE_SECRET_KEY')
17
- )
18
- bsm = BinanceSocketManager(client)
19
-
20
- # K-line socket
21
- async with bsm.kline_socket(symbol='BTCUSDT') as stream:
22
- for _ in range(5):
23
- res = await stream.recv()
24
- print(f"BTCUSDT K-line: {res['k']['c']}") # Current close price
25
-
26
- await client.close_connection()
27
-
28
- if __name__ == "__main__":
29
- asyncio.run(main())
@@ -1,31 +0,0 @@
1
- from binance import Client
2
- import os
3
-
4
- # Manual .env loading if dotenv is missing
5
- env_path = '.env'
6
- if not os.path.exists(env_path):
7
- env_path = os.path.join(os.path.dirname(__file__), '..', '..', '.env') # From scripts/ to root
8
-
9
- if os.path.exists(env_path):
10
- with open(env_path, 'r') as f:
11
- for line in f:
12
- if '=' in line and not line.startswith('#'):
13
- key, value = line.strip().split('=', 1)
14
- os.environ[key] = value.strip('"').strip("'")
15
-
16
- api_key = os.getenv('BINANCE_API_KEY')
17
- api_secret = os.getenv('BINANCE_API_SECRET') or os.getenv('BINANCE_SECRET_KEY')
18
-
19
- client = Client(api_key, api_secret)
20
-
21
- # Get market depth
22
- depth = client.get_order_book(symbol='BTCUSDT')
23
- print(f"Bids: {depth['bids'][:3]}")
24
-
25
- # Get account information
26
- account = client.get_account()
27
- print(f"Account balances: {[b for b in account['balances'] if float(b['free']) > 0]}")
28
-
29
- # Get asset balance
30
- balance = client.get_asset_balance(asset='BTC')
31
- print(f"BTC Balance: {balance}")
@@ -1,28 +0,0 @@
1
- from binance import Client
2
- import os
3
-
4
- client = Client() # Public API doesn't require keys
5
-
6
- # Fetch 1 minute klines for the last day
7
- klines = client.get_historical_klines("BTCUSDT", Client.KLINE_INTERVAL_1MINUTE, "1 day ago UTC")
8
-
9
- # kline format:
10
- # [
11
- # [
12
- # 1499040000000, # Open time
13
- # "0.01634790", # Open
14
- # "0.80000000", # High
15
- # "0.01575800", # Low
16
- # "0.01577100", # Close
17
- # "148976.11427815", # Volume
18
- # 1499644799999, # Close time
19
- # "2434.19055334", # Quote asset volume
20
- # 308, # Number of trades
21
- # "1756.87402397", # Taker buy base asset volume
22
- # "28.46694368", # Taker buy quote asset volume
23
- # "17928899.62484339" # Ignore
24
- # ]
25
- # ]
26
-
27
- for k in klines[:5]:
28
- print(f"Time: {k[0]}, Open: {k[1]}, Close: {k[4]}")
@@ -1,48 +0,0 @@
1
- from binance import Client
2
- import os
3
-
4
- # Manual .env loading if dotenv is missing
5
- env_path = '.env'
6
- if not os.path.exists(env_path):
7
- env_path = os.path.join(os.path.dirname(__file__), '..', '..', '.env')
8
-
9
- if os.path.exists(env_path):
10
- with open(env_path, 'r') as f:
11
- for line in f:
12
- if '=' in line and not line.startswith('#'):
13
- key, value = line.strip().split('=', 1)
14
- os.environ[key] = value.strip('"').strip("'")
15
-
16
- api_key = os.getenv('BINANCE_API_KEY')
17
- api_secret = os.getenv('BINANCE_API_SECRET') or os.getenv('BINANCE_SECRET_KEY')
18
-
19
- client = Client(api_key, api_secret)
20
-
21
- # 1. Market Buy Order
22
- # order = client.create_order(
23
- # symbol='BNBBTC',
24
- # side=Client.SIDE_BUY,
25
- # type=Client.ORDER_TYPE_MARKET,
26
- # quantity=100)
27
-
28
- # 2. Limit Sell Order
29
- # order = client.create_order(
30
- # symbol='BNBBTC',
31
- # side=Client.SIDE_SELL,
32
- # type=Client.ORDER_TYPE_LIMIT,
33
- # timeInForce=Client.TIME_IN_FORCE_GTC,
34
- # quantity=100,
35
- # price='0.0001')
36
-
37
- # 3. OCO (One-Cancels-the-Other) Order
38
- # OCO orders are useful for setting both a take-profit and a stop-loss
39
- # order = client.create_oco_order(
40
- # symbol='BTCUSDT',
41
- # side=Client.SIDE_SELL,
42
- # stopLimitTimeInForce=Client.TIME_IN_FORCE_GTC,
43
- # quantity=0.001,
44
- # stopPrice='60000',
45
- # stopLimitPrice='59900',
46
- # price='75000')
47
-
48
- print("Example script for order types. Uncomment code blocks to use.")