@mulsok/traders-client 0.1.0

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 (119) hide show
  1. package/README.md +103 -0
  2. package/bin/cli.js +160 -0
  3. package/bin/postinstall.js +57 -0
  4. package/bin/preuninstall.js +36 -0
  5. package/dist/server/broker/kiwoom/cache.js +86 -0
  6. package/dist/server/broker/kiwoom/cache.js.map +1 -0
  7. package/dist/server/broker/kiwoom/client.js +256 -0
  8. package/dist/server/broker/kiwoom/client.js.map +1 -0
  9. package/dist/server/broker/kiwoom/endpoints/_helpers.js +61 -0
  10. package/dist/server/broker/kiwoom/endpoints/_helpers.js.map +1 -0
  11. package/dist/server/broker/kiwoom/endpoints/account.js +448 -0
  12. package/dist/server/broker/kiwoom/endpoints/account.js.map +1 -0
  13. package/dist/server/broker/kiwoom/endpoints/detail.js +118 -0
  14. package/dist/server/broker/kiwoom/endpoints/detail.js.map +1 -0
  15. package/dist/server/broker/kiwoom/endpoints/investor.js +139 -0
  16. package/dist/server/broker/kiwoom/endpoints/investor.js.map +1 -0
  17. package/dist/server/broker/kiwoom/endpoints/order.js +134 -0
  18. package/dist/server/broker/kiwoom/endpoints/order.js.map +1 -0
  19. package/dist/server/broker/kiwoom/endpoints/quote.js +165 -0
  20. package/dist/server/broker/kiwoom/endpoints/quote.js.map +1 -0
  21. package/dist/server/broker/kiwoom/endpoints/ranking.js +180 -0
  22. package/dist/server/broker/kiwoom/endpoints/ranking.js.map +1 -0
  23. package/dist/server/broker/kiwoom/endpoints/sector.js +135 -0
  24. package/dist/server/broker/kiwoom/endpoints/sector.js.map +1 -0
  25. package/dist/server/broker/kiwoom/endpoints/theme.js +104 -0
  26. package/dist/server/broker/kiwoom/endpoints/theme.js.map +1 -0
  27. package/dist/server/broker/kiwoom/endpoints/universe.js +119 -0
  28. package/dist/server/broker/kiwoom/endpoints/universe.js.map +1 -0
  29. package/dist/server/broker/kiwoom/index.js +59 -0
  30. package/dist/server/broker/kiwoom/index.js.map +1 -0
  31. package/dist/server/broker/kiwoom/order-tracker.js +353 -0
  32. package/dist/server/broker/kiwoom/order-tracker.js.map +1 -0
  33. package/dist/server/broker/kiwoom/price-feed.js +119 -0
  34. package/dist/server/broker/kiwoom/price-feed.js.map +1 -0
  35. package/dist/server/broker/kiwoom/rate-limiter.js +97 -0
  36. package/dist/server/broker/kiwoom/rate-limiter.js.map +1 -0
  37. package/dist/server/broker/kiwoom/types.js +13 -0
  38. package/dist/server/broker/kiwoom/types.js.map +1 -0
  39. package/dist/server/broker/kiwoom/ws/client.js +370 -0
  40. package/dist/server/broker/kiwoom/ws/client.js.map +1 -0
  41. package/dist/server/broker/kiwoom/ws/endpoints/condition.js +146 -0
  42. package/dist/server/broker/kiwoom/ws/endpoints/condition.js.map +1 -0
  43. package/dist/server/broker/kiwoom/ws/realtime-bus.js +42 -0
  44. package/dist/server/broker/kiwoom/ws/realtime-bus.js.map +1 -0
  45. package/dist/server/broker/kiwoom/ws/types.js +19 -0
  46. package/dist/server/broker/kiwoom/ws/types.js.map +1 -0
  47. package/dist/server/broker/news.js +34 -0
  48. package/dist/server/broker/news.js.map +1 -0
  49. package/dist/server/bundle.js +43 -0
  50. package/dist/server/bundle.js.map +1 -0
  51. package/dist/server/calendar/krx-holidays.js +162 -0
  52. package/dist/server/calendar/krx-holidays.js.map +1 -0
  53. package/dist/server/config.js +263 -0
  54. package/dist/server/config.js.map +1 -0
  55. package/dist/server/db/sqlite.js +252 -0
  56. package/dist/server/db/sqlite.js.map +1 -0
  57. package/dist/server/diary/writer.js +266 -0
  58. package/dist/server/diary/writer.js.map +1 -0
  59. package/dist/server/index.js +316 -0
  60. package/dist/server/index.js.map +1 -0
  61. package/dist/server/jobs/universe-sync.js +132 -0
  62. package/dist/server/jobs/universe-sync.js.map +1 -0
  63. package/dist/server/jobs/watchdog.js +87 -0
  64. package/dist/server/jobs/watchdog.js.map +1 -0
  65. package/dist/server/journal/pnl-stats.js +108 -0
  66. package/dist/server/journal/pnl-stats.js.map +1 -0
  67. package/dist/server/journal/telemetry.js +174 -0
  68. package/dist/server/journal/telemetry.js.map +1 -0
  69. package/dist/server/journal/trade-journal.js +239 -0
  70. package/dist/server/journal/trade-journal.js.map +1 -0
  71. package/dist/server/llm/anthropic.js +98 -0
  72. package/dist/server/llm/anthropic.js.map +1 -0
  73. package/dist/server/llm/claude-cli.js +204 -0
  74. package/dist/server/llm/claude-cli.js.map +1 -0
  75. package/dist/server/llm/context-builder.js +229 -0
  76. package/dist/server/llm/context-builder.js.map +1 -0
  77. package/dist/server/llm/gemini.js +86 -0
  78. package/dist/server/llm/gemini.js.map +1 -0
  79. package/dist/server/llm/index.js +36 -0
  80. package/dist/server/llm/index.js.map +1 -0
  81. package/dist/server/llm/openai.js +87 -0
  82. package/dist/server/llm/openai.js.map +1 -0
  83. package/dist/server/personas/executor.js +318 -0
  84. package/dist/server/personas/executor.js.map +1 -0
  85. package/dist/server/personas/loader.js +165 -0
  86. package/dist/server/personas/loader.js.map +1 -0
  87. package/dist/server/personas/persona-agent.js +386 -0
  88. package/dist/server/personas/persona-agent.js.map +1 -0
  89. package/dist/server/personas/persona-state.js +170 -0
  90. package/dist/server/personas/persona-state.js.map +1 -0
  91. package/dist/server/personas/runner.js +162 -0
  92. package/dist/server/personas/runner.js.map +1 -0
  93. package/dist/server/personas/schema.js +123 -0
  94. package/dist/server/personas/schema.js.map +1 -0
  95. package/dist/server/personas/wake-plan.js +313 -0
  96. package/dist/server/personas/wake-plan.js.map +1 -0
  97. package/dist/server/readiness.js +414 -0
  98. package/dist/server/readiness.js.map +1 -0
  99. package/dist/server/routes.js +1216 -0
  100. package/dist/server/routes.js.map +1 -0
  101. package/dist/server/safety.js +153 -0
  102. package/dist/server/safety.js.map +1 -0
  103. package/dist/server/screener/engine.js +856 -0
  104. package/dist/server/screener/engine.js.map +1 -0
  105. package/dist/server/server-sync.js +427 -0
  106. package/dist/server/server-sync.js.map +1 -0
  107. package/dist/server/signing.js +39 -0
  108. package/dist/server/signing.js.map +1 -0
  109. package/dist/server/watchers/condition-watcher.js +519 -0
  110. package/dist/server/watchers/condition-watcher.js.map +1 -0
  111. package/dist/server/watchers/types.js +16 -0
  112. package/dist/server/watchers/types.js.map +1 -0
  113. package/dist/web/assets/index-62SMpbaf.js +79 -0
  114. package/dist/web/assets/index-BPLQR0wt.css +1 -0
  115. package/dist/web/index.html +14 -0
  116. package/package.json +93 -0
  117. package/scripts/com.mulsok.traders.client.plist.template +58 -0
  118. package/scripts/install-daemon.sh +156 -0
  119. package/scripts/uninstall-daemon.sh +62 -0
@@ -0,0 +1 @@
1
+ :root{--bg-stop-1: #DBEAFE;--bg-stop-2: #FCE7F3;--bg-stop-3: #DBF4F0;--bg-stop-4: #FEF3C7;--bg-base: #F8FAFC;--bg-mesh: radial-gradient(circle at 18% 22%, var(--bg-stop-1) 0%, transparent 38%), radial-gradient(circle at 82% 14%, var(--bg-stop-2) 0%, transparent 36%), radial-gradient(circle at 8% 80%, var(--bg-stop-3) 0%, transparent 38%), radial-gradient(circle at 90% 88%, var(--bg-stop-4) 0%, transparent 38%), var(--bg-base);--glass: rgba(255, 255, 255, .62);--glass-2: rgba(255, 255, 255, .78);--glass-3: rgba(255, 255, 255, .88);--glass-border: rgba(255, 255, 255, .95);--glass-line: rgba(15, 23, 41, .06);--glass-divider: rgba(15, 23, 41, .08);--ink-900: #0F172A;--ink-800: #1E293B;--ink-700: #334155;--ink-mute: #475569;--ink-dim: #94A3B8;--ink-faint: #CBD5E1;--accent: #6366F1;--accent-2: #EC4899;--accent-3: #818CF8;--accent-grad: linear-gradient(135deg, var(--accent), var(--accent-2));--accent-soft: rgba(99, 102, 241, .1);--accent-tint: rgba(99, 102, 241, .18);--gain: #10B981;--gain-soft: rgba(16, 185, 129, .18);--loss: #EF4444;--loss-soft: rgba(239, 68, 68, .1);--loss-tint: rgba(239, 68, 68, .18);--warn: #F59E0B;--warn-soft: rgba(245, 158, 11, .18);--info: var(--accent);--grad-purple: linear-gradient(135deg, #6366F1, #818CF8);--grad-pink: linear-gradient(135deg, #EC4899, #F472B6);--grad-green: linear-gradient(135deg, #10B981, #34D399);--grad-amber: linear-gradient(135deg, #F59E0B, #FBBF24);--grad-rose: linear-gradient(135deg, #F43F5E, #FB7185);--grad-sky: linear-gradient(135deg, #0EA5E9, #38BDF8);--persona-neoul: #0EA5E9;--persona-unknown: #94A3B8;--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Pretendard Variable", Pretendard, system-ui, sans-serif;--font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;--fs-hero: 44px;--fs-h1: 32px;--fs-h2: 22px;--fs-h3: 19px;--fs-lg: 17px;--fs-body: 15px;--fs-sm: 13px;--fs-xs: 12px;--fs-tiny: 11px;--fw-bold: 800;--fw-semi: 700;--fw-medium: 600;--fw-regular: 500;--sp-1: 4px;--sp-2: 8px;--sp-3: 12px;--sp-4: 16px;--sp-5: 22px;--sp-6: 30px;--sp-7: 44px;--sp-8: 64px;--r-xs: 6px;--r-sm: 10px;--r-md: 12px;--r-lg: 18px;--r-xl: 22px;--r-2xl: 24px;--r-3xl: 28px;--r-pill: 999px;--shadow-sm: 0 1px 3px rgba(15, 23, 41, .04);--shadow: 0 8px 32px rgba(15, 23, 41, .06), 0 2px 6px rgba(15, 23, 41, .04);--shadow-lg: 0 24px 60px rgba(15, 23, 41, .1), 0 4px 12px rgba(15, 23, 41, .05);--shadow-icon: 0 2px 6px rgba(15, 23, 41, .1);--shadow-icon-color-purple: 0 2px 8px rgba(99, 102, 241, .25);--shadow-icon-color-pink: 0 2px 8px rgba(236, 72, 153, .25);--shadow-icon-color-green: 0 2px 8px rgba(16, 185, 129, .25);--blur-sm: blur(12px) saturate(180%);--blur: blur(20px) saturate(180%);--blur-lg: blur(28px) saturate(180%);--tx-fast: all .15s ease;--tx: all .2s ease;--tx-slow: all .3s ease}*{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;background:var(--bg-mesh);background-attachment:fixed}html{font-size:16px}body{font-family:var(--font-sans);color:var(--ink-900);line-height:1.5;-webkit-font-smoothing:antialiased;letter-spacing:-.011em}button{font-family:inherit;cursor:pointer;border:0;background:transparent;color:inherit}input,select,textarea{font-family:inherit;color:inherit}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}hr{border:0;height:1px;background:var(--glass-line);margin:var(--sp-5) 0}.num{font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}.app-shell{min-height:100vh;display:flex;flex-direction:column}.app-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:12px var(--sp-4);max-width:1140px;width:calc(100% - var(--sp-7) * 2);margin:var(--sp-4) auto 0;background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-3xl);backdrop-filter:var(--blur-lg);-webkit-backdrop-filter:var(--blur-lg);box-shadow:var(--shadow);position:sticky;top:var(--sp-4);z-index:10}@media(max-width:720px){.app-bar{width:calc(100% - var(--sp-4) * 2)}}.brand{display:flex;align-items:center;gap:10px;padding-left:var(--sp-3);font-size:var(--fs-lg);font-weight:var(--fw-semi);letter-spacing:-.02em;color:var(--ink-900);text-decoration:none}.brand:hover{text-decoration:none}.brand-mark{width:34px;height:34px;border-radius:11px;background:var(--accent-grad);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:var(--fw-bold);font-size:16px;box-shadow:var(--shadow-icon-color-purple)}.brand-word{font-weight:var(--fw-semi)}.app-nav{display:flex;gap:4px;padding:0 var(--sp-2)}.app-nav button{padding:8px 16px;border-radius:var(--r-pill);font-size:var(--fs-sm);font-weight:var(--fw-medium);color:var(--ink-mute);transition:var(--tx)}.app-nav button:hover{background:#fff9;color:var(--ink-900)}.app-nav button.is-active{background:var(--ink-900);color:#fff;font-weight:var(--fw-semi);box-shadow:0 2px 6px #0f172926}.user-pill{display:flex;align-items:center;gap:var(--sp-2);padding:8px 14px;border-radius:var(--r-pill);background:var(--glass-2);font-size:var(--fs-sm);font-weight:var(--fw-medium);color:var(--ink-800);margin-right:var(--sp-2)}.user-pill .dot{width:7px;height:7px;border-radius:50%;background:var(--gain);box-shadow:0 0 0 3px #10b98133}.user-pill.is-warn .dot{background:var(--warn);box-shadow:0 0 0 3px var(--warn-soft)}.user-pill.is-halt .dot{background:var(--loss);box-shadow:0 0 0 3px var(--loss-soft)}.app-main{flex:1;max-width:1140px;width:100%;margin:0 auto;padding:var(--sp-6) var(--sp-7) var(--sp-7);display:flex;flex-direction:column;gap:var(--sp-5)}.glass{background:var(--glass-2);border:1px solid var(--glass-border);border-radius:var(--r-2xl);backdrop-filter:var(--blur);-webkit-backdrop-filter:var(--blur);box-shadow:var(--shadow)}.glass-lg{background:var(--glass-2);border:1px solid var(--glass-border);border-radius:var(--r-3xl);backdrop-filter:var(--blur-lg);-webkit-backdrop-filter:var(--blur-lg);box-shadow:var(--shadow-lg)}.glass-pad{padding:var(--sp-6)}.glass-pad-lg,.hero{padding:var(--sp-7)}.hero .pill{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:var(--r-pill);background:var(--accent-soft);color:var(--accent);font-size:var(--fs-xs);font-weight:var(--fw-medium);margin-bottom:var(--sp-4)}.hero h1{font-size:var(--fs-hero);font-weight:var(--fw-bold);letter-spacing:-.035em;line-height:1.1;color:var(--ink-900);margin-bottom:var(--sp-3)}.hero h1 .grad{background:var(--accent-grad);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.hero p{font-size:var(--fs-lg);color:var(--ink-mute);max-width:50ch}.dash-kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--sp-3);margin-bottom:var(--sp-5)}.dash-kpi{padding:var(--sp-4);background:var(--glass);border:1px solid var(--border);border-radius:var(--r-md, 12px);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:grid;grid-template-columns:36px 1fr;gap:var(--sp-3);align-items:start;transition:border-color .12s}.dash-kpi:hover{border-color:var(--ink-300)}.dash-kpi-icon{font-size:28px;line-height:1;display:flex;align-items:center;justify-content:center;width:36px;height:36px;background:var(--surface, rgba(0, 0, 0, .04));border-radius:10px;flex-shrink:0}.dash-kpi-body{display:flex;flex-direction:column;gap:2px;min-width:0}.dash-kpi-label{font-size:var(--fs-tiny);color:var(--ink-500);font-weight:600;letter-spacing:.04em}.dash-kpi-value{font-size:var(--fs-h2);font-weight:700;color:var(--ink-900);line-height:1.15;letter-spacing:-.4px;font-variant-numeric:tabular-nums}.dash-kpi-value-sm{font-size:var(--fs-body);font-weight:600;color:var(--ink-700);margin-top:2px}.dash-kpi-sub{font-size:var(--fs-tiny);color:var(--ink-500);margin-top:2px}.dash-kpi-pnl.is-gain{border-color:color-mix(in oklab,var(--gain) 35%,var(--border));background:color-mix(in oklab,var(--gain) 6%,var(--glass))}.dash-kpi-pnl.is-loss{border-color:color-mix(in oklab,var(--loss) 35%,var(--border));background:color-mix(in oklab,var(--loss) 6%,var(--glass))}.dash-kpi-value.is-gain{color:var(--gain)}.dash-kpi-value.is-loss{color:var(--loss)}.wake-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--sp-3);margin-top:var(--sp-3)}.wake-card{background:var(--glass);border:1px solid var(--border);border-radius:var(--r-md);padding:var(--sp-4);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.wake-card-head{display:flex;justify-content:space-between;align-items:baseline;padding-bottom:var(--sp-3);margin-bottom:var(--sp-3);border-bottom:1px solid var(--border)}.wake-persona{font-size:var(--fs-h3);font-weight:700;color:var(--ink-900);letter-spacing:-.3px}.wake-count{font-size:var(--fs-tiny);font-weight:600;color:var(--ink-500);background:var(--accent-soft);padding:2px 10px;border-radius:999px;font-variant-numeric:tabular-nums}.wake-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:var(--sp-3)}.wake-row{display:grid;grid-template-columns:auto 1fr auto;gap:var(--sp-3);align-items:start}.wake-when{font-size:var(--fs-h3);font-weight:700;color:var(--ink-900);font-variant-numeric:tabular-nums;letter-spacing:-.3px;white-space:nowrap;line-height:1.3}.wake-intent{font-size:var(--fs-body);color:var(--ink-700);line-height:1.5;word-break:keep-all;align-self:center}.wake-type{font-size:var(--fs-tiny);font-weight:600;padding:2px 10px;border-radius:999px;white-space:nowrap;letter-spacing:.04em;align-self:center;border:1px solid transparent}.wake-type-time{background:var(--accent-soft);color:var(--accent);border-color:color-mix(in oklab,var(--accent) 25%,transparent)}.wake-type-price{background:color-mix(in oklab,var(--warn) 14%,transparent);color:var(--warn);border-color:color-mix(in oklab,var(--warn) 30%,transparent)}.wake-type-event{background:var(--glass);color:var(--ink-700);border-color:var(--border)}.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-4)}.stat-card{padding:var(--sp-4) var(--sp-5);display:grid;grid-template-columns:40px 1fr;gap:var(--sp-3);align-items:center;position:relative;overflow:hidden}.stat-card .icon{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:var(--shadow-icon)}.stat-card .icon.purple{background:var(--grad-purple);color:#fff}.stat-card .icon.pink{background:var(--grad-pink);color:#fff}.stat-card .icon.green{background:var(--grad-green);color:#fff}.stat-card .icon.amber{background:var(--grad-amber);color:#fff}.stat-card .icon.rose{background:var(--grad-rose);color:#fff}.stat-card .icon.sky{background:var(--grad-sky);color:#fff}.stat-card .v{font-size:24px;font-weight:var(--fw-bold);letter-spacing:-.025em;color:var(--ink-900);line-height:1.05}.stat-card .v.gain{color:var(--gain)}.stat-card .v.loss{color:var(--loss)}.stat-card .v.warn{color:var(--warn)}.stat-card .k{font-size:var(--fs-xs);color:var(--ink-mute);font-weight:var(--fw-medium);margin-top:2px}.stat-card .stat-body{display:flex;flex-direction:column;min-width:0}.stat-card.is-clickable{cursor:pointer;border:1px solid var(--glass-line);text-align:left;font:inherit;width:100%;transition:var(--tx-fast)}.stat-card.is-clickable:hover{transform:translateY(-2px);box-shadow:var(--shadow)}.stat-card.is-clickable:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.stat-card.is-clickable:after{content:"↗";position:absolute;top:8px;right:10px;font-size:12px;color:var(--ink-dim);opacity:0;transition:opacity .15s;pointer-events:none}.stat-card.is-clickable:hover:after,.stat-card.is-clickable:focus-visible:after{opacity:1}.section-head{display:flex;align-items:baseline;justify-content:space-between;padding:0 var(--sp-3);margin-bottom:var(--sp-4)}.section-head h2{font-size:var(--fs-h2);font-weight:var(--fw-semi);letter-spacing:-.025em}.section-head .meta{font-size:var(--fs-sm);color:var(--ink-mute);font-weight:var(--fw-medium)}.persona-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--sp-4)}@media(min-width:1280px){.persona-grid{grid-template-columns:repeat(3,1fr)}}.trader-card{padding:var(--sp-5);display:flex;flex-direction:column;gap:var(--sp-3);cursor:pointer;transition:var(--tx-fast);border:1px solid var(--glass-line);text-align:left;font:inherit;width:100%;background:var(--glass-2)}.trader-card:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:var(--ink-faint)}.trader-card:focus-visible{outline:2px solid var(--persona-color, var(--accent));outline-offset:2px}.trader-card .head{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding-bottom:var(--sp-3);border-bottom:1px solid var(--glass-line)}.trader-card .name-row{display:flex;align-items:center;gap:8px;min-width:0}.trader-card .name-row:before{content:"";width:8px;height:8px;border-radius:50%;background:var(--persona-color, var(--accent));flex-shrink:0}.trader-card .name{font-size:var(--fs-h2);font-weight:var(--fw-semi);letter-spacing:-.02em;color:var(--ink-900)}.trader-card .ver{font-family:var(--font-mono);font-size:var(--fs-tiny);color:var(--ink-dim)}.trader-card .display-name{font-size:var(--fs-sm);color:var(--ink-mute);font-weight:var(--fw-medium);line-height:1.4}.trader-card .style-chips{display:flex;flex-wrap:wrap;gap:4px}.trader-card .style-chip{font-size:var(--fs-tiny);color:var(--ink-700);background:#fff;border:1px solid var(--glass-line);padding:2px 8px;border-radius:var(--r-pill);font-weight:var(--fw-medium)}.trader-card .stats{display:flex;flex-wrap:wrap;gap:4px var(--sp-3);align-items:baseline;padding-top:var(--sp-3);margin-top:auto;border-top:1px solid var(--glass-line);font-size:var(--fs-sm);color:var(--ink-700)}.trader-card .stats .sep{color:var(--ink-faint)}.trader-card .stats .stat-k{color:var(--ink-mute);font-size:var(--fs-xs)}.trader-card .stats .stat-v{color:var(--ink-900);font-weight:var(--fw-semi);font-variant-numeric:tabular-nums}.trader-card .stats .stat-v.sm{font-weight:var(--fw-medium);color:var(--ink-700);font-size:var(--fs-xs)}.verdict-pill{font-size:var(--fs-tiny);font-weight:var(--fw-semi);padding:4px 10px;border-radius:var(--r-pill);letter-spacing:.02em;display:inline-block;white-space:nowrap}.verdict-pill.hold{background:#94a3b82e;color:var(--ink-mute)}.verdict-pill.buy{background:var(--gain-soft);color:var(--gain)}.verdict-pill.sell{background:var(--loss-soft);color:var(--loss)}.verdict-pill.skip{background:var(--accent-soft);color:var(--accent)}.diary{background:#0f172906;border:1px solid var(--glass-line);border-radius:var(--r-2xl);padding:var(--sp-6);position:relative}.diary .head{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-4)}.diary .avatar{width:40px;height:40px;border-radius:14px;background:var(--accent-grad);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:var(--fw-semi);font-size:var(--fs-body)}.diary .who{font-weight:var(--fw-semi);font-size:var(--fs-body)}.diary .when{font-size:var(--fs-xs);color:var(--ink-mute)}.diary .body{font-size:var(--fs-lg);line-height:1.75;color:var(--ink-800);white-space:pre-wrap}.diary .body.empty{color:var(--ink-mute);font-style:italic}.sched{border-radius:var(--r-2xl);overflow:hidden}.sched .row{display:grid;grid-template-columns:110px 1fr 100px;gap:var(--sp-4);padding:var(--sp-4) var(--sp-5);border-bottom:1px solid var(--glass-line);align-items:center}.sched .row:last-child{border-bottom:0}.sched .when{font-family:var(--font-mono);font-size:var(--fs-sm);font-weight:var(--fw-semi);color:var(--accent)}.sched .what{font-size:var(--fs-body);color:var(--ink-900)}.sched .type{font-size:var(--fs-tiny);padding:4px 10px;border-radius:var(--r-pill);background:#0f17290f;color:var(--ink-mute);font-weight:var(--fw-medium);text-align:center;justify-self:end}.btn{padding:12px var(--sp-5);border-radius:var(--r-md);border:1px solid var(--glass-border);background:var(--glass-2);color:var(--ink-900);font-size:var(--fs-sm);font-weight:var(--fw-semi);display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);backdrop-filter:var(--blur);-webkit-backdrop-filter:var(--blur);box-shadow:var(--shadow-sm);transition:var(--tx)}.btn:hover{transform:translateY(-1px);box-shadow:var(--shadow);border-color:var(--ink-faint)}.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.btn.primary{background:var(--ink-900);color:#fff;border-color:var(--ink-900)}.btn.primary:hover{background:var(--accent);border-color:var(--accent)}.btn.danger{background:var(--loss-soft);color:var(--loss);border-color:#ef444433}.btn.danger:hover{background:var(--loss-tint);border-color:#ef444466}.btn.ghost{background:transparent;border-color:var(--glass-line);box-shadow:none;color:var(--ink-mute)}.btn.ghost:hover{color:var(--ink-900);background:var(--glass)}.btn.full{width:100%}.btn.lg{padding:14px var(--sp-6);font-size:var(--fs-body);border-radius:var(--r-xl)}.btn.sm{padding:6px 12px;font-size:var(--fs-xs);border-radius:var(--r-sm)}.btn-row{display:flex;gap:var(--sp-3)}.btn-row .btn{flex:1}.field{display:flex;flex-direction:column;gap:var(--sp-2);margin-bottom:var(--sp-5)}.field:last-child{margin-bottom:0}.field-label{font-size:var(--fs-sm);font-weight:var(--fw-semi);color:var(--ink-700);letter-spacing:-.005em}.field-hint{font-size:var(--fs-xs);color:var(--ink-mute);line-height:1.5}.field input[type=text],.field input[type=number],.field input[type=password],.field select,.field textarea{padding:12px var(--sp-4);border-radius:var(--r-md);border:1px solid var(--glass-line);background:var(--glass-3);font-size:var(--fs-body);width:100%;outline:none;transition:var(--tx-fast);line-height:1.5}.field input:hover,.field select:hover,.field textarea:hover{border-color:var(--ink-faint)}.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:#fff}.field input:disabled,.field select:disabled,.field textarea:disabled{background:var(--glass);color:var(--ink-dim);cursor:not-allowed;border-color:var(--glass-line)}.field-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-4)}.field-group{background:var(--glass);border:1px solid var(--glass-line);border-radius:var(--r-lg);padding:var(--sp-5);margin-bottom:var(--sp-5)}.field-group:last-child{margin-bottom:0}.field-group .group-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--sp-4)}.field-group .group-title{font-size:var(--fs-sm);font-weight:var(--fw-semi);color:var(--ink-700);letter-spacing:.02em;text-transform:uppercase}.field-group .group-meta{font-size:var(--fs-tiny);color:var(--ink-mute)}.toggle{position:relative;display:inline-block;width:48px;height:28px;flex-shrink:0}.toggle input{opacity:0;width:0;height:0}.toggle .slider{position:absolute;top:0;right:0;bottom:0;left:0;background:#0f172926;border-radius:var(--r-pill);transition:var(--tx);cursor:pointer}.toggle .slider:before{content:"";position:absolute;width:22px;height:22px;left:3px;top:3px;background:#fff;border-radius:50%;box-shadow:0 2px 4px #0f172933;transition:var(--tx)}.toggle input:checked+.slider{background:var(--gain)}.toggle input:checked+.slider:before{transform:translate(20px)}.toggle.danger input:checked+.slider{background:var(--loss)}.toggle-row{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) var(--sp-5);background:var(--glass);border:1px solid var(--glass-line);border-radius:var(--r-lg)}.toggle-row .label{font-size:var(--fs-body);font-weight:var(--fw-medium);color:var(--ink-900)}.toggle-row .label-meta{font-size:var(--fs-xs);color:var(--ink-mute);margin-top:2px;font-weight:var(--fw-regular)}.setup-grid{display:grid;grid-template-columns:240px 1fr;gap:var(--sp-5)}.setup-nav{padding:var(--sp-3);display:flex;flex-direction:column;gap:6px;align-self:start;position:sticky;top:110px}.setup-nav button{text-align:left;padding:var(--sp-3) var(--sp-4);border-radius:var(--r-md);font-size:var(--fs-sm);color:var(--ink-mute);font-weight:var(--fw-medium);display:flex;align-items:center;gap:var(--sp-3);transition:var(--tx-fast);position:relative}.setup-nav button:hover{background:#ffffff80;color:var(--ink-900)}.setup-nav button.is-active{background:var(--ink-900);color:#fff}.setup-nav button.is-active .nav-meta{color:#ffffffb3}.setup-nav button .nav-icon{font-size:18px;width:24px;text-align:center;flex-shrink:0}.setup-nav button .nav-text{display:flex;flex-direction:column;gap:2px;flex:1}.setup-nav button .nav-meta{font-size:var(--fs-tiny);color:var(--ink-dim);font-weight:var(--fw-regular)}.setup-section{padding:var(--sp-6) var(--sp-6) 0;display:flex;flex-direction:column}.setup-section h2{font-size:var(--fs-h2);font-weight:var(--fw-semi);letter-spacing:-.025em;margin-bottom:var(--sp-3)}.setup-section .lead{font-size:var(--fs-sm);color:var(--ink-mute);margin-bottom:var(--sp-7);line-height:1.6;max-width:60ch}.setup-form{max-width:640px;width:100%;margin:0 auto;padding-bottom:var(--sp-6)}.save-bar{position:sticky;bottom:0;margin:0 calc(var(--sp-6) * -1);padding:var(--sp-4) var(--sp-6);background:linear-gradient(180deg,transparent 0%,rgba(255,255,255,.85) 30%,var(--glass-3) 100%);backdrop-filter:var(--blur);-webkit-backdrop-filter:var(--blur);border-top:1px solid var(--glass-line);display:flex;gap:var(--sp-3);align-items:center;z-index:5;border-radius:0 0 var(--r-3xl) var(--r-3xl)}.save-bar .spacer{flex:1}.setup-section .banner{position:sticky;top:110px;z-index:4;margin-bottom:var(--sp-5);box-shadow:var(--shadow)}.banner{padding:var(--sp-4) var(--sp-5);border-radius:var(--r-xl);display:flex;align-items:center;gap:var(--sp-3);font-size:var(--fs-sm)}.banner.success{background:var(--gain-soft);color:var(--gain)}.banner.warn{background:var(--warn-soft);color:var(--warn)}.banner.danger{background:var(--loss-soft);color:var(--loss)}.banner.info{background:var(--accent-soft);color:var(--accent)}.empty{padding:var(--sp-8);text-align:center;color:var(--ink-mute);font-size:var(--fs-sm)}.journal-grid{display:grid;grid-template-columns:380px 1fr;gap:var(--sp-4);align-items:start;max-width:1100px}.journal-list{display:flex;flex-direction:column;gap:6px;padding:var(--sp-3)}.pagination{display:flex;align-items:center;justify-content:space-between;padding:var(--sp-3) var(--sp-4);margin-top:var(--sp-2);border-top:1px solid var(--glass-line);font-size:var(--fs-xs);color:var(--ink-mute)}.pagination .pages{display:flex;gap:4px;align-items:center}.pagination .page-btn{padding:6px 10px;border-radius:var(--r-sm);border:1px solid transparent;background:transparent;color:var(--ink-mute);font-size:var(--fs-xs);font-weight:var(--fw-medium);cursor:pointer;transition:var(--tx-fast);min-width:28px}.pagination .page-btn:hover:not(:disabled){background:var(--glass);color:var(--ink-900)}.pagination .page-btn:disabled{opacity:.35;cursor:not-allowed}.pagination .page-num{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-mute);padding:0 var(--sp-2)}.pagination .page-num strong{color:var(--ink-900);font-weight:var(--fw-semi)}.journal-list .item{padding:var(--sp-3) var(--sp-4);border-radius:var(--r-md);border:1px solid transparent;background:var(--glass);cursor:pointer;display:flex;flex-direction:column;gap:4px;transition:var(--tx-fast);text-align:left;width:100%}.journal-list .item:hover{background:var(--glass-2);border-color:var(--glass-line)}.journal-list .item.is-active{background:var(--ink-900);color:#fff;border-color:var(--ink-900)}.journal-list .item.is-active .ev-meta{color:#ffffffb3}.journal-list .item.is-active .ev-summary{color:#fff}.journal-list .item.is-active .ev-time{color:#ffffffb3}.journal-list .item .ev-row{display:flex;justify-content:space-between;align-items:center;gap:var(--sp-2)}.journal-list .item .ev-time{font-family:var(--font-mono);font-size:var(--fs-tiny);color:var(--ink-mute)}.journal-list .item .ev-summary{font-size:var(--fs-sm);color:var(--ink-900);line-height:1.4}.journal-list .item .ev-meta{font-size:var(--fs-tiny);color:var(--ink-dim)}.journal-detail{padding:var(--sp-5) var(--sp-6)}.journal-detail h3{font-size:var(--fs-h2);font-weight:var(--fw-semi);margin-bottom:var(--sp-3);letter-spacing:-.025em}.journal-detail .reasoning{font-size:var(--fs-body);line-height:1.7;color:var(--ink-800);white-space:pre-wrap;background:var(--glass);border:1px solid var(--glass-line);padding:var(--sp-4) var(--sp-5);border-radius:var(--r-md);margin-bottom:var(--sp-4)}.journal-detail .reasoning.markdown{white-space:normal}.journal-detail .reasoning.markdown>*:first-child{margin-top:0}.journal-detail .reasoning.markdown>*:last-child{margin-bottom:0}.journal-detail .reasoning.markdown h1,.journal-detail .reasoning.markdown h2,.journal-detail .reasoning.markdown h3,.journal-detail .reasoning.markdown h4{font-weight:var(--fw-semi);letter-spacing:-.02em;color:var(--ink-900);margin:var(--sp-5) 0 var(--sp-3);line-height:1.3}.journal-detail .reasoning.markdown h1{font-size:var(--fs-h2)}.journal-detail .reasoning.markdown h2{font-size:calc(var(--fs-h3) * 1.05)}.journal-detail .reasoning.markdown h3{font-size:var(--fs-h3)}.journal-detail .reasoning.markdown h4{font-size:var(--fs-body-lg)}.journal-detail .reasoning.markdown p{margin:0 0 var(--sp-3)}.journal-detail .reasoning.markdown strong{font-weight:var(--fw-semi);color:var(--ink-900)}.journal-detail .reasoning.markdown em{font-style:italic;color:var(--ink-700)}.journal-detail .reasoning.markdown ul,.journal-detail .reasoning.markdown ol{margin:0 0 var(--sp-3);padding-left:var(--sp-5)}.journal-detail .reasoning.markdown li{margin-bottom:var(--sp-2)}.journal-detail .reasoning.markdown li>p{margin-bottom:var(--sp-1)}.journal-detail .reasoning.markdown a{color:var(--accent);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}.journal-detail .reasoning.markdown code{font-family:var(--font-mono);font-size:.9em;background:var(--glass-strong, rgba(0,0,0,.05));padding:.1em .35em;border-radius:var(--r-sm);color:var(--ink-900)}.journal-detail .reasoning.markdown pre{font-family:var(--font-mono);font-size:var(--fs-sm);background:var(--glass-strong, rgba(0,0,0,.05));border:1px solid var(--glass-line);border-radius:var(--r-md);padding:var(--sp-3) var(--sp-4);margin:0 0 var(--sp-3);overflow-x:auto;line-height:1.55}.journal-detail .reasoning.markdown pre code{background:transparent;padding:0;border-radius:0}.journal-detail .reasoning.markdown blockquote{margin:0 0 var(--sp-3);padding:var(--sp-2) var(--sp-4);border-left:3px solid var(--accent);background:var(--glass);border-radius:0 var(--r-sm) var(--r-sm) 0;color:var(--ink-700)}.journal-detail .reasoning.markdown hr{border:0;border-top:1px solid var(--glass-line);margin:var(--sp-4) 0}.journal-detail .reasoning.markdown table{width:100%;border-collapse:collapse;margin:0 0 var(--sp-4);font-size:var(--fs-sm);font-variant-numeric:tabular-nums;display:block;overflow-x:auto}.journal-detail .reasoning.markdown thead{background:var(--glass);border-bottom:2px solid var(--glass-line)}.journal-detail .reasoning.markdown th,.journal-detail .reasoning.markdown td{text-align:left;padding:var(--sp-2) var(--sp-3);border-bottom:1px solid var(--glass-line);vertical-align:top}.journal-detail .reasoning.markdown th{font-weight:var(--fw-semi);color:var(--ink-900);white-space:nowrap}.journal-detail .reasoning.markdown tbody tr:hover{background:var(--glass)}.journal-detail .reasoning.markdown tbody tr:last-child td{border-bottom:0}.kv-list{display:grid;grid-template-columns:max-content 1fr;gap:var(--sp-2) var(--sp-4);font-size:var(--fs-sm)}.kv-list dt{color:var(--ink-mute);font-weight:var(--fw-medium);white-space:nowrap}.kv-list dd{color:var(--ink-900);font-weight:var(--fw-medium);word-break:break-word;min-width:0}.filter-bar{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap}.filter-bar select,.filter-bar input{padding:8px var(--sp-3);border-radius:var(--r-sm);border:1px solid var(--glass-line);background:var(--glass-2);font-size:var(--fs-sm);outline:none;cursor:pointer;transition:var(--tx-fast)}.filter-bar select:hover,.filter-bar input:hover{border-color:var(--ink-faint);background:var(--glass-3)}.filter-bar select:focus,.filter-bar input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.alloc-list,.sub-list{display:flex;flex-direction:column;gap:var(--sp-3)}.alloc-row{display:grid;grid-template-columns:100px 1fr 200px auto;gap:var(--sp-4);padding:var(--sp-4) var(--sp-5);border-radius:var(--r-lg);background:var(--glass);border:1px solid var(--glass-line);align-items:center}.alloc-row .slug{font-size:var(--fs-body);font-weight:var(--fw-semi)}.alloc-row .name{font-size:var(--fs-sm);color:var(--ink-mute)}.alloc-row .name strong{color:var(--ink-900);font-weight:var(--fw-semi)}.alloc-row input{padding:10px var(--sp-3);border-radius:var(--r-sm);border:1px solid var(--glass-line);background:#fff;font-family:var(--font-mono);text-align:right;font-size:var(--fs-sm)}.alloc-row input:focus{border-color:var(--accent);outline:0;box-shadow:0 0 0 3px var(--accent-soft)}.alloc-row .actions{display:flex;gap:var(--sp-2)}.sub-row{display:grid;grid-template-columns:48px 1fr auto;gap:var(--sp-4);padding:var(--sp-4) var(--sp-5);border-radius:var(--r-lg);background:var(--glass);border:1px solid var(--glass-line);align-items:center}.sub-row .avatar{width:36px;height:36px;border-radius:11px;background:var(--accent-grad);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:var(--fw-semi);font-size:var(--fs-body)}.sub-row .info{display:flex;flex-direction:column;gap:2px;min-width:0}.sub-row .info .name{font-size:var(--fs-body);font-weight:var(--fw-semi);color:var(--ink-900);display:flex;align-items:baseline;gap:var(--sp-2)}.sub-row .info .ver{font-family:var(--font-mono);font-size:var(--fs-tiny);color:var(--ink-dim);font-weight:var(--fw-regular)}.sub-row .info .meta{font-size:var(--fs-xs);color:var(--ink-mute)}.sub-row .status{font-size:var(--fs-tiny);font-weight:var(--fw-semi);padding:4px 10px;border-radius:var(--r-pill);background:var(--gain-soft);color:var(--gain);letter-spacing:.02em}.sub-row .status.idle{background:#94a3b82e;color:var(--ink-mute)}.step-list{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--sp-3)}.step-chip{padding:var(--sp-4);display:flex;flex-direction:column;gap:6px}.step-chip .icon{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:var(--fs-sm);font-weight:var(--fw-bold)}.step-chip .icon.is-ok{background:var(--gain-soft);color:var(--gain)}.step-chip .icon.is-fail{background:var(--loss-soft);color:var(--loss)}.step-chip .label{font-size:var(--fs-sm);font-weight:var(--fw-semi);color:var(--ink-900)}.step-chip .detail{font-size:var(--fs-tiny);color:var(--ink-mute);line-height:1.4}.disclaimer{margin-top:var(--sp-5);padding:var(--sp-4) var(--sp-5);font-size:var(--fs-tiny);color:var(--ink-mute);line-height:1.7;text-align:center}.dialog-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#0f172966;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index:100;display:flex;align-items:center;justify-content:center;padding:var(--sp-4)}.dialog{background:var(--glass-3);border:1px solid var(--glass-border);border-radius:var(--r-3xl);padding:var(--sp-7);max-width:800px;width:100%;max-height:80vh;overflow-y:auto;backdrop-filter:var(--blur-lg);-webkit-backdrop-filter:var(--blur-lg);box-shadow:var(--shadow-lg);position:relative}.dialog .close{position:absolute;top:var(--sp-4);right:var(--sp-4);width:32px;height:32px;border-radius:50%;background:var(--glass-2);display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--ink-mute)}.dialog .close:hover{color:var(--ink-900);background:#fff}.metric-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-3);background:#0f172908;border-radius:var(--r-lg);padding:var(--sp-4);margin-bottom:var(--sp-4)}.metric-grid .item{display:flex;justify-content:space-between;font-size:var(--fs-sm);padding:var(--sp-2) 0;border-bottom:1px solid var(--glass-line)}.metric-grid .item:nth-last-child(-n+2){border-bottom:0}.metric-grid .item .k{color:var(--ink-mute)}.metric-grid .item .v{color:var(--ink-900);font-weight:var(--fw-semi)}.dialog-section{margin-bottom:var(--sp-5)}.dialog-section+.dialog-section{padding-top:var(--sp-4);border-top:1px solid var(--glass-line)}.dialog-section h3{font-size:var(--fs-h3);font-weight:var(--fw-semi);margin-bottom:var(--sp-3);letter-spacing:-.02em}.dialog-section .sub{font-size:var(--fs-sm);color:var(--ink-mute);margin-bottom:var(--sp-3)}.persona-chip{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-sm);font-weight:var(--fw-medium);color:var(--ink-800)}.persona-chip .dot{width:8px;height:8px;border-radius:50%;background:var(--persona-color, var(--persona-unknown));box-shadow:0 0 0 2px var(--persona-color-soft, transparent)}.bar-row{display:grid;grid-template-columns:90px 1fr 70px;gap:var(--sp-3);align-items:center;padding:var(--sp-2) 0}.bar-row .bar-label{font-size:var(--fs-sm);color:var(--ink-700);font-weight:var(--fw-medium);display:flex;align-items:center;gap:6px;min-width:0}.bar-row .bar-label .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.bar-row .bar-label .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bar-row .bar-track{height:8px;background:var(--glass);border-radius:var(--r-pill);overflow:hidden;position:relative}.bar-row .bar-fill{height:100%;border-radius:var(--r-pill);transition:width .3s ease;background:var(--bar-color, var(--accent))}.bar-row .bar-value{text-align:right;font-size:var(--fs-sm);color:var(--ink-900);font-weight:var(--fw-semi);font-variant-numeric:tabular-nums}.mini-chart{background:var(--glass);border:1px solid var(--glass-line);border-radius:var(--r-md);padding:var(--sp-4);margin-bottom:var(--sp-3)}.mini-chart svg{width:100%;height:auto;display:block}.mini-chart .axis{fill:var(--ink-dim);font-size:10px;font-family:var(--font-mono)}.watcher-group{margin-bottom:var(--sp-4)}.watcher-group .group-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--sp-2)}.watcher-group .count{font-size:var(--fs-xs);color:var(--ink-mute);font-weight:var(--fw-medium)}.watcher-row{display:grid;grid-template-columns:1fr auto;gap:var(--sp-3);padding:var(--sp-3);background:var(--glass);border:1px solid var(--glass-line);border-radius:var(--r-sm);margin-bottom:6px;align-items:center}.watcher-row .intent{font-size:var(--fs-sm);color:var(--ink-900);font-weight:var(--fw-medium);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.watcher-row .symbol{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-mute);margin-top:2px}.watcher-row .status{font-size:var(--fs-xs);font-weight:var(--fw-medium);padding:2px 8px;border-radius:var(--r-pill);background:var(--gain-soft);color:var(--gain)}.watcher-row .status.expired{background:var(--glass);color:var(--ink-dim)}.watcher-row .status.triggered{background:var(--accent-soft);color:var(--accent)}.watcher-row .status.revoked{background:var(--loss-soft);color:var(--loss)}.matrix-table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--fs-sm)}.matrix-table th,.matrix-table td{padding:var(--sp-2) var(--sp-3);text-align:right;font-variant-numeric:tabular-nums}.matrix-table thead th{font-weight:var(--fw-medium);color:var(--ink-mute);font-size:var(--fs-xs);border-bottom:1px solid var(--glass-line)}.matrix-table tbody td{color:var(--ink-800);border-bottom:1px solid var(--glass-line)}.matrix-table tbody tr:last-child td{border-bottom:0}.matrix-table .row-head{text-align:left;font-weight:var(--fw-medium);color:var(--ink-900)}.matrix-table .total{font-weight:var(--fw-semi);color:var(--ink-900)}.matrix-table .zero{color:var(--ink-faint)}.dialog.trader-detail{background:#fff}.trader-detail .detail-head{margin-bottom:var(--sp-5);display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-4)}.trader-detail .detail-title{min-width:0}.trader-detail .detail-title h2{font-size:var(--fs-h1);font-weight:var(--fw-bold);letter-spacing:-.02em;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}.trader-detail .detail-title h2 .ver{font-family:var(--font-mono);font-size:var(--fs-sm);color:var(--ink-dim);font-weight:var(--fw-regular)}.trader-detail .detail-title .tagline{color:var(--ink-mute);font-size:var(--fs-sm);font-style:italic;margin-top:4px;line-height:1.4}.trader-detail .detail-title .funnel{margin-top:6px;font-size:var(--fs-xs);color:var(--ink-dim);font-variant-numeric:tabular-nums}.trader-detail .detail-title .funnel .arrow{color:var(--ink-faint);margin:0 4px}.trader-detail .candidate-bar{position:sticky;top:calc(var(--sp-7) * -1);margin:0 calc(var(--sp-7) * -1);padding:var(--sp-3) var(--sp-7);background:transparent;border-bottom:1px solid var(--glass-line);z-index:1;display:flex;flex-direction:row;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-4)}.trader-detail .candidate-bar .filter-bar{flex:1;min-width:0}.trader-detail .candidate-bar .pager{flex-shrink:0;display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-xs);color:var(--ink-mute)}.trader-detail .candidate-bar .pager .count{font-variant-numeric:tabular-nums;white-space:nowrap}.trader-detail .candidate-bar:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:#fff;z-index:-1}.trader-detail .cand-head{margin-bottom:var(--sp-4)}.trader-detail .cand-head .row{display:flex;align-items:baseline;gap:var(--sp-3);flex-wrap:wrap;margin-bottom:6px}.trader-detail .cand-head h3{font-size:var(--fs-h2);font-weight:var(--fw-semi);letter-spacing:-.02em}.trader-detail .cand-head .code{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-mute)}.trader-detail .cand-head .quote{display:flex;gap:var(--sp-4);font-size:var(--fs-sm);flex-wrap:wrap;color:var(--ink-700)}.pattern-chip{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-xs);font-weight:var(--fw-medium);padding:3px 10px;border-radius:var(--r-pill);background:var(--glass);border:1px solid var(--glass-line);color:var(--ink-700)}.pattern-chip:before{content:"▲";font-size:9px;color:var(--ink-dim)}.metric-group{margin-bottom:var(--sp-4)}.metric-group .group-head{font-size:var(--fs-sm);font-weight:var(--fw-semi);color:var(--ink-700);margin-bottom:6px;padding-left:2px}.metric-group .metric-grid{margin-bottom:0}.watchlist-section{margin-bottom:var(--sp-5)}.watchlist-section.empty{padding:var(--sp-5);background:#0f172906;border-radius:var(--r-md);text-align:center;color:var(--ink-mute);font-size:var(--fs-sm);margin-bottom:var(--sp-5)}.watchlist-section .memory-meta{display:flex;flex-wrap:wrap;gap:var(--sp-3);font-size:var(--fs-xs);color:var(--ink-mute);padding:var(--sp-3) var(--sp-4);background:#0f172906;border-radius:var(--r-md);margin-bottom:var(--sp-4)}.watchlist-section .memory-meta strong{color:var(--ink-700);font-weight:var(--fw-semi)}.dialog-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);margin-bottom:var(--sp-2);flex-wrap:wrap}.dialog-section-head h3{font-size:var(--fs-h3);font-weight:var(--fw-semi);letter-spacing:-.02em;color:var(--ink-900)}.dialog-section-head .sub{font-size:var(--fs-xs);color:var(--ink-dim)}.empty-line{font-size:var(--fs-sm);color:var(--ink-mute);padding:var(--sp-3) var(--sp-4);background:#0f172905;border-radius:var(--r-sm)}.watchlist-list{display:flex;flex-direction:column;gap:var(--sp-2);margin-bottom:var(--sp-3)}.watchlist-chips{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-bottom:var(--sp-3)}.watchlist-detail{padding:var(--sp-4);background:#0f172908;border-radius:var(--r-md);border-left:3px solid var(--accent);margin-bottom:var(--sp-4)}.watchlist-detail .head{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-2);margin-bottom:6px}.watchlist-detail .head .name{font-weight:var(--fw-semi);color:var(--ink-900);font-size:var(--fs-h3);letter-spacing:-.02em}.watchlist-detail .head .code{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-mute)}.watchlist-detail .head .pattern{font-size:var(--fs-xs);color:var(--accent);background:var(--accent-soft);padding:2px 8px;border-radius:var(--r-pill);font-weight:var(--fw-medium)}.watchlist-detail .head .trigger{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-700);font-weight:var(--fw-medium);margin-left:auto}.watchlist-detail .reason{font-size:var(--fs-sm);color:var(--ink-700);line-height:1.6;margin:6px 0 8px}.watchlist-detail .meta{display:flex;flex-wrap:wrap;gap:var(--sp-3);font-size:var(--fs-xs);color:var(--ink-mute);margin-bottom:var(--sp-3)}.watchlist-detail .meta strong{color:var(--ink-700);font-weight:var(--fw-medium)}.watchlist-detail .watchlist-chart{margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:1px dashed var(--glass-line)}.watchlist-row{padding:var(--sp-3) var(--sp-4);background:#0f172908;border-radius:var(--r-md);border-left:3px solid var(--accent);transition:var(--tx-fast)}.watchlist-row.is-selected{background:#6366f10f;border-left-color:var(--accent);box-shadow:0 1px 3px #0f17290a}.watchlist-row-head{background:transparent;border:0;padding:0;margin:0 0 4px;cursor:pointer;width:100%;text-align:left;font:inherit;color:inherit}.watchlist-row-head:hover .name{color:var(--accent)}.watchlist-row-head:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm)}.watchlist-row .head{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-2);margin-bottom:4px}.watchlist-row .head .caret{color:var(--ink-dim);font-size:10px;width:10px;display:inline-block}.watchlist-chart{margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:1px dashed var(--glass-line)}.watchlist-row .head .name{font-weight:var(--fw-semi);color:var(--ink-900);font-size:var(--fs-body)}.watchlist-row .head .code{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-mute)}.watchlist-row .head .pattern{font-size:var(--fs-xs);color:var(--accent);background:var(--accent-soft);padding:2px 8px;border-radius:var(--r-pill);font-weight:var(--fw-medium)}.watchlist-row .head .trigger{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--ink-700);font-weight:var(--fw-medium);margin-left:auto}.watchlist-row .reason{font-size:var(--fs-sm);color:var(--ink-700);line-height:1.5;margin:4px 0 6px}.watchlist-row .meta{display:flex;flex-wrap:wrap;gap:var(--sp-3);font-size:var(--fs-xs);color:var(--ink-mute)}.watchlist-row .meta strong{color:var(--ink-700);font-weight:var(--fw-medium)}.market-view{font-size:var(--fs-sm);color:var(--ink-700);line-height:1.6;white-space:pre-wrap;padding:var(--sp-4);background:#0f172906;border-radius:var(--r-md);margin-bottom:var(--sp-3)}.candle-chart{background:#0f172906;border:1px solid var(--glass-line);border-radius:var(--r-md);padding:var(--sp-3) var(--sp-3) var(--sp-2);margin-bottom:var(--sp-4)}.candle-chart svg{width:100%;height:auto;display:block}.candle-chart .axis{fill:var(--ink-dim);font-size:10px;font-family:var(--font-mono)}.candle-chart .legend{display:flex;align-items:center;gap:var(--sp-3);margin-top:6px;font-size:var(--fs-xs);color:var(--ink-mute);flex-wrap:wrap}.candle-chart .legend .item{display:inline-flex;align-items:center;gap:4px}.candle-chart .legend .swatch{display:inline-block;width:10px;height:10px;border-radius:2px}.candle-chart .legend .swatch.ma5{background:#f59e0b;height:2px;border-radius:999px;width:14px}.candle-chart .legend .swatch.ma20{background:var(--ink-mute);height:2px;border-radius:999px;width:14px}.candle-chart .legend .swatch.up{background:var(--gain)}.candle-chart .legend .swatch.down{background:var(--loss)}.candle-chart .legend .swatch.decision-line{background:transparent;border-top:2px dashed var(--accent);width:14px;height:0;border-radius:0;align-self:center}.candle-chart .data-caption{margin-top:4px;font-size:var(--fs-xs);color:var(--ink-mute);font-variant-numeric:tabular-nums;line-height:1.4}.candle-chart .data-caption strong{color:var(--ink-700);font-weight:var(--fw-semi)}.candle-chart .legend .spacer{flex:1}.candle-chart .legend .meta{font-variant-numeric:tabular-nums}.candle-chart.loading,.candle-chart.empty{min-height:80px;display:flex;align-items:center;justify-content:center}.candle-chart .msg{font-size:var(--fs-sm);color:var(--ink-mute)}.reasons-toggle{margin-top:var(--sp-3);border:1px solid var(--glass-line);background:var(--glass);border-radius:var(--r-md);padding:0;transition:var(--tx-fast)}.reasons-toggle[open]{border-color:var(--ink-faint);background:#0f172906}.reasons-toggle>summary{cursor:pointer;list-style:none;padding:var(--sp-3) var(--sp-4);font-size:var(--fs-sm);color:var(--ink-mute);font-weight:var(--fw-medium);display:flex;align-items:center;gap:6px}.reasons-toggle>summary::-webkit-details-marker{display:none}.reasons-toggle>summary:before{content:"▸";font-size:10px;color:var(--ink-dim);transition:transform .15s}.reasons-toggle[open]>summary:before{transform:rotate(90deg)}.reasons-toggle[open]>summary{color:var(--ink-700)}.reasons-toggle .reasons-list{padding:0 var(--sp-4) var(--sp-4) var(--sp-7);margin:0;font-size:var(--fs-sm);color:var(--ink-700);line-height:1.6}.mode-badge{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-xs);font-weight:var(--fw-semi);padding:4px 10px;border-radius:var(--r-pill);letter-spacing:.02em}.mode-badge .dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.mode-badge.live{background:var(--loss-soft);color:var(--loss)}.mode-badge.live .dot{background:var(--loss);box-shadow:0 0 0 3px var(--loss-soft);animation:pulse 2s ease-in-out infinite}.mode-badge.paper{background:var(--glass);color:var(--ink-700);border:1px solid var(--glass-line)}.mode-badge.paper .dot{background:var(--ink-mute)}.mode-badge.halted{background:#94a3b82e;color:var(--ink-mute)}.mode-badge.halted .dot{background:var(--ink-mute)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.4}}.toast-stack{position:fixed;right:var(--sp-5);bottom:var(--sp-5);z-index:200;display:flex;flex-direction:column;gap:var(--sp-3);pointer-events:none;max-width:380px}.toast{pointer-events:auto;background:var(--glass-3);backdrop-filter:var(--blur-lg);-webkit-backdrop-filter:var(--blur-lg);border:1px solid var(--glass-border);border-radius:var(--r-lg);padding:var(--sp-4) var(--sp-5);box-shadow:var(--shadow-lg);display:grid;grid-template-columns:28px 1fr auto;gap:var(--sp-3);align-items:start;animation:toast-in .25s ease-out}.toast.is-leaving{animation:toast-out .2s ease-in forwards}.toast .icon{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}.toast.success .icon{background:var(--gain-soft);color:var(--gain)}.toast.danger .icon{background:var(--loss-soft);color:var(--loss)}.toast.warn .icon{background:var(--warn-soft);color:var(--warn)}.toast.info .icon{background:var(--accent-soft);color:var(--accent)}.toast .body{min-width:0}.toast .title{font-size:var(--fs-sm);font-weight:var(--fw-semi);color:var(--ink-900);line-height:1.3}.toast .desc{font-size:var(--fs-xs);color:var(--ink-mute);margin-top:2px;line-height:1.4}.toast .close{background:transparent;border:none;color:var(--ink-dim);cursor:pointer;font-size:18px;padding:0 4px;line-height:1}.toast .close:hover{color:var(--ink-900)}@keyframes toast-in{0%{transform:translateY(8px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes toast-out{to{transform:translateY(8px);opacity:0}}.dialog-backdrop.is-confirm{z-index:300}.confirm-panel{max-width:600px;width:100%}.confirm-panel .alert-icon{width:56px;height:56px;border-radius:50%;background:var(--loss-soft);color:var(--loss);display:flex;align-items:center;justify-content:center;font-size:28px;margin-bottom:var(--sp-4)}.confirm-panel h2{font-size:var(--fs-h2);font-weight:var(--fw-bold);letter-spacing:-.02em;margin-bottom:var(--sp-3)}.confirm-panel .desc{font-size:var(--fs-body);color:var(--ink-700);line-height:1.6;margin-bottom:var(--sp-4)}.confirm-panel ul{background:var(--loss-soft);border-radius:var(--r-md);padding:var(--sp-4) var(--sp-5);margin-bottom:var(--sp-5);list-style:none}.confirm-panel ul li{font-size:var(--fs-sm);color:var(--loss);padding:4px 0;display:flex;gap:8px}.confirm-panel ul li:before{content:"•";flex-shrink:0;font-weight:700}.confirm-panel .actions{display:flex;gap:var(--sp-3);justify-content:flex-end}.confirm-panel .actions .btn{min-width:100px}.btn.live{background:var(--loss);color:#fff;border-color:var(--loss)}.btn.live:hover:not(:disabled){background:#dc2626;border-color:#dc2626}.dots-loading:after{content:"";display:inline-block;width:12px;text-align:left;animation:dots 1.5s steps(4,end) infinite}@keyframes dots{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}to{content:""}}@media(max-width:1100px){.stat-row{grid-template-columns:repeat(2,1fr)}.persona-grid{grid-template-columns:1fr 1fr}.journal-grid{grid-template-columns:1fr}.journal-detail{position:static}.setup-grid{grid-template-columns:1fr}.setup-nav{position:static;flex-direction:row;flex-wrap:wrap}}@media(max-width:720px){.app-bar{flex-wrap:wrap}.stat-row{grid-template-columns:1fr 1fr}.persona-grid{grid-template-columns:1fr}.step-list{grid-template-columns:1fr 1fr}.alloc-row{grid-template-columns:1fr}}
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>mulsok</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
8
+ <script type="module" crossorigin src="/assets/index-62SMpbaf.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BPLQR0wt.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@mulsok/traders-client",
3
+ "version": "0.1.0",
4
+ "description": "mulsok-traders 데스크톱 클라이언트 · 한국 주식 AI 자율 트레이더 (macOS · claude CLI)",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "author": "Taeeun Jang",
8
+ "homepage": "https://mulsok-traders.vercel.app",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/TaeeunJang/mulsok-traders.git",
12
+ "directory": "apps/client"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/TaeeunJang/mulsok-traders/issues"
16
+ },
17
+ "keywords": [
18
+ "mulsok-traders",
19
+ "trading",
20
+ "korean-stocks",
21
+ "claude",
22
+ "llm",
23
+ "kiwoom",
24
+ "macos",
25
+ "launchagent"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "os": [
31
+ "darwin"
32
+ ],
33
+ "bin": {
34
+ "mulsok-client": "bin/cli.js"
35
+ },
36
+ "files": [
37
+ "bin/",
38
+ "dist/",
39
+ "scripts/com.mulsok.traders.client.plist.template",
40
+ "scripts/install-daemon.sh",
41
+ "scripts/uninstall-daemon.sh",
42
+ "README.md"
43
+ ],
44
+ "scripts": {
45
+ "dev": "concurrently -n server,web -c blue,green \"npm run dev:server\" \"npm run dev:web\"",
46
+ "dev:server": "tsx watch src-server/index.ts",
47
+ "dev:web": "vite",
48
+ "build": "vite build && tsc -p tsconfig.server.json",
49
+ "start": "node dist/server/index.js",
50
+ "typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.web.json",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "test:unit": "vitest run test/unit",
54
+ "test:integration": "vitest run test/integration",
55
+ "config": "tsx scripts/config.ts",
56
+ "prepack": "npm run build",
57
+ "postinstall": "node bin/postinstall.js",
58
+ "preuninstall": "node bin/preuninstall.js",
59
+ "publish:dry": "npm pack --dry-run",
60
+ "publish:public": "npm publish --access public"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "registry": "https://registry.npmjs.org/"
65
+ },
66
+ "dependencies": {
67
+ "@fastify/static": "^8.0.3",
68
+ "better-sqlite3": "^12.9.0",
69
+ "dotenv": "^17.4.2",
70
+ "fastify": "^5.1.0",
71
+ "gray-matter": "^4.0.3",
72
+ "open": "^10.1.0",
73
+ "pino-pretty": "^13.1.3",
74
+ "react": "^19.0.0",
75
+ "react-dom": "^19.0.0",
76
+ "react-markdown": "^10.1.0",
77
+ "remark-gfm": "^4.0.1",
78
+ "ws": "^8.20.0",
79
+ "zod": "^3.23.8"
80
+ },
81
+ "devDependencies": {
82
+ "@types/better-sqlite3": "^7.6.13",
83
+ "@types/react": "^19.0.0",
84
+ "@types/react-dom": "^19.0.0",
85
+ "@types/ws": "^8.18.1",
86
+ "@vitejs/plugin-react": "^4.3.4",
87
+ "concurrently": "^9.1.0",
88
+ "tsx": "^4.19.2",
89
+ "typescript": "^5.6.0",
90
+ "vite": "^6.0.3",
91
+ "vitest": "^2.1.8"
92
+ }
93
+ }
@@ -0,0 +1,58 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ mulsok-traders client · LaunchAgent 템플릿
5
+
6
+ install-daemon.sh 가 다음 placeholder 를 치환해서 ~/Library/LaunchAgents/ 에 복사합니다.
7
+ __USER_HOME__ 사용자 홈 디렉토리 (예: /Users/taeeunjang)
8
+ __APPS_CLIENT_PATH__ apps/client 절대 경로
9
+ __NODE_BIN__ node 실행 파일 절대 경로
10
+ __PATH__ 데몬 PATH (node + claude 디렉토리 포함)
11
+
12
+ 베타 1라운드 안전 기본값:
13
+ - CLIENT_HOST=127.0.0.1 (loopback only · LAN 노출 금지 · 인증 미들웨어 미구현)
14
+ - KeepAlive + ThrottleInterval 30 (비정상 종료 시 30초 후 자동 재기동)
15
+ - RunAtLoad (부팅 시 자동 시작)
16
+ -->
17
+ <plist version="1.0">
18
+ <dict>
19
+ <key>Label</key>
20
+ <string>com.mulsok.traders.client</string>
21
+
22
+ <key>ProgramArguments</key>
23
+ <array>
24
+ <string>__NODE_BIN__</string>
25
+ <string>__APPS_CLIENT_PATH__/dist/server/index.js</string>
26
+ </array>
27
+
28
+ <key>WorkingDirectory</key>
29
+ <string>__APPS_CLIENT_PATH__</string>
30
+
31
+ <key>EnvironmentVariables</key>
32
+ <dict>
33
+ <key>CLIENT_PORT</key>
34
+ <string>5903</string>
35
+ <key>CLIENT_HOST</key>
36
+ <string>127.0.0.1</string>
37
+ <key>NODE_ENV</key>
38
+ <string>production</string>
39
+ <key>PATH</key>
40
+ <string>__PATH__</string>
41
+ </dict>
42
+
43
+ <key>RunAtLoad</key>
44
+ <true/>
45
+
46
+ <key>KeepAlive</key>
47
+ <true/>
48
+
49
+ <key>ThrottleInterval</key>
50
+ <integer>30</integer>
51
+
52
+ <key>StandardOutPath</key>
53
+ <string>__USER_HOME__/.mulsok-traders/logs/client.out.log</string>
54
+
55
+ <key>StandardErrorPath</key>
56
+ <string>__USER_HOME__/.mulsok-traders/logs/client.err.log</string>
57
+ </dict>
58
+ </plist>
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # mulsok-traders client · LaunchAgent 설치 스크립트
4
+ #
5
+ # 사용법:
6
+ # cd apps/client
7
+ # ./scripts/install-daemon.sh
8
+ #
9
+ # 동작:
10
+ # 1. 사전 점검 (Node 20+ · plist 템플릿 존재 · claude CLI · git root)
11
+ # 2. dist/ 미존재 시 npm run build
12
+ # 3. plist 템플릿 placeholder 치환 → ~/Library/LaunchAgents/ 복사
13
+ # 4. 기존 데몬 idempotent bootout → load
14
+ # 5. 헬스체크 + 다음 단계 안내
15
+ #
16
+ # 실패 시 exit code:
17
+ # 1 사전 점검 실패 · 2 빌드 실패 · 3 가동 실패
18
+
19
+ set -euo pipefail
20
+
21
+ LABEL="com.mulsok.traders.client"
22
+ PLIST_NAME="${LABEL}.plist"
23
+ LAUNCHAGENT_PATH="${HOME}/Library/LaunchAgents/${PLIST_NAME}"
24
+ DATA_DIR="${HOME}/.mulsok-traders"
25
+ LOGS_DIR="${DATA_DIR}/logs"
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ APPS_CLIENT_PATH="$(cd "${SCRIPT_DIR}/.." && pwd)"
29
+ TEMPLATE_PATH="${SCRIPT_DIR}/${PLIST_NAME}.template"
30
+
31
+ bold() { printf '\033[1m%s\033[0m' "$*"; }
32
+ green() { printf '\033[32m%s\033[0m' "$*"; }
33
+ yellow() { printf '\033[33m%s\033[0m' "$*"; }
34
+ red() { printf '\033[31m%s\033[0m' "$*"; }
35
+
36
+ echo
37
+ echo "$(bold '▶ mulsok-traders client · 데몬 설치')"
38
+ echo " apps/client : ${APPS_CLIENT_PATH}"
39
+ echo " plist : ${LAUNCHAGENT_PATH}"
40
+ echo " 데이터 : ${DATA_DIR}"
41
+ echo
42
+
43
+ # ── 1. 사전 점검 ─────────────────────────────────
44
+ if [[ ! -f "${TEMPLATE_PATH}" ]]; then
45
+ echo "$(red '✗') plist 템플릿이 없습니다: ${TEMPLATE_PATH}" >&2
46
+ exit 1
47
+ fi
48
+
49
+ NODE_BIN="$(command -v node || true)"
50
+ if [[ -z "${NODE_BIN}" ]]; then
51
+ echo "$(red '✗') node 명령을 찾지 못했습니다. Node.js 20+ 를 설치하세요 (nodejs.org)." >&2
52
+ exit 1
53
+ fi
54
+ NODE_MAJOR="$("${NODE_BIN}" -v | sed 's/^v\([0-9]*\).*/\1/')"
55
+ if (( NODE_MAJOR < 20 )); then
56
+ echo "$(red '✗') Node.js 20+ 가 필요합니다. 현재: $("${NODE_BIN}" -v)" >&2
57
+ exit 1
58
+ fi
59
+ echo "$(green '✓') node $("${NODE_BIN}" -v) (${NODE_BIN})"
60
+
61
+ CLAUDE_BIN="$(command -v claude || true)"
62
+ if [[ -n "${CLAUDE_BIN}" ]]; then
63
+ echo "$(green '✓') claude CLI: $(claude --version 2>/dev/null | head -1) (${CLAUDE_BIN})"
64
+ else
65
+ echo "$(yellow '⚠') claude CLI 미설치 — claude.com/claude-code 참고"
66
+ echo " 계속 진행하지만 readiness 의 llm 항목이 통과하지 못합니다."
67
+ fi
68
+
69
+ # plist 의 PATH 동적 구성 — node + claude 디렉토리를 prepend
70
+ NODE_DIR="$(dirname "${NODE_BIN}")"
71
+ PLIST_PATH="${NODE_DIR}"
72
+ if [[ -n "${CLAUDE_BIN}" ]]; then
73
+ CLAUDE_DIR="$(dirname "${CLAUDE_BIN}")"
74
+ if [[ "${CLAUDE_DIR}" != "${NODE_DIR}" ]]; then
75
+ PLIST_PATH="${CLAUDE_DIR}:${PLIST_PATH}"
76
+ fi
77
+ fi
78
+ PLIST_PATH="${PLIST_PATH}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
79
+
80
+ # ── 2. 빌드 ──────────────────────────────────────
81
+ if [[ ! -f "${APPS_CLIENT_PATH}/dist/server/index.js" ]]; then
82
+ echo
83
+ echo "$(bold '▶ dist/ 미존재 · 빌드 실행')"
84
+ if ! (cd "${APPS_CLIENT_PATH}" && npm run build); then
85
+ echo "$(red '✗') 빌드 실패" >&2
86
+ exit 2
87
+ fi
88
+ fi
89
+ echo "$(green '✓') dist/ 빌드 확인"
90
+
91
+ mkdir -p "${LOGS_DIR}"
92
+
93
+ # ── 3. 기존 데몬 idempotent bootout ──────────────
94
+ if launchctl print "gui/$(id -u)/${LABEL}" >/dev/null 2>&1; then
95
+ echo
96
+ echo "$(bold '▶ 기존 데몬 bootout (idempotent)')"
97
+ launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
98
+ sleep 1
99
+ fi
100
+
101
+ # ── 4. 템플릿 치환 + 복사 + 등록 ─────────────────
102
+ echo
103
+ echo "$(bold '▶ plist 생성')"
104
+ sed -e "s|__USER_HOME__|${HOME}|g" \
105
+ -e "s|__APPS_CLIENT_PATH__|${APPS_CLIENT_PATH}|g" \
106
+ -e "s|__NODE_BIN__|${NODE_BIN}|g" \
107
+ -e "s|__PATH__|${PLIST_PATH}|g" \
108
+ "${TEMPLATE_PATH}" > "${LAUNCHAGENT_PATH}"
109
+ chmod 600 "${LAUNCHAGENT_PATH}"
110
+ echo "$(green '✓') ${LAUNCHAGENT_PATH} (0600)"
111
+
112
+ echo "$(bold '▶ launchctl load')"
113
+ launchctl load -w "${LAUNCHAGENT_PATH}"
114
+
115
+ # ── 5. 헬스체크 ──────────────────────────────────
116
+ echo
117
+ printf "%s" "$(bold '▶ 헬스체크 ')"
118
+ HEALTH_URL="http://127.0.0.1:5903/api/health"
119
+ HEALTHY=0
120
+ for i in 1 2 3 4 5 6 7 8 9 10; do
121
+ if curl -sf "${HEALTH_URL}" >/dev/null 2>&1; then
122
+ HEALTHY=1
123
+ break
124
+ fi
125
+ printf '.'
126
+ sleep 1
127
+ done
128
+ echo
129
+
130
+ if (( HEALTHY == 0 )); then
131
+ echo "$(red '✗') 데몬이 10초 안에 응답하지 않았습니다."
132
+ echo " 로그: tail ${LOGS_DIR}/client.err.log"
133
+ exit 3
134
+ fi
135
+ echo "$(green '✓') 데몬 가동 (${HEALTH_URL})"
136
+
137
+ # ── 6. 다음 단계 안내 ────────────────────────────
138
+ cat <<'EOF'
139
+
140
+ ────────────────────────────────────────────────
141
+ 다음 단계:
142
+
143
+ 1. 브라우저: http://127.0.0.1:5903/
144
+ 2. 6탭 입력: 구독 → AI 모델 → 증권사 → 자본 배정 → 디바이스 → 안전망
145
+ 3. readiness: curl http://127.0.0.1:5903/api/readiness | python3 -m json.tool
146
+
147
+ 운영 명령:
148
+ 상태: launchctl list | grep mulsok.traders
149
+ 재시작: launchctl kickstart -k gui/$(id -u)/com.mulsok.traders.client
150
+ 로그: tail -f ~/.mulsok-traders/logs/client.{out,err}.log
151
+ 제거: ./scripts/uninstall-daemon.sh
152
+
153
+ ⚠️ CLIENT_HOST=0.0.0.0 으로 바꾸지 마세요.
154
+ 인증 미들웨어가 미구현이라 LAN 누구나 데몬 제어 가능합니다 (다음 라운드 추가).
155
+ ────────────────────────────────────────────────
156
+ EOF
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # mulsok-traders client · LaunchAgent 제거 스크립트
4
+ #
5
+ # 사용법:
6
+ # cd apps/client
7
+ # ./scripts/uninstall-daemon.sh
8
+ #
9
+ # 동작:
10
+ # 1. launchctl bootout (가동 중이면 중지)
11
+ # 2. ~/Library/LaunchAgents/com.mulsok.traders.client.plist 삭제
12
+ # 3. ~/.mulsok-traders/ 데이터 디렉토리는 보존 (수동 삭제 안내)
13
+
14
+ set -euo pipefail
15
+
16
+ LABEL="com.mulsok.traders.client"
17
+ PLIST_NAME="${LABEL}.plist"
18
+ LAUNCHAGENT_PATH="${HOME}/Library/LaunchAgents/${PLIST_NAME}"
19
+ DATA_DIR="${HOME}/.mulsok-traders"
20
+
21
+ bold() { printf '\033[1m%s\033[0m' "$*"; }
22
+ green() { printf '\033[32m%s\033[0m' "$*"; }
23
+
24
+ echo
25
+ echo "$(bold '▶ mulsok-traders client · 데몬 제거')"
26
+ echo " plist : ${LAUNCHAGENT_PATH}"
27
+ echo " 데이터 : ${DATA_DIR} (보존)"
28
+ echo
29
+
30
+ # ── 데몬 중지 (있으면) ──────────────────────────
31
+ if launchctl print "gui/$(id -u)/${LABEL}" >/dev/null 2>&1; then
32
+ echo "▶ 데몬 중지"
33
+ launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
34
+ sleep 1
35
+ echo "$(green '✓') bootout"
36
+ else
37
+ echo " 데몬 미등록 — bootout 건너뜀"
38
+ fi
39
+
40
+ # ── plist 삭제 ──────────────────────────────────
41
+ if [[ -f "${LAUNCHAGENT_PATH}" ]]; then
42
+ rm -f "${LAUNCHAGENT_PATH}"
43
+ echo "$(green '✓') plist 삭제"
44
+ else
45
+ echo " plist 없음"
46
+ fi
47
+
48
+ cat <<EOF
49
+
50
+ ────────────────────────────────────────────────
51
+ $(green '✓ 데몬 제거 완료')
52
+
53
+ 데이터는 보존되었습니다:
54
+ ${DATA_DIR}
55
+
56
+ 다시 가동하려면:
57
+ ./scripts/install-daemon.sh
58
+
59
+ 데이터까지 완전히 지우려면 (되돌릴 수 없음):
60
+ rm -rf ${DATA_DIR}
61
+ ────────────────────────────────────────────────
62
+ EOF