zapmyco 0.4.0 → 0.6.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { D as buildSkillSnapshot, E as eventBus, M as __VERSION__, N as __require, O as loadSkills, _ as createLlmBasedAgent, a as SubAgentManager, h as ZapmycoErrorCode, j as APP_NAME, p as WebError, s as logger, t as loadConfig } from "../loader-BK1Z72gI.mjs";
2
+ import { D as eventBus, F as __require, M as APP_NAME, N as SESSION_DIR_NAME, O as buildSkillSnapshot, P as __VERSION__, a as SubAgentManager, c as logger, g as ZapmycoErrorCode, k as loadSkills, m as WebError, s as configureLogger, t as loadConfig, v as createLlmBasedAgent } from "../loader-CyGlMdl7.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
5
5
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
@@ -11,7 +11,7 @@ import { EventEmitter } from "node:events";
11
11
  import chalk, { Chalk } from "chalk";
12
12
  import { Command } from "commander";
13
13
  import { getModel } from "@mariozechner/pi-ai";
14
- import { Container, Editor, Key, ProcessTerminal, TUI, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
14
+ import { CombinedAutocompleteProvider, Container, Editor, Key, ProcessTerminal, TUI, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
15
15
  import { spawn } from "node:child_process";
16
16
  import TurndownService from "turndown";
17
17
  import { lookup } from "node:dns/promises";
@@ -19,7 +19,7 @@ import { Client } from "@modelcontextprotocol/sdk/client";
19
19
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
20
20
 
21
21
  //#region src/cli/repl/command-registry.ts
22
- const log$5 = logger.child("repl:command-registry");
22
+ const log$6 = logger.child("repl:command-registry");
23
23
  /**
24
24
  * 命令注册表
25
25
  */
@@ -34,7 +34,7 @@ var CommandRegistry = class {
34
34
  */
35
35
  register(cmd) {
36
36
  const canonicalName = cmd.name.toLowerCase();
37
- if (this.commands.has(canonicalName)) log$5.warn(`命令 "${canonicalName}" 已存在,将被覆盖`);
37
+ if (this.commands.has(canonicalName)) log$6.warn(`命令 "${canonicalName}" 已存在,将被覆盖`);
38
38
  this.commands.set(canonicalName, cmd);
39
39
  for (const alias of cmd.aliases) {
40
40
  const lowerAlias = alias.toLowerCase();
@@ -62,7 +62,7 @@ var CommandRegistry = class {
62
62
  */
63
63
  async dispatch(parsed) {
64
64
  if (parsed.kind !== "command") {
65
- log$5.warn("dispatch 收到了非 command 类型的输入");
65
+ log$6.warn("dispatch 收到了非 command 类型的输入");
66
66
  return;
67
67
  }
68
68
  const cmd = this.getCommand(parsed.name);
@@ -74,7 +74,7 @@ var CommandRegistry = class {
74
74
  await cmd.handler(parsed.args, this.session);
75
75
  } catch (error) {
76
76
  const message = error instanceof Error ? error.message : String(error);
77
- log$5.error(`命令 /${cmd.name} 执行出错`, {}, error);
77
+ log$6.error(`命令 /${cmd.name} 执行出错`, {}, error);
78
78
  console.log(`\n 命令执行出错: ${message}\n`);
79
79
  }
80
80
  }
@@ -359,6 +359,8 @@ var ZapmycoEditor = class extends Editor {
359
359
  onCtrlD;
360
360
  /** 是否正在执行(用于显示 loading) */
361
361
  #executing = false;
362
+ /** 是否显示 spinner(执行期间禁用输入但不一定显示 spinner) */
363
+ #showSpinner = true;
362
364
  /** loading 动画帧索引 */
363
365
  #loadingFrame = 0;
364
366
  /** loading 动画定时器 */
@@ -380,11 +382,14 @@ var ZapmycoEditor = class extends Editor {
380
382
  }
381
383
  /**
382
384
  * 设置执行状态(控制 loading spinner 显示)
385
+ * @param executing 是否正在执行
386
+ * @param showSpinner 是否显示 spinner(默认 true)。设为 false 时仅禁用输入,不显示动画
383
387
  */
384
- setExecuting(executing) {
385
- if (this.#executing === executing) return;
388
+ setExecuting(executing, showSpinner = true) {
389
+ if (this.#executing === executing && this.#showSpinner === showSpinner) return;
386
390
  this.#executing = executing;
387
- if (executing) {
391
+ this.#showSpinner = showSpinner;
392
+ if (executing && showSpinner) {
388
393
  this.#loadingFrame = 0;
389
394
  this.#loadingTimer = setInterval(() => {
390
395
  this.#loadingFrame = (this.#loadingFrame + 1) % LOADING_FRAMES.length;
@@ -417,7 +422,7 @@ var ZapmycoEditor = class extends Editor {
417
422
  for (let i = 0; i < contentLines.length; i++) {
418
423
  const prefix = i === 0 ? PROMPT_PREFIX : " ".repeat(promptWidth);
419
424
  let line;
420
- if (i === 0 && this.#executing) line = `${prefix}${LOADING_FRAMES[this.#loadingFrame]} ${contentLines[i]}`;
425
+ if (i === 0 && this.#executing && this.#showSpinner) line = `${prefix}${LOADING_FRAMES[this.#loadingFrame]} ${contentLines[i]}`;
421
426
  else line = prefix + contentLines[i];
422
427
  contentLines[i] = truncateToWidth(line, width);
423
428
  }
@@ -718,7 +723,7 @@ const CRON_CONSTANTS = {
718
723
  *
719
724
  * @module cli/repl/cron/cron-scheduler
720
725
  */
721
- const log$4 = logger.child("cron:scheduler");
726
+ const log$5 = logger.child("cron:scheduler");
722
727
  var CronScheduler = class extends EventEmitter {
723
728
  store;
724
729
  jobs = [];
@@ -739,7 +744,7 @@ var CronScheduler = class extends EventEmitter {
739
744
  if (this.running) return;
740
745
  const loadedJobs = await this.store.load();
741
746
  this.jobs = loadedJobs;
742
- log$4.info(`调度器启动,加载 ${loadedJobs.length} 个 durable 任务`);
747
+ log$5.info(`调度器启动,加载 ${loadedJobs.length} 个 durable 任务`);
743
748
  await this.handleMissedJobs();
744
749
  this.checkAutoExpiry();
745
750
  this.running = true;
@@ -755,7 +760,7 @@ var CronScheduler = class extends EventEmitter {
755
760
  clearInterval(this.timer);
756
761
  this.timer = null;
757
762
  }
758
- log$4.info("调度器已停止");
763
+ log$5.info("调度器已停止");
759
764
  }
760
765
  /** 添加任务 */
761
766
  async addJob(job) {
@@ -767,7 +772,7 @@ var CronScheduler = class extends EventEmitter {
767
772
  this.jobs.push(job);
768
773
  await this.store.persist(this.jobs);
769
774
  } else this.sessionJobs.push(job);
770
- log$4.info("任务已添加", {
775
+ log$5.info("任务已添加", {
771
776
  id: job.id,
772
777
  cron: job.cron,
773
778
  durable: job.durable
@@ -897,7 +902,7 @@ var CronScheduler = class extends EventEmitter {
897
902
  }, delay);
898
903
  }
899
904
  if (toDelete.length > 0) {
900
- log$4.info(`跳过 ${toDelete.length} 个错过的一次性任务(超出补发上限)`);
905
+ log$5.info(`跳过 ${toDelete.length} 个错过的一次性任务(超出补发上限)`);
901
906
  this.emit("missed-overflow", {
902
907
  count: toDelete.length,
903
908
  jobIds: toDelete.map((m) => m.id)
@@ -915,7 +920,7 @@ var CronScheduler = class extends EventEmitter {
915
920
  job.lastFiredAt = now;
916
921
  job.fireCount++;
917
922
  this.removeJob(job.id);
918
- log$4.info("任务已过期并触发最后一次", { id: job.id });
923
+ log$5.info("任务已过期并触发最后一次", { id: job.id });
919
924
  }
920
925
  }
921
926
  }
@@ -976,7 +981,7 @@ function applyOneShotJitter(jobId, rawNext) {
976
981
  *
977
982
  * @module cli/repl/cron/cron-store
978
983
  */
979
- const log$3 = logger.child("cron:store");
984
+ const log$4 = logger.child("cron:store");
980
985
  const STORE_FILE = join(join(homedir(), ".zapmyco", "cron"), "scheduled_tasks.json");
981
986
  var CronStore = class {
982
987
  filePath;
@@ -1000,13 +1005,13 @@ var CronStore = class {
1000
1005
  const raw = await readFile(this.filePath, "utf-8");
1001
1006
  const data = JSON.parse(raw);
1002
1007
  if (!Array.isArray(data)) {
1003
- log$3.warn("存储文件格式无效(非数组),将使用空列表");
1008
+ log$4.warn("存储文件格式无效(非数组),将使用空列表");
1004
1009
  return [];
1005
1010
  }
1006
1011
  return this.validateJobs(data);
1007
1012
  } catch (err) {
1008
1013
  if (err.code === "ENOENT") return [];
1009
- log$3.warn("加载定时任务文件失败,将使用空列表", { error: err instanceof Error ? err.message : String(err) });
1014
+ log$4.warn("加载定时任务文件失败,将使用空列表", { error: err instanceof Error ? err.message : String(err) });
1010
1015
  return [];
1011
1016
  }
1012
1017
  }
@@ -1053,7 +1058,7 @@ var CronStore = class {
1053
1058
  if (typeof obj.maxFires === "number") job.maxFires = obj.maxFires;
1054
1059
  valid.push(job);
1055
1060
  }
1056
- if (valid.length < raw.length) log$3.warn(`跳过 ${raw.length - valid.length} 个无效任务条目`);
1061
+ if (valid.length < raw.length) log$4.warn(`跳过 ${raw.length - valid.length} 个无效任务条目`);
1057
1062
  return valid;
1058
1063
  }
1059
1064
  };
@@ -1065,8 +1070,19 @@ function getCronStore() {
1065
1070
 
1066
1071
  //#endregion
1067
1072
  //#region src/cli/repl/history-store.ts
1073
+ /**
1074
+ * 会话历史存储
1075
+ *
1076
+ * 基于内存的环形缓冲区,记录 REPL 会话中的用户输入和执行结果。
1077
+ * 支持文件持久化到 ~/.zapmyco/history.json,跨会话恢复。
1078
+ */
1079
+ const log$3 = logger.child("history:store");
1068
1080
  /** 默认最大历史条数 */
1069
1081
  const DEFAULT_MAX_SIZE = 100;
1082
+ /** 历史文件存储路径 */
1083
+ function getHistoryFilePath() {
1084
+ return join(homedir(), SESSION_DIR_NAME, "history.json");
1085
+ }
1070
1086
  /**
1071
1087
  * 历史存储类
1072
1088
  */
@@ -1074,8 +1090,11 @@ var HistoryStore = class {
1074
1090
  entries = [];
1075
1091
  nextId = 1;
1076
1092
  maxSize;
1093
+ filePath;
1077
1094
  constructor(maxSize = DEFAULT_MAX_SIZE) {
1078
1095
  this.maxSize = maxSize;
1096
+ this.filePath = getHistoryFilePath();
1097
+ this.load();
1079
1098
  }
1080
1099
  /** 添加条目 */
1081
1100
  push(entry) {
@@ -1085,6 +1104,7 @@ var HistoryStore = class {
1085
1104
  };
1086
1105
  this.entries.push(newEntry);
1087
1106
  if (this.entries.length > this.maxSize) this.entries.shift();
1107
+ this.save();
1088
1108
  return newEntry;
1089
1109
  }
1090
1110
  /** 获取所有条目 */
@@ -1096,16 +1116,52 @@ var HistoryStore = class {
1096
1116
  const count = Math.min(n, this.entries.length);
1097
1117
  return this.entries.slice(-count);
1098
1118
  }
1099
- /** 清空所有条目 */
1119
+ /** 清空所有条目(同时清除持久化文件) */
1100
1120
  clear() {
1101
1121
  this.entries = [];
1122
+ this.save();
1102
1123
  }
1103
1124
  /** 搜索条目(按输入内容模糊匹配) */
1104
1125
  search(query) {
1105
1126
  const lowerQuery = query.toLowerCase();
1106
1127
  return this.entries.filter((entry) => entry.input.toLowerCase().includes(lowerQuery));
1107
1128
  }
1129
+ /** 从文件加载历史记录 */
1130
+ load() {
1131
+ try {
1132
+ ensureDir(dirname(this.filePath));
1133
+ const raw = readFileSync(this.filePath, "utf-8");
1134
+ const data = JSON.parse(raw);
1135
+ if (Array.isArray(data.entries)) {
1136
+ this.entries = data.entries.slice(-this.maxSize);
1137
+ this.nextId = typeof data.nextId === "number" ? data.nextId : 1;
1138
+ log$3.debug("历史记录已加载", {
1139
+ count: this.entries.length,
1140
+ nextId: this.nextId
1141
+ });
1142
+ }
1143
+ } catch {
1144
+ log$3.debug("无历史文件或加载失败,使用空历史");
1145
+ }
1146
+ }
1147
+ /** 持久化历史记录到文件 */
1148
+ save() {
1149
+ try {
1150
+ ensureDir(dirname(this.filePath));
1151
+ const data = JSON.stringify({
1152
+ entries: this.entries,
1153
+ nextId: this.nextId
1154
+ }, null, 2);
1155
+ writeFileSync(this.filePath, data, "utf-8");
1156
+ } catch (err) {
1157
+ log$3.warn("历史记录保存失败", { error: err instanceof Error ? err.message : String(err) });
1158
+ }
1159
+ }
1108
1160
  };
1161
+ /** 确保目录存在 */
1162
+ function ensureDir(dir) {
1163
+ mkdirSync(dir, { recursive: true });
1164
+ }
1109
1165
 
1110
1166
  //#endregion
1111
1167
  //#region src/cli/repl/input-parser.ts
@@ -6599,6 +6655,12 @@ var OutputArea = class extends Container {
6599
6655
  else this.lines[this.lines.length - 1] += text;
6600
6656
  this.invalidate();
6601
6657
  }
6658
+ /** 替换最后一行的完整内容(用于 spinner 动画和首 chunk 替换) */
6659
+ replaceLastLine(text) {
6660
+ if (this.lines.length > 0) this.lines[this.lines.length - 1] = text;
6661
+ else this.lines.push(text);
6662
+ this.invalidate();
6663
+ }
6602
6664
  /** 清空所有内容 */
6603
6665
  clear() {
6604
6666
  this.lines = [];
@@ -6666,6 +6728,7 @@ var ReplSession = class {
6666
6728
  this.registry = new CommandRegistry(this);
6667
6729
  this.renderer = new Renderer(this.options);
6668
6730
  this.history = new HistoryStore(this.options.maxHistorySize);
6731
+ for (const entry of this.history.getAll()) this.editor.addToHistory(entry.input);
6669
6732
  this.agent = this.createReplAgent();
6670
6733
  this.taskStore = new TaskStore();
6671
6734
  this.taskStore.load();
@@ -6749,10 +6812,13 @@ var ReplSession = class {
6749
6812
  const startTime = Date.now();
6750
6813
  let historyEntry;
6751
6814
  const taskId = `task-${Date.now()}`;
6815
+ const ZAPMYCO_PREFIX = "ZapMyco: ";
6816
+ let spinnerActive = true;
6817
+ let spinnerInterval;
6752
6818
  try {
6753
6819
  this._state = "executing";
6754
6820
  this.updateStatsState();
6755
- this.editor.setExecuting(true);
6821
+ this.editor.setExecuting(true, false);
6756
6822
  this.currentTaskAbort = new AbortController();
6757
6823
  historyEntry = this.history.push({
6758
6824
  timestamp: Date.now(),
@@ -6762,11 +6828,24 @@ var ReplSession = class {
6762
6828
  goalId: `goal-${startTime}`,
6763
6829
  rawInput
6764
6830
  });
6765
- const goalLines = [`Me: ${rawInput}`, "ZapMyco: "];
6766
- this.outputArea.append(goalLines);
6831
+ this.outputArea.append([`Me: ${rawInput}`, ZAPMYCO_PREFIX + LOADING_FRAMES[0]]);
6832
+ let spinnerFrame = 0;
6833
+ spinnerActive = true;
6834
+ spinnerInterval = setInterval(() => {
6835
+ if (!spinnerActive) return;
6836
+ spinnerFrame = (spinnerFrame + 1) % LOADING_FRAMES.length;
6837
+ this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + LOADING_FRAMES[spinnerFrame]);
6838
+ this.tui.requestRender();
6839
+ }, 100);
6840
+ let firstOutputReceived = false;
6767
6841
  const outputHandler = (event) => {
6768
6842
  if (event.taskId === taskId) {
6769
- this.outputArea.appendText(event.text);
6843
+ if (!firstOutputReceived) {
6844
+ firstOutputReceived = true;
6845
+ spinnerActive = false;
6846
+ clearInterval(spinnerInterval);
6847
+ this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + event.text);
6848
+ } else this.outputArea.appendText(event.text);
6770
6849
  this.tui.requestRender();
6771
6850
  }
6772
6851
  };
@@ -6806,6 +6885,11 @@ var ReplSession = class {
6806
6885
  duration: Date.now() - startTime
6807
6886
  });
6808
6887
  const outputText = typeof taskResult.output === "string" ? taskResult.output : taskResult.output != null ? JSON.stringify(taskResult.output) : null;
6888
+ if (spinnerActive) {
6889
+ spinnerActive = false;
6890
+ clearInterval(spinnerInterval);
6891
+ if (outputText) this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + outputText);
6892
+ }
6809
6893
  if (taskResult.status !== "success") {
6810
6894
  const errorMsg = taskResult.error?.message ?? "Agent 执行失败(无详细错误信息)";
6811
6895
  this.outputArea.appendText(`[错误] ${errorMsg}`);
@@ -6854,14 +6938,17 @@ var ReplSession = class {
6854
6938
  } catch (error) {
6855
6939
  const err = error instanceof Error ? error : new Error(String(error));
6856
6940
  log.error("目标执行失败", { input: rawInput }, err);
6941
+ spinnerActive = false;
6942
+ clearInterval(spinnerInterval);
6857
6943
  this.stats.totalRequests++;
6858
6944
  this.stats.failureCount++;
6859
6945
  eventBus.emit("goal:failed", {
6860
6946
  goalId: `goal-${startTime}`,
6861
6947
  error: err
6862
6948
  });
6863
- const errorLines = this.renderer.renderError(err);
6864
- this.outputArea.append(errorLines);
6949
+ this.outputArea.replaceLastLine(`ZapMyco: [错误] ${err.message}`);
6950
+ const errorLines = this.renderer.renderError(err).slice(1);
6951
+ if (errorLines.length > 0) this.outputArea.append(errorLines);
6865
6952
  const duration = Date.now() - startTime;
6866
6953
  return {
6867
6954
  goalId: `goal-${startTime}`,
@@ -6878,6 +6965,8 @@ var ReplSession = class {
6878
6965
  }
6879
6966
  };
6880
6967
  } finally {
6968
+ spinnerActive = false;
6969
+ if (spinnerInterval) clearInterval(spinnerInterval);
6881
6970
  this._state = "idle";
6882
6971
  this.updateStatsState();
6883
6972
  this.editor.setExecuting(false);
@@ -6906,9 +6995,11 @@ var ReplSession = class {
6906
6995
  case "incomplete": break;
6907
6996
  case "command":
6908
6997
  await this.registry.dispatch(parsed);
6998
+ this.editor.addToHistory(line);
6909
6999
  break;
6910
7000
  case "goal":
6911
7001
  await this.executeGoal(parsed.rawInput);
7002
+ this.editor.addToHistory(line);
6912
7003
  break;
6913
7004
  }
6914
7005
  }
@@ -6921,6 +7012,26 @@ var ReplSession = class {
6921
7012
  this.registry.register(createConfigCommand());
6922
7013
  this.registry.register(createAgentsCommand());
6923
7014
  this.registry.register(createStatusCommand());
7015
+ this.buildAutocompleteProvider();
7016
+ }
7017
+ /** 构建并设置 autocomplete provider,将命令注册表中的命令接入 pi-tui 补全系统 */
7018
+ buildAutocompleteProvider() {
7019
+ const slashCommands = [];
7020
+ for (const cmd of this.registry.listCommands()) {
7021
+ const base = {
7022
+ name: cmd.name,
7023
+ description: cmd.description
7024
+ };
7025
+ if (cmd.usage !== `/${cmd.name}`) base.argumentHint = cmd.usage;
7026
+ slashCommands.push(base);
7027
+ for (const alias of cmd.aliases) slashCommands.push({
7028
+ name: alias,
7029
+ description: `${cmd.description}(别名: /${cmd.name})`
7030
+ });
7031
+ }
7032
+ const provider = new CombinedAutocompleteProvider(slashCommands, process.cwd(), null);
7033
+ this.editor.setAutocompleteProvider(provider);
7034
+ this.editor.setAutocompleteMaxVisible(12);
6924
7035
  }
6925
7036
  /**
6926
7037
  * 创建 REPL 专用的 Agent 实例
@@ -7010,6 +7121,7 @@ var ReplSession = class {
7010
7121
  const snapshot = buildSkillSnapshot(entries, skillConfig.maxSkillsInPrompt);
7011
7122
  this.agent.skillPrompt = snapshot.prompt;
7012
7123
  this._registerSkillCommands(entries);
7124
+ this.buildAutocompleteProvider();
7013
7125
  log.info("Skill 系统初始化完成", {
7014
7126
  count: snapshot.count,
7015
7127
  names: snapshot.names
@@ -7118,7 +7230,14 @@ var ReplSession = class {
7118
7230
  * 加载配置 → 创建会话 → 进入输入循环
7119
7231
  */
7120
7232
  async function startRepl() {
7121
- await new ReplSession(await loadConfig()).start();
7233
+ configureLogger({
7234
+ logFilePath: join(homedir(), ".zapmyco", "logs", "zapmyco.log"),
7235
+ quiet: true
7236
+ });
7237
+ const config = await loadConfig();
7238
+ if (config.logging?.level) configureLogger({ level: config.logging.level });
7239
+ if (config.logging?.file) configureLogger({ logFilePath: config.logging.file });
7240
+ await new ReplSession(config).start();
7122
7241
  }
7123
7242
 
7124
7243
  //#endregion