mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Add a safe Toss Securities lookup surface without trading mutations
Wrap the upstream tossinvest-cli binary behind a small read-only Node package, add the matching skill/docs wiring, and lock the behavior with regression tests plus publish/release metadata so the new workspace stays release-ready. Constraint: Issue #25 required using JungHoonGhae/tossinvest-cli rather than reimplementing Toss APIs directly Constraint: The public package surface must stay read-only and avoid wrapping trading mutations Rejected: Direct HTTP client against unofficial Toss endpoints | issue explicitly required the upstream CLI and would widen maintenance risk Rejected: Skill docs only with no package wrapper | repo convention is to ship reusable package + docs + release wiring together Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep toss-securities limited to read-only tossctl commands unless a future issue explicitly approves trading flows and adds stronger safeguards Tested: npm run ci; brew tap JungHoonGhae/tossinvest-cli && brew install tossctl && tossctl version; node smoke calling packages/toss-securities/src/index.js getQuote('TSLA'); lsp diagnostics on affected files Not-tested: Authenticated account/portfolio/order-history commands against a real Toss session
This commit is contained in:
parent
aa51d2cbab
commit
2700e426a9
14 changed files with 699 additions and 2 deletions
5
.changeset/fair-eagles-drum.md
Normal file
5
.changeset/fair-eagles-drum.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"toss-securities": minor
|
||||
---
|
||||
|
||||
Add the first safe read-only Toss Securities wrapper package and skill docs.
|
||||
|
|
@ -25,6 +25,7 @@ Claude code, codex, opencode 등 각종 코딩 에이전트 지원합니다.
|
|||
| 사용자 위치 미세먼지 조회 | `k-skill-proxy` 로 현재 위치 또는 지역 fallback 기준 PM10/PM2.5 확인 | 불필요 | [사용자 위치 미세먼지 조회 가이드](docs/features/fine-dust-location.md) |
|
||||
| KBO 경기 결과 조회 | 날짜별 경기 일정, 결과, 팀별 필터링 | 불필요 | [KBO 결과 가이드](docs/features/kbo-results.md) |
|
||||
| K리그 경기 결과 조회 | 날짜별 K리그1/K리그2 경기 결과, 팀별 필터링, 현재 순위 확인 | 불필요 | [K리그 결과 가이드](docs/features/kleague-results.md) |
|
||||
| 토스증권 조회 | `tossctl` 기반 계좌 요약, 포트폴리오, 시세, 주문내역, 관심종목 조회 | 필요 | [토스증권 조회 가이드](docs/features/toss-securities.md) |
|
||||
| 로또 당첨 확인 | 최신 회차, 특정 회차, 번호 대조 | 불필요 | [로또 결과 가이드](docs/features/lotto-results.md) |
|
||||
| HWP 문서 처리 | `.hwp` → JSON/Markdown/HTML 변환, 이미지 추출, 배치 처리, Windows 직접 제어 선택 | 불필요 | [HWP 문서 처리 가이드](docs/features/hwp.md) |
|
||||
| 근처 블루리본 맛집 | 현재 위치를 먼저 확인한 뒤 블루리본 서베이 공식 표면으로 근처 블루리본 맛집 검색 | 불필요 | [근처 블루리본 맛집 가이드](docs/features/blue-ribbon-nearby.md) |
|
||||
|
|
@ -63,6 +64,7 @@ Claude code, codex, opencode 등 각종 코딩 에이전트 지원합니다.
|
|||
- [사용자 위치 미세먼지 조회](docs/features/fine-dust-location.md)
|
||||
- [KBO 경기 결과 조회](docs/features/kbo-results.md)
|
||||
- [K리그 경기 결과 조회](docs/features/kleague-results.md)
|
||||
- [토스증권 조회 가이드](docs/features/toss-securities.md)
|
||||
- [로또 당첨 확인](docs/features/lotto-results.md)
|
||||
- [HWP 문서 처리](docs/features/hwp.md)
|
||||
- [근처 블루리본 맛집 가이드](docs/features/blue-ribbon-nearby.md)
|
||||
|
|
|
|||
83
docs/features/toss-securities.md
Normal file
83
docs/features/toss-securities.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# 토스증권 조회 가이드
|
||||
|
||||
## 이 기능으로 할 수 있는 일
|
||||
|
||||
- `tossctl` 기반 토스증권 계좌 목록 / 계좌 요약 조회
|
||||
- 포트폴리오 보유 종목 / 자산 비중 조회
|
||||
- 단일 종목 / 다중 종목 시세 조회
|
||||
- 미체결 주문 / 월간 체결 내역 조회
|
||||
- 관심종목 목록 조회
|
||||
|
||||
## 먼저 필요한 것
|
||||
|
||||
- macOS + Homebrew
|
||||
- `tossctl` 설치
|
||||
- `tossctl auth login` 으로 브라우저 세션 확보
|
||||
- `node` 18+
|
||||
|
||||
## upstream 설치와 로그인
|
||||
|
||||
이 기능은 `JungHoonGhae/tossinvest-cli` 의 `tossctl` 을 그대로 사용한다.
|
||||
|
||||
```bash
|
||||
brew tap JungHoonGhae/tossinvest-cli
|
||||
brew install tossctl
|
||||
tossctl doctor
|
||||
tossctl auth doctor
|
||||
tossctl auth login
|
||||
```
|
||||
|
||||
로그인이 끝나기 전에는 계좌/포트폴리오 조회를 시도하지 않는다.
|
||||
|
||||
## 지원하는 read-only 명령
|
||||
|
||||
- `tossctl account list --output json`
|
||||
- `tossctl account summary --output json`
|
||||
- `tossctl portfolio positions --output json`
|
||||
- `tossctl portfolio allocation --output json`
|
||||
- `tossctl quote get TSLA --output json`
|
||||
- `tossctl quote batch TSLA 005930 VOO --output json`
|
||||
- `tossctl orders list --output json`
|
||||
- `tossctl orders completed --market all --output json`
|
||||
- `tossctl watchlist list --output json`
|
||||
|
||||
## Node.js 예시
|
||||
|
||||
```js
|
||||
const {
|
||||
getAccountSummary,
|
||||
getPortfolioPositions,
|
||||
getQuote,
|
||||
listCompletedOrders
|
||||
} = require("toss-securities");
|
||||
|
||||
async function main() {
|
||||
const summary = await getAccountSummary();
|
||||
const positions = await getPortfolioPositions();
|
||||
const quote = await getQuote("TSLA");
|
||||
const completed = await listCompletedOrders({ market: "all" });
|
||||
|
||||
console.log(summary.data);
|
||||
console.log(positions.data);
|
||||
console.log(quote.data);
|
||||
console.log(completed.data);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
|
||||
## 운영 팁
|
||||
|
||||
- 계좌 요약과 포트폴리오는 로그인 세션이 있어야만 동작한다.
|
||||
- `TSLA`, `VOO`, `005930` 같이 심볼을 그대로 넘기면 된다.
|
||||
- 주문 관련 답변은 **조회 결과만** 정리하고, 실거래로 이어지는 행동은 권하지 않는다.
|
||||
- 민감한 계좌 정보는 꼭 필요한 값만 답한다.
|
||||
|
||||
## 주의할 점
|
||||
|
||||
- `tossctl` 은 비공식 CLI 이므로 웹 내부 API 변경에 영향을 받을 수 있다.
|
||||
- 브라우저 세션이 만료되면 `tossctl auth login` 을 다시 해야 할 수 있다.
|
||||
- 이 레포의 `toss-securities` 패키지는 read-only wrapper 이며, 거래 mutation 명령은 공개 API에 포함하지 않는다.
|
||||
|
|
@ -47,6 +47,7 @@ npx --yes skills add <owner/repo> \
|
|||
--skill hwp \
|
||||
--skill kbo-results \
|
||||
--skill kleague-results \
|
||||
--skill toss-securities \
|
||||
--skill lotto-results \
|
||||
--skill kakaotalk-mac \
|
||||
--skill fine-dust-location \
|
||||
|
|
@ -102,7 +103,7 @@ npm run ci
|
|||
### Node 패키지
|
||||
|
||||
```bash
|
||||
npm install -g @ohah/hwpjs kbo-game kleague-results k-lotto
|
||||
npm install -g @ohah/hwpjs kbo-game kleague-results toss-securities k-lotto
|
||||
export NODE_PATH="$(npm root -g)"
|
||||
```
|
||||
|
||||
|
|
@ -112,6 +113,8 @@ export NODE_PATH="$(npm root -g)"
|
|||
|
||||
```bash
|
||||
brew install silver-flight-group/tap/kakaocli
|
||||
brew tap JungHoonGhae/tossinvest-cli
|
||||
brew install tossctl
|
||||
```
|
||||
|
||||
### Python 패키지
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
- KTX
|
||||
- KBO 경기 결과
|
||||
- K리그 경기 결과 조회 스킬 출시
|
||||
- 토스증권 조회 스킬 출시
|
||||
- 로또 당첨번호
|
||||
- 서울 지하철 도착 정보
|
||||
- 사용자 위치 미세먼지 조회 스킬 출시
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
- `korail2` / `carpedm20/korail2`: https://github.com/carpedm20/korail2
|
||||
- `korail2` anti-bot bypass PR #54: https://github.com/carpedm20/korail2/pull/54
|
||||
- `kbo-game`: https://github.com/vkehfdl1/kbo-game
|
||||
- tossinvest-cli: https://github.com/JungHoonGhae/tossinvest-cli
|
||||
- K League 일정/결과 JSON: https://www.kleague.com/getScheduleList.do
|
||||
- K League 팀 순위 JSON: https://www.kleague.com/record/teamRank.do
|
||||
- `@ohah/hwpjs`: https://github.com/ohah/hwpjs
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"lint": "node --check scripts/skill-docs.test.js && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --test scripts/skill-docs.test.js && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace kleague-results --dry-run",
|
||||
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace toss-securities --dry-run",
|
||||
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
|
||||
"version-packages": "changeset version",
|
||||
"release:npm": "changeset publish"
|
||||
|
|
|
|||
73
packages/toss-securities/README.md
Normal file
73
packages/toss-securities/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# toss-securities
|
||||
|
||||
`JungHoonGhae/tossinvest-cli` 의 `tossctl` 바이너리를 감싸는 **read-only tossctl wrapper** 입니다. 이 패키지는 설치/로그인/조회 흐름만 정리하고, 거래 mutation 은 공개 API에서 지원하지 않습니다.
|
||||
|
||||
## Install
|
||||
|
||||
먼저 upstream CLI 를 설치합니다.
|
||||
|
||||
```bash
|
||||
brew tap JungHoonGhae/tossinvest-cli
|
||||
brew install tossctl
|
||||
tossctl doctor
|
||||
tossctl auth doctor
|
||||
tossctl auth login
|
||||
```
|
||||
|
||||
그 다음 배포된 패키지를 설치합니다.
|
||||
|
||||
```bash
|
||||
npm install toss-securities
|
||||
```
|
||||
|
||||
## Supported read-only helpers
|
||||
|
||||
- `listAccounts()`
|
||||
- `getAccountSummary()`
|
||||
- `getPortfolioPositions()`
|
||||
- `getPortfolioAllocation()`
|
||||
- `getQuote(symbol)`
|
||||
- `getQuoteBatch(symbols)`
|
||||
- `listOrders()`
|
||||
- `listCompletedOrders({ market })`
|
||||
- `listWatchlist()`
|
||||
|
||||
모든 helper 는 내부적으로 `tossctl ... --output json` 을 실행하고, `commandName`, `bin`, `args`, `data` 를 반환합니다.
|
||||
|
||||
대응되는 대표 CLI 는 `tossctl account summary --output json`, `tossctl quote get TSLA --output json`, `tossctl watchlist list --output json` 입니다.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const {
|
||||
getAccountSummary,
|
||||
getQuote,
|
||||
listWatchlist
|
||||
} = require("toss-securities");
|
||||
|
||||
async function main() {
|
||||
const summary = await getAccountSummary({
|
||||
configDir: "/Users/me/.config/tossctl"
|
||||
});
|
||||
const quote = await getQuote("TSLA");
|
||||
const watchlist = await listWatchlist();
|
||||
|
||||
console.log(summary.data);
|
||||
console.log(quote.data);
|
||||
console.log(watchlist.data);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
|
||||
## What is intentionally not supported
|
||||
|
||||
- `tossctl order place`
|
||||
- `tossctl order cancel`
|
||||
- `tossctl order amend`
|
||||
- permission grant/revoke
|
||||
|
||||
이 패키지는 조회 전용이다. 실거래에 영향을 주는 명령은 upstream safety gate 를 우회하지 않도록 래핑하지 않는다.
|
||||
32
packages/toss-securities/package.json
Normal file
32
packages/toss-securities/package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "toss-securities",
|
||||
"version": "0.1.0",
|
||||
"description": "Safe read-only tossctl wrapper for Toss Securities skill workflows",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/NomaDamas/k-skill.git"
|
||||
},
|
||||
"keywords": [
|
||||
"k-skill",
|
||||
"korea",
|
||||
"toss",
|
||||
"securities",
|
||||
"tossctl"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "node --check src/index.js && node --check src/parse.js && node --check test/index.test.js",
|
||||
"test": "node --test"
|
||||
}
|
||||
}
|
||||
98
packages/toss-securities/src/index.js
Normal file
98
packages/toss-securities/src/index.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
const childProcess = require("node:child_process");
|
||||
const util = require("node:util");
|
||||
|
||||
const {
|
||||
buildReadOnlyArgs,
|
||||
parseJsonOutput
|
||||
} = require("./parse");
|
||||
|
||||
const execFile = util.promisify(childProcess.execFile);
|
||||
|
||||
function buildReadOnlyCommand(commandName, options = {}) {
|
||||
return {
|
||||
bin: options.bin || "tossctl",
|
||||
args: buildReadOnlyArgs(commandName, options)
|
||||
};
|
||||
}
|
||||
|
||||
async function runReadOnlyCommand(commandName, options = {}) {
|
||||
const command = buildReadOnlyCommand(commandName, options);
|
||||
|
||||
try {
|
||||
const result = await execFile(command.bin, command.args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
timeout: options.timeoutMs,
|
||||
maxBuffer: options.maxBuffer || 1024 * 1024
|
||||
});
|
||||
|
||||
return {
|
||||
...command,
|
||||
...parseJsonOutput(result.stdout, commandName),
|
||||
stderr: result.stderr
|
||||
};
|
||||
} catch (error) {
|
||||
const stderr = String(error.stderr || "").trim();
|
||||
const detail = stderr || error.message;
|
||||
|
||||
throw new Error(`tossctl ${commandName} failed: ${detail}`, {
|
||||
cause: error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function listAccounts(options = {}) {
|
||||
return runReadOnlyCommand("accountList", options);
|
||||
}
|
||||
|
||||
function getAccountSummary(options = {}) {
|
||||
return runReadOnlyCommand("accountSummary", options);
|
||||
}
|
||||
|
||||
function getPortfolioPositions(options = {}) {
|
||||
return runReadOnlyCommand("portfolioPositions", options);
|
||||
}
|
||||
|
||||
function getPortfolioAllocation(options = {}) {
|
||||
return runReadOnlyCommand("portfolioAllocation", options);
|
||||
}
|
||||
|
||||
function listOrders(options = {}) {
|
||||
return runReadOnlyCommand("ordersList", options);
|
||||
}
|
||||
|
||||
function listCompletedOrders(options = {}) {
|
||||
return runReadOnlyCommand("ordersCompleted", options);
|
||||
}
|
||||
|
||||
function listWatchlist(options = {}) {
|
||||
return runReadOnlyCommand("watchlistList", options);
|
||||
}
|
||||
|
||||
function getQuote(symbol, options = {}) {
|
||||
return runReadOnlyCommand("quoteGet", {
|
||||
...options,
|
||||
symbol
|
||||
});
|
||||
}
|
||||
|
||||
function getQuoteBatch(symbols, options = {}) {
|
||||
return runReadOnlyCommand("quoteBatch", {
|
||||
...options,
|
||||
symbols
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildReadOnlyCommand,
|
||||
getAccountSummary,
|
||||
getPortfolioAllocation,
|
||||
getPortfolioPositions,
|
||||
getQuote,
|
||||
getQuoteBatch,
|
||||
listAccounts,
|
||||
listCompletedOrders,
|
||||
listOrders,
|
||||
listWatchlist,
|
||||
runReadOnlyCommand
|
||||
};
|
||||
123
packages/toss-securities/src/parse.js
Normal file
123
packages/toss-securities/src/parse.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
const READ_ONLY_COMMANDS = Object.freeze({
|
||||
accountList: {
|
||||
segments: ["account", "list"]
|
||||
},
|
||||
accountSummary: {
|
||||
segments: ["account", "summary"]
|
||||
},
|
||||
portfolioPositions: {
|
||||
segments: ["portfolio", "positions"]
|
||||
},
|
||||
portfolioAllocation: {
|
||||
segments: ["portfolio", "allocation"]
|
||||
},
|
||||
ordersList: {
|
||||
segments: ["orders", "list"]
|
||||
},
|
||||
ordersCompleted: {
|
||||
segments: ["orders", "completed"],
|
||||
buildExtraArgs(options = {}) {
|
||||
return ["--market", normalizeMarket(options.market || "all")];
|
||||
}
|
||||
},
|
||||
watchlistList: {
|
||||
segments: ["watchlist", "list"]
|
||||
},
|
||||
quoteGet: {
|
||||
segments: ["quote", "get"],
|
||||
buildExtraArgs(options = {}) {
|
||||
return [normalizeSymbol(options.symbol)];
|
||||
}
|
||||
},
|
||||
quoteBatch: {
|
||||
segments: ["quote", "batch"],
|
||||
buildExtraArgs(options = {}) {
|
||||
return normalizeSymbols(options.symbols);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function assertReadOnlyCommandName(commandName) {
|
||||
if (!READ_ONLY_COMMANDS[commandName]) {
|
||||
throw new Error(`Unsupported read-only tossctl command: ${commandName}`);
|
||||
}
|
||||
|
||||
return commandName;
|
||||
}
|
||||
|
||||
function buildReadOnlyArgs(commandName, options = {}) {
|
||||
const resolvedName = assertReadOnlyCommandName(commandName);
|
||||
const spec = READ_ONLY_COMMANDS[resolvedName];
|
||||
const args = ["--output", "json"];
|
||||
|
||||
if (options.configDir) {
|
||||
args.push("--config-dir", String(options.configDir));
|
||||
}
|
||||
|
||||
if (options.sessionFile) {
|
||||
args.push("--session-file", String(options.sessionFile));
|
||||
}
|
||||
|
||||
args.push(...spec.segments);
|
||||
|
||||
if (typeof spec.buildExtraArgs === "function") {
|
||||
args.push(...spec.buildExtraArgs(options));
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function parseJsonOutput(stdout, commandName) {
|
||||
const text = String(stdout || "").trim();
|
||||
|
||||
if (!text) {
|
||||
throw new Error(`tossctl ${commandName} returned empty output.`);
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
commandName,
|
||||
data: JSON.parse(text)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse tossctl JSON output for ${commandName}: ${error.message}`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMarket(value) {
|
||||
const market = String(value || "").trim().toLowerCase();
|
||||
|
||||
if (!["all", "us", "kr"].includes(market)) {
|
||||
throw new Error(`market must be one of all, us, kr. Received: ${value}`);
|
||||
}
|
||||
|
||||
return market;
|
||||
}
|
||||
|
||||
function normalizeSymbol(value) {
|
||||
const symbol = String(value || "").trim();
|
||||
|
||||
if (!symbol) {
|
||||
throw new Error("symbol is required.");
|
||||
}
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function normalizeSymbols(values) {
|
||||
if (!Array.isArray(values) || values.length === 0) {
|
||||
throw new Error("symbols must be a non-empty array.");
|
||||
}
|
||||
|
||||
return values.map(normalizeSymbol);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
READ_ONLY_COMMANDS,
|
||||
assertReadOnlyCommandName,
|
||||
buildReadOnlyArgs,
|
||||
parseJsonOutput
|
||||
};
|
||||
103
packages/toss-securities/test/index.test.js
Normal file
103
packages/toss-securities/test/index.test.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const {
|
||||
buildReadOnlyCommand,
|
||||
getAccountSummary,
|
||||
getQuote
|
||||
} = require("../src/index");
|
||||
const {
|
||||
assertReadOnlyCommandName,
|
||||
parseJsonOutput
|
||||
} = require("../src/parse");
|
||||
|
||||
test("buildReadOnlyCommand assembles tossctl args for supported read-only commands", () => {
|
||||
const command = buildReadOnlyCommand("quoteGet", {
|
||||
symbol: "TSLA",
|
||||
configDir: "/tmp/toss",
|
||||
sessionFile: "/tmp/toss/session.json"
|
||||
});
|
||||
|
||||
assert.equal(command.bin, "tossctl");
|
||||
assert.deepEqual(command.args, [
|
||||
"--output",
|
||||
"json",
|
||||
"--config-dir",
|
||||
"/tmp/toss",
|
||||
"--session-file",
|
||||
"/tmp/toss/session.json",
|
||||
"quote",
|
||||
"get",
|
||||
"TSLA"
|
||||
]);
|
||||
});
|
||||
|
||||
test("read-only command validation rejects unsupported or dangerous command names", () => {
|
||||
assert.equal(assertReadOnlyCommandName("accountSummary"), "accountSummary");
|
||||
assert.throws(() => assertReadOnlyCommandName("orderPlace"), /Unsupported read-only tossctl command/);
|
||||
});
|
||||
|
||||
test("parseJsonOutput annotates JSON payloads with the originating command", () => {
|
||||
const result = parseJsonOutput('{"ok":true,"items":[1,2]}', "watchlistList");
|
||||
|
||||
assert.equal(result.commandName, "watchlistList");
|
||||
assert.deepEqual(result.data, {
|
||||
ok: true,
|
||||
items: [1, 2]
|
||||
});
|
||||
});
|
||||
|
||||
test("buildReadOnlyCommand adds the completed-orders market filter", () => {
|
||||
const command = buildReadOnlyCommand("ordersCompleted", {
|
||||
market: "us"
|
||||
});
|
||||
|
||||
assert.deepEqual(command.args.slice(-4), [
|
||||
"orders",
|
||||
"completed",
|
||||
"--market",
|
||||
"us"
|
||||
]);
|
||||
});
|
||||
|
||||
test("public helpers execute a mock tossctl binary and parse its JSON output", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-"));
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
const logFile = path.join(tempDir, "invocation.json");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const script = `#!/bin/sh
|
||||
printf '%s\n' "$@" > "${logFile}"
|
||||
if [ "$7" = "account" ] && [ "$8" = "summary" ]; then
|
||||
printf '{"accountNo":"123-45","totalAssetAmount":1500000}\n'
|
||||
exit 0
|
||||
fi
|
||||
if [ "$3" = "quote" ] && [ "$4" = "get" ]; then
|
||||
printf '{"symbol":"%s","price":123.45}\n' "$5"
|
||||
exit 0
|
||||
fi
|
||||
printf '{"args":"%s"}\n' "$*"
|
||||
`;
|
||||
|
||||
const binPath = path.join(binDir, "tossctl");
|
||||
fs.writeFileSync(binPath, script, { mode: 0o755 });
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH || ""}`
|
||||
};
|
||||
const account = await getAccountSummary({
|
||||
configDir: "/tmp/toss-config",
|
||||
sessionFile: "/tmp/toss-session.json",
|
||||
env
|
||||
});
|
||||
const quote = await getQuote("005930", { env });
|
||||
|
||||
assert.equal(account.commandName, "accountSummary");
|
||||
assert.equal(account.data.totalAssetAmount, 1500000);
|
||||
assert.equal(quote.data.symbol, "005930");
|
||||
assert.match(fs.readFileSync(logFile, "utf8"), /quote|get/);
|
||||
});
|
||||
|
|
@ -809,3 +809,70 @@ test("fine-dust helper python regression tests pass", () => {
|
|||
`expected python fine-dust helper regression tests to pass\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("repository docs advertise the toss-securities skill across the documented surfaces", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
const roadmap = read(path.join("docs", "roadmap.md"));
|
||||
const sources = read(path.join("docs", "sources.md"));
|
||||
const featureDocPath = path.join(repoRoot, "docs", "features", "toss-securities.md");
|
||||
|
||||
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/toss-securities.md to exist");
|
||||
assert.match(readme, /\| 토스증권 조회 \|/);
|
||||
assert.match(readme, /\[토스증권 조회 가이드\]\(docs\/features\/toss-securities\.md\)/);
|
||||
assert.match(install, /--skill toss-securities/);
|
||||
assert.match(roadmap, /토스증권 조회 스킬 출시/);
|
||||
assert.match(sources, /tossinvest-cli: https:\/\/github\.com\/JungHoonGhae\/tossinvest-cli/);
|
||||
});
|
||||
|
||||
test("toss-securities skill documents the tossctl install, auth, and read-only workflow", () => {
|
||||
const skillPath = path.join(repoRoot, "toss-securities", "SKILL.md");
|
||||
|
||||
assert.ok(fs.existsSync(skillPath), "expected toss-securities/SKILL.md to exist");
|
||||
|
||||
const skill = read(path.join("toss-securities", "SKILL.md"));
|
||||
const featureDoc = read(path.join("docs", "features", "toss-securities.md"));
|
||||
|
||||
assert.match(skill, /^name: toss-securities$/m);
|
||||
|
||||
for (const doc of [skill, featureDoc]) {
|
||||
assert.match(doc, /tossctl/);
|
||||
assert.match(doc, /JungHoonGhae\/tossinvest-cli/);
|
||||
assert.match(doc, /auth login/);
|
||||
assert.match(doc, /account summary/);
|
||||
assert.match(doc, /portfolio positions/);
|
||||
assert.match(doc, /quote get/);
|
||||
assert.match(doc, /watchlist list/);
|
||||
assert.match(doc, /read-only|조회 전용/u);
|
||||
assert.doesNotMatch(doc, /order place/);
|
||||
}
|
||||
});
|
||||
|
||||
test("toss-securities package exposes safe read-only tossctl helpers", () => {
|
||||
const pkg = require(path.join(repoRoot, "packages", "toss-securities", "src", "index.js"));
|
||||
|
||||
assert.equal(typeof pkg.buildReadOnlyCommand, "function");
|
||||
assert.equal(typeof pkg.runReadOnlyCommand, "function");
|
||||
assert.equal(typeof pkg.getAccountSummary, "function");
|
||||
assert.equal(typeof pkg.getPortfolioPositions, "function");
|
||||
assert.equal(typeof pkg.getQuote, "function");
|
||||
assert.equal(typeof pkg.getQuoteBatch, "function");
|
||||
assert.equal(typeof pkg.listWatchlist, "function");
|
||||
});
|
||||
|
||||
test("toss-securities package README stays aligned with the read-only tossctl wrapper contract", () => {
|
||||
const packageReadme = read(path.join("packages", "toss-securities", "README.md"));
|
||||
|
||||
assert.match(packageReadme, /read-only tossctl wrapper/i);
|
||||
assert.match(packageReadme, /brew tap JungHoonGhae\/tossinvest-cli/);
|
||||
assert.match(packageReadme, /account summary/);
|
||||
assert.match(packageReadme, /quote get/);
|
||||
assert.match(packageReadme, /order place/);
|
||||
assert.match(packageReadme, /지원하지 않음|not supported/u);
|
||||
});
|
||||
|
||||
test("pack:dry-run includes the toss-securities workspace", () => {
|
||||
const packageJson = JSON.parse(read("package.json"));
|
||||
|
||||
assert.match(packageJson.scripts["pack:dry-run"], /workspace toss-securities/);
|
||||
});
|
||||
|
|
|
|||
106
toss-securities/SKILL.md
Normal file
106
toss-securities/SKILL.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
name: toss-securities
|
||||
description: 토스증권 조회형 질문에 대해 tossinvest-cli의 tossctl을 설치/로그인한 뒤 계좌 요약, 포트폴리오, 시세, 주문내역, 관심종목을 안전한 read-only 흐름으로 조회한다.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: finance
|
||||
locale: ko-KR
|
||||
phase: v1
|
||||
---
|
||||
|
||||
# Toss Securities
|
||||
|
||||
## What this skill does
|
||||
|
||||
`JungHoonGhae/tossinvest-cli` 의 `tossctl` 을 이용해 토스증권 **조회 전용(read-only)** 흐름을 실행한다.
|
||||
|
||||
- 계좌 목록 / 요약
|
||||
- 포트폴리오 보유 종목 / 비중
|
||||
- 단일 종목 / 다중 종목 시세
|
||||
- 미체결 주문 / 월간 체결 내역
|
||||
- 관심 종목
|
||||
|
||||
## When to use
|
||||
|
||||
- "토스증권 계좌 요약 보여줘"
|
||||
- "토스증권 TSLA 시세 확인해줘"
|
||||
- "관심종목 목록 보여줘"
|
||||
- "이번 달 체결 내역 조회해줘"
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS + Homebrew
|
||||
- `tossctl` 설치
|
||||
- `tossctl auth login` 으로 브라우저 세션 확보
|
||||
- Node.js 18+
|
||||
|
||||
## Workflow
|
||||
|
||||
### 0. Install `tossctl` first when missing
|
||||
|
||||
```bash
|
||||
brew tap JungHoonGhae/tossinvest-cli
|
||||
brew install tossctl
|
||||
tossctl doctor
|
||||
tossctl auth doctor
|
||||
tossctl auth login
|
||||
```
|
||||
|
||||
로그인 세션이 없으면 먼저 위 흐름을 끝낸다. 다른 비공식 크롤링이나 임의 HTTP 재구현으로 우회하지 않는다.
|
||||
|
||||
### 1. Prefer the read-only `tossctl` surface
|
||||
|
||||
지원하는 기본 명령:
|
||||
|
||||
- `tossctl account list --output json`
|
||||
- `tossctl account summary --output json`
|
||||
- `tossctl portfolio positions --output json`
|
||||
- `tossctl portfolio allocation --output json`
|
||||
- `tossctl quote get TSLA --output json`
|
||||
- `tossctl quote batch TSLA 005930 VOO --output json`
|
||||
- `tossctl orders list --output json`
|
||||
- `tossctl orders completed --market all --output json`
|
||||
- `tossctl watchlist list --output json`
|
||||
|
||||
### 2. Use the local package wrapper when scripting helps
|
||||
|
||||
```js
|
||||
const {
|
||||
getAccountSummary,
|
||||
getQuote,
|
||||
listWatchlist
|
||||
} = require("toss-securities");
|
||||
|
||||
async function main() {
|
||||
const summary = await getAccountSummary();
|
||||
const quote = await getQuote("TSLA");
|
||||
const watchlist = await listWatchlist();
|
||||
|
||||
console.log(summary.data);
|
||||
console.log(quote.data);
|
||||
console.log(watchlist.data);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Answer conservatively
|
||||
|
||||
- 계좌번호/민감정보는 꼭 필요한 범위만 노출한다.
|
||||
- 사용자가 "오늘" 같은 상대 날짜를 말하면 절대 날짜로 풀어 답한다.
|
||||
- 이 스킬은 조회 전용이다. 실거래 mutation 은 범위 밖이라고 분명히 말한다.
|
||||
|
||||
## Done when
|
||||
|
||||
- `tossctl` 설치/로그인 상태가 확인되었다.
|
||||
- 요청에 맞는 read-only 명령을 실행했다.
|
||||
- 결과를 한국어로 짧게 정리했다.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- `tossctl auth login` 전이면 계좌/포트폴리오 조회가 실패할 수 있다.
|
||||
- upstream 웹 API 구조가 바뀌면 `tossctl` 자체 업데이트가 필요할 수 있다.
|
||||
- 계좌/주문 정보는 민감하므로 출력 범위를 과도하게 넓히지 않는다.
|
||||
Loading…
Add table
Add a link
Reference in a new issue