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:
Jeffrey (Dongkyu) Kim 2026-03-30 11:22:55 +09:00
commit 2700e426a9
14 changed files with 699 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
"toss-securities": minor
---
Add the first safe read-only Toss Securities wrapper package and skill docs.

View file

@ -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)

View 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에 포함하지 않는다.

View file

@ -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 패키지

View file

@ -8,6 +8,7 @@
- KTX
- KBO 경기 결과
- K리그 경기 결과 조회 스킬 출시
- 토스증권 조회 스킬 출시
- 로또 당첨번호
- 서울 지하철 도착 정보
- 사용자 위치 미세먼지 조회 스킬 출시

View file

@ -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

View file

@ -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"

View 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 를 우회하지 않도록 래핑하지 않는다.

View 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"
}
}

View 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
};

View 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
};

View 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/);
});

View file

@ -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
View 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` 자체 업데이트가 필요할 수 있다.
- 계좌/주문 정보는 민감하므로 출력 범위를 과도하게 넓히지 않는다.