dskcode 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -88,28 +88,28 @@ var ENV_MAP = {
88
88
  [`${ENV_PREFIX}MAX_TOOL_ROUNDS`]: "maxToolRounds"
89
89
  };
90
90
  function applyEnvVars(config) {
91
+ const result = { ...config, providers: [...config.providers] };
91
92
  for (const [envKey, configKey] of Object.entries(ENV_MAP)) {
92
93
  const raw = process.env[envKey];
93
94
  if (raw === void 0) continue;
94
- const cfg = config;
95
95
  switch (configKey) {
96
96
  case "verbose":
97
97
  case "defaultProvider": {
98
- cfg[configKey] = raw;
98
+ result[configKey] = raw;
99
99
  break;
100
100
  }
101
101
  case "maxTokens":
102
102
  case "maxToolRounds": {
103
103
  const n = Number(raw);
104
104
  if (Number.isFinite(n) && n > 0) {
105
- cfg[configKey] = n;
105
+ result[configKey] = n;
106
106
  }
107
107
  break;
108
108
  }
109
109
  case "temperature": {
110
110
  const n = Number(raw);
111
111
  if (Number.isFinite(n) && n >= 0 && n <= 2) {
112
- cfg[configKey] = n;
112
+ result[configKey] = n;
113
113
  }
114
114
  break;
115
115
  }
@@ -117,12 +117,14 @@ function applyEnvVars(config) {
117
117
  }
118
118
  const apiKey = process.env.DEEPSEEK_API_KEY;
119
119
  if (apiKey) {
120
- const deepseek = config.providers.find((p) => p.name === "deepseek");
121
- if (deepseek && !deepseek.apiKey) {
122
- deepseek.apiKey = apiKey;
123
- }
124
- if (!deepseek) {
125
- config.providers.unshift({
120
+ const deepseekIdx = result.providers.findIndex((p) => p.name === "deepseek");
121
+ if (deepseekIdx !== -1) {
122
+ const existing = result.providers[deepseekIdx];
123
+ if (!existing.apiKey) {
124
+ result.providers[deepseekIdx] = { ...existing, apiKey };
125
+ }
126
+ } else {
127
+ result.providers.unshift({
126
128
  name: "deepseek",
127
129
  baseUrl: "https://api.deepseek.com",
128
130
  model: "deepseek-v4-flash",
@@ -130,27 +132,31 @@ function applyEnvVars(config) {
130
132
  });
131
133
  }
132
134
  }
133
- return config;
135
+ return result;
134
136
  }
135
137
  function applyCliOverrides(config, flags) {
138
+ const result = { ...config, providers: [...config.providers] };
136
139
  if (flags.verbose !== void 0) {
137
- config.verbose = flags.verbose;
140
+ result.verbose = flags.verbose;
138
141
  }
139
142
  if (flags.model !== void 0) {
140
- const provider = config.providers.find(
141
- (p) => p.name === config.defaultProvider
143
+ const providerIdx = result.providers.findIndex(
144
+ (p) => p.name === result.defaultProvider
142
145
  );
143
- if (provider) {
144
- provider.model = flags.model;
146
+ if (providerIdx !== -1) {
147
+ result.providers[providerIdx] = {
148
+ ...result.providers[providerIdx],
149
+ model: flags.model
150
+ };
145
151
  }
146
152
  }
147
153
  if (flags.maxTokens !== void 0 && flags.maxTokens > 0) {
148
- config.maxTokens = flags.maxTokens;
154
+ result.maxTokens = flags.maxTokens;
149
155
  }
150
156
  if (flags.temperature !== void 0 && flags.temperature >= 0 && flags.temperature <= 2) {
151
- config.temperature = flags.temperature;
157
+ result.temperature = flags.temperature;
152
158
  }
153
- return config;
159
+ return result;
154
160
  }
155
161
  function validateConfig(config) {
156
162
  const errors = [];
@@ -192,6 +198,12 @@ function validateConfig(config) {
192
198
  message: "temperature \u5FC5\u987B\u5728 0.0 ~ 2.0 \u4E4B\u95F4\u3002"
193
199
  });
194
200
  }
201
+ if (config.maxTokens !== void 0 && config.maxTokens < 1) {
202
+ errors.push({
203
+ field: "maxTokens",
204
+ message: "maxTokens \u5FC5\u987B\u5927\u4E8E\u7B49\u4E8E 1\u3002"
205
+ });
206
+ }
195
207
  if (config.maxToolRounds !== void 0 && config.maxToolRounds < 1) {
196
208
  errors.push({
197
209
  field: "maxToolRounds",
@@ -339,36 +351,32 @@ function hasApiKey(providers) {
339
351
  if (process.env.DEEPSEEK_API_KEY) return true;
340
352
  return false;
341
353
  }
354
+ var MIN_API_KEY_LENGTH = 10;
342
355
  async function promptForApiKey() {
343
- console.log(
344
- chalk2.yellow("\n \u26A0 \u672A\u68C0\u6D4B\u5230 API Key \u914D\u7F6E")
345
- );
346
- console.log(
347
- chalk2.dim(" \u4F60\u53EF\u4EE5\u901A\u8FC7\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u914D\u7F6E\uFF1A")
348
- );
349
- console.log(
350
- chalk2.dim(" \xB7 \u73AF\u5883\u53D8\u91CF: export DEEPSEEK_API_KEY=sk-xxx")
351
- );
352
- console.log(
353
- chalk2.dim(" \xB7 \u914D\u7F6E\u6587\u4EF6: ~/.dskcode/settings.json")
354
- );
355
- console.log(
356
- chalk2.dim(" \xB7 \u4E0B\u9762\u76F4\u63A5\u8F93\u5165\uFF0C\u81EA\u52A8\u4FDD\u5B58\u5230\u5168\u5C40\u914D\u7F6E\n")
357
- );
356
+ console.log(chalk2.yellow("\n \u26A0 \u672A\u68C0\u6D4B\u5230 API Key \u914D\u7F6E"));
357
+ console.log(chalk2.dim(" \u4F60\u53EF\u4EE5\u901A\u8FC7\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u914D\u7F6E\uFF1A"));
358
+ console.log(chalk2.dim(" \xB7 \u73AF\u5883\u53D8\u91CF: export DEEPSEEK_API_KEY=sk-xxx"));
359
+ console.log(chalk2.dim(" \xB7 \u914D\u7F6E\u6587\u4EF6: ~/.dskcode/settings.json"));
360
+ console.log(chalk2.dim(" \xB7 \u4E0B\u9762\u76F4\u63A5\u8F93\u5165\uFF0C\u81EA\u52A8\u4FDD\u5B58\u5230\u5168\u5C40\u914D\u7F6E\n"));
358
361
  const rl = createInterface({
359
362
  input: process.stdin,
360
363
  output: process.stdout
361
364
  });
362
365
  return new Promise((resolve) => {
366
+ let resolved = false;
363
367
  const cleanup = () => {
368
+ if (resolved) return;
369
+ resolved = true;
370
+ process.stdin.removeListener("keypress", onKeypress);
364
371
  rl.close();
365
372
  };
366
- process.stdin.on("keypress", (_, key) => {
373
+ const onKeypress = (_, key) => {
367
374
  if (key.ctrl && key.name === "c") {
368
375
  cleanup();
369
376
  resolve(null);
370
377
  }
371
- });
378
+ };
379
+ process.stdin.on("keypress", onKeypress);
372
380
  rl.question(
373
381
  ` ${chalk2.cyan("\u{1F511}")} ${chalk2.bold("\u8BF7\u8F93\u5165\u4F60\u7684 DeepSeek API Key:")} `,
374
382
  (answer) => {
@@ -379,7 +387,7 @@ async function promptForApiKey() {
379
387
  resolve(null);
380
388
  return;
381
389
  }
382
- if (trimmed.length < 10) {
390
+ if (trimmed.length < MIN_API_KEY_LENGTH) {
383
391
  console.log(chalk2.red(" \u2716 API Key \u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u957F\u5EA6\u81F3\u5C11 10 \u4F4D"));
384
392
  resolve(null);
385
393
  return;
@@ -393,7 +401,7 @@ async function promptForApiKey() {
393
401
  // src/ui/RenderScope.tsx
394
402
  import { render } from "ink";
395
403
  function renderApp(node) {
396
- const { waitUntilExit, clear, unmount } = render(node);
404
+ const { waitUntilExit, clear, unmount } = render(node, { exitOnCtrlC: false });
397
405
  return { waitUntilExit: waitUntilExit(), clear, unmount };
398
406
  }
399
407
 
@@ -410,12 +418,6 @@ import { jsxs as jsxs2 } from "react/jsx-runtime";
410
418
  import { Box as Box2, Text as Text3 } from "ink";
411
419
  import { useEffect, useState } from "react";
412
420
  import { jsx as jsx2 } from "react/jsx-runtime";
413
-
414
- // src/ui/ChatSession.tsx
415
- import { Box as Box3, Text as Text4 } from "ink";
416
- import TextInput from "ink-text-input";
417
- import { useEffect as useEffect2, useState as useState2, useCallback } from "react";
418
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
419
421
  var CYBER_PALETTE = ["#00ffff", "#ff00ff", "#00ff41", "#ff1493", "#8b00ff"];
420
422
  var LOGO_LINES = [
421
423
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
@@ -425,57 +427,119 @@ var LOGO_LINES = [
425
427
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557",
426
428
  " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
427
429
  ];
428
- var COMMANDS = {
429
- "/exit": { desc: "\u9000\u51FA\u5BF9\u8BDD", handler: () => "" },
430
- "/quit": { desc: "\u9000\u51FA\u5BF9\u8BDD", handler: () => "" },
431
- "/help": {
432
- desc: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F",
433
- handler: () => [
434
- "\u53EF\u7528\u547D\u4EE4\uFF1A",
435
- " /exit, /quit \u9000\u51FA\u5BF9\u8BDD",
436
- " /help \u663E\u793A\u6B64\u5E2E\u52A9",
437
- " /clear \u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2",
438
- " /version \u663E\u793A\u7248\u672C\u4FE1\u606F"
439
- ].join("\n")
440
- },
441
- "/clear": { desc: "\u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2", handler: () => "" },
442
- "/version": { desc: "\u663E\u793A\u7248\u672C\u4FE1\u606F", handler: () => "dskcode v0.0.0" }
443
- };
444
- function ChatSession({ providerCount, toolCount, verbose }) {
445
- const [offset, setOffset] = useState2(0);
446
- const [messages, setMessages] = useState2([]);
447
- const [input, setInput] = useState2("");
430
+
431
+ // src/ui/ChatSession.tsx
432
+ import { Box as Box3, Text as Text4, useInput } from "ink";
433
+ import TextInput from "ink-text-input";
434
+ import { useEffect as useEffect3, useState as useState3, useCallback as useCallback2 } from "react";
435
+
436
+ // src/ui/useDoubleCtrlC.ts
437
+ import { useState as useState2, useCallback, useEffect as useEffect2, useRef } from "react";
438
+ var CTRL_C_TIMEOUT_MS = 1500;
439
+ function useDoubleCtrlC(onExit) {
440
+ const [doubleCtrlC, setDoubleCtrlC] = useState2(false);
441
+ const timerRef = useRef(null);
442
+ const onExitRef = useRef(onExit);
443
+ onExitRef.current = onExit;
448
444
  useEffect2(() => {
445
+ return () => {
446
+ if (timerRef.current) clearTimeout(timerRef.current);
447
+ };
448
+ }, []);
449
+ const handleCtrlC = useCallback(() => {
450
+ if (doubleCtrlC) {
451
+ onExitRef.current();
452
+ return;
453
+ }
454
+ setDoubleCtrlC(true);
455
+ if (timerRef.current) clearTimeout(timerRef.current);
456
+ timerRef.current = setTimeout(() => {
457
+ setDoubleCtrlC(false);
458
+ timerRef.current = null;
459
+ }, CTRL_C_TIMEOUT_MS);
460
+ }, [doubleCtrlC]);
461
+ return { doubleCtrlC, handleCtrlC };
462
+ }
463
+
464
+ // src/ui/ChatSession.tsx
465
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
466
+ var commandRegistry = /* @__PURE__ */ new Map();
467
+ function registerCommand(name, cmd) {
468
+ commandRegistry.set(name, cmd);
469
+ }
470
+ function getRegisteredCommands() {
471
+ return commandRegistry;
472
+ }
473
+ registerCommand("/exit", { desc: "\u9000\u51FA\u5BF9\u8BDD", handler: () => ({ kind: "exit" }) });
474
+ registerCommand("/quit", { desc: "\u9000\u51FA\u5BF9\u8BDD", handler: () => ({ kind: "exit" }) });
475
+ registerCommand("/help", {
476
+ desc: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F",
477
+ handler: () => {
478
+ const commands = getRegisteredCommands();
479
+ const lines = ["\u53EF\u7528\u547D\u4EE4\uFF1A"];
480
+ for (const [name, cmd] of commands) {
481
+ lines.push(` ${name.padEnd(16)}${cmd.desc}`);
482
+ }
483
+ return { kind: "text", content: lines.join("\n") };
484
+ }
485
+ });
486
+ registerCommand("/clear", { desc: "\u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2", handler: () => ({ kind: "clear" }) });
487
+ registerCommand("/version", { desc: "\u663E\u793A\u7248\u672C\u4FE1\u606F", handler: () => ({ kind: "text", content: "dskcode v0.0.0" }) });
488
+ registerCommand("/game", { desc: "\u542F\u52A8\u6E38\u620F", handler: () => ({ kind: "navigate", target: "game" }) });
489
+ registerCommand("/stock", { desc: "\u67E5\u770B\u80A1\u7968\u884C\u60C5", handler: () => ({ kind: "navigate", target: "stock" }) });
490
+ function ChatSession({ providerCount, toolCount, verbose, onLaunchGame, onLaunchStock }) {
491
+ const [offset, setOffset] = useState3(0);
492
+ const [messages, setMessages] = useState3([]);
493
+ const [input, setInput] = useState3("");
494
+ const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(() => process.exit(0));
495
+ useInput(
496
+ useCallback2(
497
+ (input2, key) => {
498
+ if (input2 === "c" && key.ctrl) {
499
+ handleCtrlC();
500
+ }
501
+ },
502
+ [handleCtrlC]
503
+ )
504
+ );
505
+ useEffect3(() => {
449
506
  const timer = setInterval(() => {
450
507
  setOffset((prev) => (prev + 1) % CYBER_PALETTE.length);
451
508
  }, 500);
452
509
  return () => clearInterval(timer);
453
510
  }, []);
454
- const handleSubmit = useCallback((value) => {
511
+ const handleSubmit = useCallback2((value) => {
455
512
  const trimmed = value.trim();
456
513
  if (!trimmed) return;
457
514
  if (trimmed.startsWith("/")) {
458
- const cmd = COMMANDS[trimmed.toLowerCase()];
515
+ const cmd = commandRegistry.get(trimmed.toLowerCase());
459
516
  if (cmd) {
460
- if (trimmed.toLowerCase() === "/exit" || trimmed.toLowerCase() === "/quit") {
461
- process.exit(0);
462
- return;
463
- }
464
- if (trimmed.toLowerCase() === "/clear") {
465
- setMessages([]);
466
- setInput("");
467
- return;
468
- }
469
517
  const result = cmd.handler();
470
- if (result) {
471
- setMessages((prev) => [
472
- ...prev,
473
- { role: "user", content: trimmed },
474
- { role: "assistant", content: result }
475
- ]);
518
+ switch (result.kind) {
519
+ case "exit":
520
+ process.exit(0);
521
+ return;
522
+ case "clear":
523
+ setMessages([]);
524
+ setInput("");
525
+ return;
526
+ case "navigate":
527
+ setInput("");
528
+ if (result.target === "game") {
529
+ onLaunchGame?.();
530
+ } else if (result.target === "stock") {
531
+ onLaunchStock?.();
532
+ }
533
+ return;
534
+ case "text":
535
+ setMessages((prev) => [
536
+ ...prev,
537
+ { role: "user", content: trimmed },
538
+ { role: "assistant", content: result.content }
539
+ ]);
540
+ setInput("");
541
+ return;
476
542
  }
477
- setInput("");
478
- return;
479
543
  }
480
544
  setMessages((prev) => [
481
545
  ...prev,
@@ -491,7 +555,7 @@ function ChatSession({ providerCount, toolCount, verbose }) {
491
555
  { role: "assistant", content: "dskcode AI \u2014 \u5F85\u5B9E\u73B0\uFF08\u7B2C07\u7AE0\uFF09\u3002\u5F53\u524D\u4E3A CLI \u6846\u67B6\u6F14\u793A\u6A21\u5F0F\u3002" }
492
556
  ]);
493
557
  setInput("");
494
- }, []);
558
+ }, [onLaunchGame, onLaunchStock]);
495
559
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [
496
560
  /* @__PURE__ */ jsxs3(Box3, { flexDirection: "row", marginBottom: 1, children: [
497
561
  /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginRight: 4, children: LOGO_LINES.map((line, i) => {
@@ -530,19 +594,26 @@ function ChatSession({ providerCount, toolCount, verbose }) {
530
594
  }
531
595
  ) })
532
596
  ] }),
533
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text4, { color: "#00ffff", dimColor: true, children: " " + "\u2500".repeat(36) }) })
597
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text4, { color: "#00ffff", dimColor: true, children: " " + "\u2500".repeat(36) }) }),
598
+ doubleCtrlC && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text4, { color: "#ff1493", bold: true, children: " \u26A0 \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA dskcode" }) })
534
599
  ] });
535
600
  }
536
601
 
537
602
  // src/ui/GamePicker.tsx
538
- import { Box as Box4, Text as Text5, useInput } from "ink";
539
- import { useState as useState3, useCallback as useCallback2 } from "react";
603
+ import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
604
+ import { useState as useState4, useCallback as useCallback3 } from "react";
540
605
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
541
- function GamePicker({ games, onSelect, onExit }) {
542
- const [selectedIndex, setSelectedIndex] = useState3(0);
543
- useInput(
544
- useCallback2(
606
+ function GamePicker({ games, onSelect, onExit, onBackToChat }) {
607
+ const [selectedIndex, setSelectedIndex] = useState4(0);
608
+ const exitAction = onBackToChat ?? onExit ?? (() => process.exit(0));
609
+ const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(exitAction);
610
+ useInput2(
611
+ useCallback3(
545
612
  (input, key) => {
613
+ if (input === "c" && key.ctrl) {
614
+ handleCtrlC();
615
+ return;
616
+ }
546
617
  if (games.length === 0) return;
547
618
  if (key.upArrow || input === "k") {
548
619
  setSelectedIndex((prev) => prev > 0 ? prev - 1 : games.length - 1);
@@ -552,10 +623,11 @@ function GamePicker({ games, onSelect, onExit }) {
552
623
  const game = games[selectedIndex];
553
624
  if (game) onSelect(game);
554
625
  } else if (key.escape || input === "q") {
555
- onExit();
626
+ if (onBackToChat) onBackToChat();
627
+ else onExit?.();
556
628
  }
557
629
  },
558
- [games, selectedIndex, onSelect, onExit]
630
+ [games, selectedIndex, onSelect, onExit, onBackToChat, handleCtrlC]
559
631
  )
560
632
  );
561
633
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
@@ -568,7 +640,8 @@ function GamePicker({ games, onSelect, onExit }) {
568
640
  /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text5, { color: "#888888", children: game.description }) })
569
641
  ] }, game.id);
570
642
  }) }),
571
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: " \u2191/\u2193 \u9009\u62E9 Enter \u542F\u52A8 q \u8FD4\u56DE" }) })
643
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: " \u2191/\u2193 \u9009\u62E9 Enter \u542F\u52A8 q \u8FD4\u56DE" }) }),
644
+ doubleCtrlC && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text5, { color: "#ff1493", bold: true, children: " \u26A0 \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA dskcode" }) })
572
645
  ] });
573
646
  }
574
647
 
@@ -585,8 +658,8 @@ function listGames() {
585
658
  }
586
659
 
587
660
  // src/game/brick-breaker/index.tsx
588
- import { Box as Box5, Text as Text6, useInput as useInput2, render as render2 } from "ink";
589
- import { useState as useState4, useEffect as useEffect3, useRef, useCallback as useCallback3 } from "react";
661
+ import { Box as Box5, Text as Text6, useInput as useInput3, render as render2 } from "ink";
662
+ import { useState as useState5, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback4 } from "react";
590
663
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
591
664
  var GAME_WIDTH = 40;
592
665
  var GAME_HEIGHT = 18;
@@ -718,13 +791,13 @@ function buildBoard(state) {
718
791
  return lines.map((l) => `\u2502${l}\u2502`).join("\n");
719
792
  }
720
793
  function BrickBreakerGame({ onExit: _onExit }) {
721
- const [initialLevel, setInitialLevel] = useState4(1);
722
- const [selectingLevel, setSelectingLevel] = useState4(true);
723
- const stateRef = useRef(createInitialState(initialLevel));
724
- const [tick, setTick] = useState4(0);
725
- const onExitRef = useRef(_onExit);
794
+ const [initialLevel, setInitialLevel] = useState5(1);
795
+ const [selectingLevel, setSelectingLevel] = useState5(true);
796
+ const stateRef = useRef2(createInitialState(initialLevel));
797
+ const [tick, setTick] = useState5(0);
798
+ const onExitRef = useRef2(_onExit);
726
799
  onExitRef.current = _onExit;
727
- useEffect3(() => {
800
+ useEffect4(() => {
728
801
  if (selectingLevel) return;
729
802
  const interval = setInterval(() => {
730
803
  update(stateRef.current);
@@ -732,18 +805,18 @@ function BrickBreakerGame({ onExit: _onExit }) {
732
805
  }, 80);
733
806
  return () => clearInterval(interval);
734
807
  }, [selectingLevel]);
735
- const restart = useCallback3((level) => {
808
+ const restart = useCallback4((level) => {
736
809
  const lv = level ?? stateRef.current.level;
737
810
  stateRef.current = createInitialState(lv);
738
811
  setInitialLevel(lv);
739
812
  setSelectingLevel(false);
740
813
  setTick(0);
741
814
  }, []);
742
- const startLevelSelect = useCallback3(() => {
815
+ const startLevelSelect = useCallback4(() => {
743
816
  setSelectingLevel(true);
744
817
  }, []);
745
- useInput2(
746
- useCallback3((input, key) => {
818
+ useInput3(
819
+ useCallback4((input, key) => {
747
820
  const s2 = stateRef.current;
748
821
  if (selectingLevel) {
749
822
  if (input >= "1" && input <= "9") {
@@ -857,8 +930,8 @@ var brick_breaker_default = {
857
930
  };
858
931
 
859
932
  // src/game/coder-check/index.tsx
860
- import { Box as Box6, Text as Text7, useInput as useInput3, render as render3 } from "ink";
861
- import { useState as useState5, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback4 } from "react";
933
+ import { Box as Box6, Text as Text7, useInput as useInput4, render as render3 } from "ink";
934
+ import { useState as useState6, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback5 } from "react";
862
935
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
863
936
  var GAME_W = 66;
864
937
  var GAME_H = 20;
@@ -1250,26 +1323,26 @@ function buildGameView(s, scoreLines, scoreColor, message) {
1250
1323
  return rows;
1251
1324
  }
1252
1325
  function CoderCheck({ onExit: _onExit }) {
1253
- const stateRef = useRef2(createInitialState2());
1254
- const [tick, setTick] = useState5(0);
1255
- const [colorOffset, setColorOffset] = useState5(0);
1256
- const onExitRef = useRef2(_onExit);
1326
+ const stateRef = useRef3(createInitialState2());
1327
+ const [tick, setTick] = useState6(0);
1328
+ const [colorOffset, setColorOffset] = useState6(0);
1329
+ const onExitRef = useRef3(_onExit);
1257
1330
  onExitRef.current = _onExit;
1258
- useEffect4(() => {
1331
+ useEffect5(() => {
1259
1332
  const timer = setInterval(() => {
1260
1333
  setColorOffset((prev) => (prev + 1) % CYBER_PALETTE2.length);
1261
1334
  }, 400);
1262
1335
  return () => clearInterval(timer);
1263
1336
  }, []);
1264
- useEffect4(() => {
1337
+ useEffect5(() => {
1265
1338
  const interval = setInterval(() => {
1266
1339
  update2(stateRef.current);
1267
1340
  setTick((t) => t + 1);
1268
1341
  }, 60);
1269
1342
  return () => clearInterval(interval);
1270
1343
  }, []);
1271
- useInput3(
1272
- useCallback4((input, key) => {
1344
+ useInput4(
1345
+ useCallback5((input, key) => {
1273
1346
  const s2 = stateRef.current;
1274
1347
  if (s2.gameOver) {
1275
1348
  if (input === "r") {
@@ -1398,25 +1471,24 @@ import { render as render4 } from "ink";
1398
1471
  import chalk3 from "chalk";
1399
1472
 
1400
1473
  // src/stock/StockList.tsx
1401
- import { Box as Box7, Text as Text8, useInput as useInput4 } from "ink";
1402
- import { useState as useState6, useCallback as useCallback5, useEffect as useEffect5 } from "react";
1474
+ import { Box as Box7, Text as Text8, useInput as useInput5 } from "ink";
1475
+ import { useState as useState7, useCallback as useCallback6, useEffect as useEffect6 } from "react";
1403
1476
  import asciichart from "asciichart";
1404
1477
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1405
1478
  var MINUTE_API = "https://web.ifzq.gtimg.cn/appstock/app/minute/query?code={code}&r=0.1";
1406
- function toApiCode(code) {
1407
- if (/^sh|^sz/.test(code)) return code;
1408
- if (/^60/.test(code) || /^68/.test(code)) return "sh" + code;
1409
- if (/^00/.test(code) || /^30/.test(code) || /^39/.test(code)) return "sz" + code;
1410
- if (/^51/.test(code)) return "sh" + code;
1479
+ function normalizeApiCode(code) {
1480
+ if (code.startsWith("sh") || code.startsWith("sz")) return code;
1481
+ if (/^60|^68|^51/.test(code)) return "sh" + code;
1482
+ if (/^00|^30|^39/.test(code)) return "sz" + code;
1411
1483
  return "sh" + code;
1412
1484
  }
1413
1485
  async function fetchStockMinute(code) {
1414
- const url = MINUTE_API.replace("{code}", toApiCode(code));
1486
+ const url = MINUTE_API.replace("{code}", normalizeApiCode(code));
1415
1487
  try {
1416
1488
  const resp = await fetch(url);
1417
1489
  const json = await resp.json();
1418
1490
  if (json.code !== 0) return null;
1419
- const stockKey = toApiCode(code);
1491
+ const stockKey = normalizeApiCode(code);
1420
1492
  const stockData = json.data?.[stockKey];
1421
1493
  if (!stockData) return null;
1422
1494
  const rawMinutes = stockData.data?.data ?? [];
@@ -1487,17 +1559,18 @@ function latestPoints(data, maxPoints = 60) {
1487
1559
  if (data.length <= maxPoints) return data;
1488
1560
  return data.slice(data.length - maxPoints);
1489
1561
  }
1490
- function StockList({ codes, onExit }) {
1491
- const [stocks, setStocks] = useState6([]);
1492
- const [selectedIndex, setSelectedIndex] = useState6(0);
1493
- const [loading, setLoading] = useState6(true);
1494
- const [lastUpdate, setLastUpdate] = useState6("");
1495
- const [detailView, setDetailView] = useState6(null);
1496
- const [detailPrices, setDetailPrices] = useState6(null);
1497
- const [detailLoading, setDetailLoading] = useState6(false);
1498
- const [detailCountdown, setDetailCountdown] = useState6(10);
1499
- const [countdown, setCountdown] = useState6(5);
1500
- const loadData = useCallback5(async () => {
1562
+ function StockList({ codes, onExit, onBackToChat }) {
1563
+ const [stocks, setStocks] = useState7([]);
1564
+ const [selectedIndex, setSelectedIndex] = useState7(0);
1565
+ const [loading, setLoading] = useState7(true);
1566
+ const [lastUpdate, setLastUpdate] = useState7("");
1567
+ const [detailView, setDetailView] = useState7(null);
1568
+ const [detailPrices, setDetailPrices] = useState7(null);
1569
+ const [detailLoading, setDetailLoading] = useState7(false);
1570
+ const [detailCountdown, setDetailCountdown] = useState7(10);
1571
+ const [countdown, setCountdown] = useState7(5);
1572
+ const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(onExit);
1573
+ const loadData = useCallback6(async () => {
1501
1574
  setLoading(true);
1502
1575
  try {
1503
1576
  const data = await fetchStocks(codes ?? []);
@@ -1507,10 +1580,10 @@ function StockList({ codes, onExit }) {
1507
1580
  }
1508
1581
  setLoading(false);
1509
1582
  }, [codes]);
1510
- useEffect5(() => {
1583
+ useEffect6(() => {
1511
1584
  loadData();
1512
1585
  }, [loadData]);
1513
- useEffect5(() => {
1586
+ useEffect6(() => {
1514
1587
  const interval = setInterval(() => {
1515
1588
  setCountdown((prev) => {
1516
1589
  if (prev <= 1) {
@@ -1522,7 +1595,7 @@ function StockList({ codes, onExit }) {
1522
1595
  }, 1e3);
1523
1596
  return () => clearInterval(interval);
1524
1597
  }, [loadData]);
1525
- useEffect5(() => {
1598
+ useEffect6(() => {
1526
1599
  if (!detailView) {
1527
1600
  setDetailPrices(null);
1528
1601
  setDetailLoading(false);
@@ -1542,16 +1615,20 @@ function StockList({ codes, onExit }) {
1542
1615
  const timer = setInterval(loadDetail, 1e4);
1543
1616
  return () => clearInterval(timer);
1544
1617
  }, [detailView]);
1545
- useEffect5(() => {
1618
+ useEffect6(() => {
1546
1619
  if (!detailView) return;
1547
1620
  const timer = setInterval(() => {
1548
1621
  setDetailCountdown((prev) => prev > 0 ? prev - 1 : 10);
1549
1622
  }, 1e3);
1550
1623
  return () => clearInterval(timer);
1551
1624
  }, [detailView]);
1552
- useInput4(
1553
- useCallback5(
1625
+ useInput5(
1626
+ useCallback6(
1554
1627
  (input, key) => {
1628
+ if (input === "c" && key.ctrl) {
1629
+ handleCtrlC();
1630
+ return;
1631
+ }
1555
1632
  if (detailView) {
1556
1633
  if (key.escape || input === "q" || input === " ") {
1557
1634
  setDetailView(null);
@@ -1567,13 +1644,14 @@ function StockList({ codes, onExit }) {
1567
1644
  const stock = stocks[selectedIndex];
1568
1645
  if (stock) setDetailView(stock);
1569
1646
  } else if (key.escape || input === "q") {
1570
- onExit();
1647
+ if (onBackToChat) onBackToChat();
1648
+ else onExit();
1571
1649
  } else if (input === "r") {
1572
1650
  setCountdown(5);
1573
1651
  loadData();
1574
1652
  }
1575
1653
  },
1576
- [stocks, selectedIndex, detailView, onExit, loadData]
1654
+ [stocks, selectedIndex, detailView, onExit, onBackToChat, loadData, handleCtrlC]
1577
1655
  )
1578
1656
  );
1579
1657
  if (detailView) {
@@ -1623,7 +1701,8 @@ function StockList({ codes, onExit }) {
1623
1701
  ] }, stock.code);
1624
1702
  }) }),
1625
1703
  /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: ` \u2191/\u2193 \u9009\u62E9 Enter \u8BE6\u60C5 r \u624B\u52A8\u5237\u65B0 q \u8FD4\u56DE` }) }),
1626
- /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: ` \u6700\u540E\u66F4\u65B0: ${lastUpdate}` }) })
1704
+ /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: ` \u6700\u540E\u66F4\u65B0: ${lastUpdate}` }) }),
1705
+ doubleCtrlC && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text8, { color: "#ff1493", bold: true, children: " \u26A0 \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA dskcode" }) })
1627
1706
  ] });
1628
1707
  }
1629
1708
  function renderDetail(stock, _onBack, prices, countdown = 10) {
@@ -1704,18 +1783,64 @@ function createCli() {
1704
1783
  const result = await loadAndValidate();
1705
1784
  ctx = { ...ctx, config: result.config };
1706
1785
  }
1707
- const app = renderApp(
1786
+ startChat(ctx);
1787
+ });
1788
+ function startChat(ctx) {
1789
+ const chatApp = renderApp(
1708
1790
  /* @__PURE__ */ jsx8(
1709
1791
  ChatSession,
1710
1792
  {
1711
1793
  providerCount: ctx?.config.providers.length ?? 1,
1712
1794
  toolCount: ctx?.config.tools.length ?? 0,
1713
- verbose: ctx?.verbose ?? false
1795
+ verbose: ctx?.verbose ?? false,
1796
+ onLaunchGame: () => {
1797
+ chatApp.unmount();
1798
+ setImmediate(() => {
1799
+ initGames();
1800
+ const games = listGames();
1801
+ const { unmount } = render4(
1802
+ /* @__PURE__ */ jsx8(
1803
+ GamePicker,
1804
+ {
1805
+ games,
1806
+ onSelect: async (game) => {
1807
+ unmount();
1808
+ await game.play();
1809
+ startChat(ctx);
1810
+ },
1811
+ onBackToChat: () => {
1812
+ unmount();
1813
+ setImmediate(() => startChat(ctx));
1814
+ }
1815
+ }
1816
+ ),
1817
+ { exitOnCtrlC: false }
1818
+ );
1819
+ });
1820
+ },
1821
+ onLaunchStock: () => {
1822
+ chatApp.unmount();
1823
+ setImmediate(() => {
1824
+ const defaultStockCodes = ctx?.config.stock?.symbols?.map((s) => s.code) ?? ["sh000001", "sz399006", "sh601688"];
1825
+ const stockApp = renderApp(
1826
+ /* @__PURE__ */ jsx8(
1827
+ StockList,
1828
+ {
1829
+ codes: defaultStockCodes,
1830
+ onBackToChat: () => {
1831
+ stockApp.unmount();
1832
+ setImmediate(() => startChat(ctx));
1833
+ },
1834
+ onExit: () => process.exit(0)
1835
+ }
1836
+ )
1837
+ );
1838
+ });
1839
+ }
1714
1840
  }
1715
1841
  )
1716
1842
  );
1717
- await app.waitUntilExit;
1718
- });
1843
+ }
1719
1844
  program2.command("run").description("\u6267\u884C\u4E00\u6B21\u6027\u4EFB\u52A1").argument("[prompt...]", "\u4EFB\u52A1\u63CF\u8FF0").option("--model <name>", "\u6307\u5B9A\u4F7F\u7528\u7684\u6A21\u578B").action(async function(_prompt) {
1720
1845
  console.log("dskcode run \u2014 \u5F85\u5B9E\u73B0\uFF08\u7B2C07\u7AE0\uFF09");
1721
1846
  });
@@ -1760,7 +1885,8 @@ compdef _dskcode_completion dskcode`);
1760
1885
  }
1761
1886
  });
1762
1887
  program2.command("stock").description("\u67E5\u770B\u81EA\u9009\u80A1\u5B9E\u65F6\u884C\u60C5").argument("[codes...]", "\u80A1\u7968\u4EE3\u7801\uFF08\u7A7A\u683C\u5206\u9694\uFF09\uFF0C\u5982 513090 600519").action(async function(codes) {
1763
- const codeList = codes && codes.length > 0 ? codes : ["sh000001", "sz399006", "sh601688"];
1888
+ const ctx = this.dskcodeCtx;
1889
+ const codeList = codes && codes.length > 0 ? codes : ctx?.config.stock?.symbols?.map((s) => s.code) ?? ["sh000001", "sz399006", "sh601688"];
1764
1890
  const app = renderApp(
1765
1891
  /* @__PURE__ */ jsx8(
1766
1892
  StockList,
@@ -1804,7 +1930,8 @@ compdef _dskcode_completion dskcode`);
1804
1930
  resolve(null);
1805
1931
  }
1806
1932
  }
1807
- )
1933
+ ),
1934
+ { exitOnCtrlC: false }
1808
1935
  );
1809
1936
  });
1810
1937
  if (selectedGame) {
@@ -1831,8 +1958,18 @@ var ExitCode = {
1831
1958
  };
1832
1959
 
1833
1960
  // src/index.ts
1961
+ var sigintCount = 0;
1962
+ var sigintTimer = null;
1834
1963
  process.on("SIGINT", () => {
1835
- process.exit(ExitCode.SIGINT);
1964
+ sigintCount++;
1965
+ if (sigintCount >= 2) {
1966
+ process.exit(ExitCode.SIGINT);
1967
+ }
1968
+ process.stdout.write("\n \u26A0 \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA dskcode\n");
1969
+ if (sigintTimer) clearTimeout(sigintTimer);
1970
+ sigintTimer = setTimeout(() => {
1971
+ sigintCount = 0;
1972
+ }, 1500);
1836
1973
  });
1837
1974
  var program = createCli();
1838
1975
  try {