@armadra/agent 0.3.0 → 0.4.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 (198) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +93 -44
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +1 -1
  6. package/dist/agent/session-classifier.d.ts +18 -0
  7. package/dist/agent/session-classifier.js +103 -0
  8. package/dist/agent/session-core.d.ts +13 -0
  9. package/dist/agent/session-tools.js +24 -3
  10. package/dist/agent/session.d.ts +3 -0
  11. package/dist/agent/session.js +12 -3
  12. package/dist/agent/tool-runner.js +15 -2
  13. package/dist/agent/types.d.ts +9 -1
  14. package/dist/ai/apis/anthropic-messages.js +3 -2
  15. package/dist/ai/apis/google-generative-ai.js +3 -2
  16. package/dist/ai/apis/openai-completions.js +3 -2
  17. package/dist/ai/apis/openai-responses.js +3 -2
  18. package/dist/ai/http.d.ts +28 -6
  19. package/dist/ai/http.js +41 -8
  20. package/dist/ai/providers/registry.d.ts +8 -1
  21. package/dist/ai/providers/registry.js +30 -22
  22. package/dist/ai/providers/suggest.d.ts +18 -0
  23. package/dist/ai/providers/suggest.js +72 -0
  24. package/dist/ai/sse.d.ts +6 -2
  25. package/dist/ai/sse.js +22 -2
  26. package/dist/ai/types.d.ts +9 -2
  27. package/dist/bundle/ama.cjs +15279 -10151
  28. package/dist/cli/args.d.ts +19 -4
  29. package/dist/cli/args.js +83 -10
  30. package/dist/cli/bootstrap.js +27 -3
  31. package/dist/cli/codemode-notice.d.ts +20 -0
  32. package/dist/cli/codemode-notice.js +55 -0
  33. package/dist/cli/compose-session.d.ts +5 -0
  34. package/dist/cli/compose-session.js +32 -1
  35. package/dist/cli/compose-store.d.ts +1 -1
  36. package/dist/cli/compose-store.js +3 -1
  37. package/dist/cli/compose.d.ts +14 -4
  38. package/dist/cli/compose.js +29 -10
  39. package/dist/cli/default-model.d.ts +38 -1
  40. package/dist/cli/default-model.js +96 -9
  41. package/dist/cli/deps.d.ts +36 -0
  42. package/dist/cli/exit-codes.d.ts +2 -0
  43. package/dist/cli/exit-codes.js +3 -0
  44. package/dist/cli/fake-visibility.d.ts +13 -0
  45. package/dist/cli/fake-visibility.js +26 -0
  46. package/dist/cli/from-prompt.d.ts +18 -0
  47. package/dist/cli/from-prompt.js +49 -0
  48. package/dist/cli/main.d.ts +9 -2
  49. package/dist/cli/main.js +77 -3
  50. package/dist/cli/proxy.d.ts +51 -0
  51. package/dist/cli/proxy.js +135 -0
  52. package/dist/cli/startup-screen.d.ts +29 -0
  53. package/dist/cli/startup-screen.js +47 -0
  54. package/dist/cli/startup-steps.d.ts +1 -1
  55. package/dist/cli/startup-steps.js +14 -12
  56. package/dist/cli/subcommands/config.d.ts +19 -3
  57. package/dist/cli/subcommands/config.js +108 -21
  58. package/dist/cli/subcommands/context.js +4 -1
  59. package/dist/cli/subcommands/doctor.js +12 -1
  60. package/dist/cli/subcommands/init.js +2 -1
  61. package/dist/cli/subcommands/models-discover.d.ts +15 -9
  62. package/dist/cli/subcommands/models-discover.js +53 -46
  63. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  64. package/dist/cli/subcommands/probe-runner.js +264 -0
  65. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  66. package/dist/cli/subcommands/providers-probe.js +87 -0
  67. package/dist/cli/subcommands/providers.d.ts +3 -2
  68. package/dist/cli/subcommands/providers.js +48 -45
  69. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  70. package/dist/cli/subcommands/sessions-export.js +59 -0
  71. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  72. package/dist/cli/subcommands/sessions-search.js +103 -0
  73. package/dist/cli/subcommands/sessions.d.ts +3 -2
  74. package/dist/cli/subcommands/sessions.js +22 -1
  75. package/dist/cli/subcommands/stats.d.ts +17 -0
  76. package/dist/cli/subcommands/stats.js +198 -0
  77. package/dist/cli/system-prompt-arg.d.ts +11 -0
  78. package/dist/cli/system-prompt-arg.js +34 -0
  79. package/dist/codemode/modes.d.ts +4 -11
  80. package/dist/codemode/modes.js +5 -27
  81. package/dist/codemode/tool.d.ts +18 -14
  82. package/dist/codemode/tool.js +71 -28
  83. package/dist/config/init.d.ts +6 -2
  84. package/dist/config/init.js +12 -5
  85. package/dist/config/json-schema.d.ts +1 -0
  86. package/dist/config/json-schema.js +36 -3
  87. package/dist/config/key-docs.d.ts +22 -0
  88. package/dist/config/key-docs.js +105 -0
  89. package/dist/config/merge.d.ts +9 -8
  90. package/dist/config/merge.js +22 -7
  91. package/dist/config/schema.d.ts +1 -1
  92. package/dist/config/schema.js +24 -5
  93. package/dist/config/types.d.ts +34 -6
  94. package/dist/config/types.js +17 -1
  95. package/dist/modes/commands-core.js +6 -5
  96. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  97. package/dist/modes/interactive/approval-dialog.js +106 -27
  98. package/dist/modes/interactive/commands.d.ts +11 -2
  99. package/dist/modes/interactive/commands.js +56 -16
  100. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  101. package/dist/modes/interactive/interactive-mode.js +65 -81
  102. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  103. package/dist/modes/interactive/key-dispatch.js +5 -6
  104. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  105. package/dist/modes/interactive/line/line-mode.js +6 -4
  106. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  107. package/dist/modes/interactive/line/line-render.js +28 -6
  108. package/dist/modes/interactive/message-view.d.ts +48 -9
  109. package/dist/modes/interactive/message-view.js +238 -44
  110. package/dist/modes/interactive/panels.d.ts +18 -0
  111. package/dist/modes/interactive/panels.js +143 -0
  112. package/dist/modes/interactive/pickers.d.ts +23 -2
  113. package/dist/modes/interactive/pickers.js +48 -15
  114. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  115. package/dist/modes/interactive/run-indicator.js +189 -0
  116. package/dist/modes/interactive/startup-header.d.ts +40 -0
  117. package/dist/modes/interactive/startup-header.js +169 -0
  118. package/dist/modes/interactive/status-bar.d.ts +18 -15
  119. package/dist/modes/interactive/status-bar.js +98 -56
  120. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  121. package/dist/modes/interactive/tool-summary.js +218 -0
  122. package/dist/modes/interactive/tool-view.d.ts +48 -15
  123. package/dist/modes/interactive/tool-view.js +203 -145
  124. package/dist/modes/print/print-mode.d.ts +31 -4
  125. package/dist/modes/print/print-mode.js +116 -7
  126. package/dist/modes/rpc/commands.js +5 -0
  127. package/dist/permissions/auto-safe.d.ts +60 -0
  128. package/dist/permissions/auto-safe.js +529 -0
  129. package/dist/permissions/classifier.d.ts +64 -0
  130. package/dist/permissions/classifier.js +184 -0
  131. package/dist/permissions/dangerous.d.ts +5 -0
  132. package/dist/permissions/dangerous.js +1 -1
  133. package/dist/permissions/modes.d.ts +30 -0
  134. package/dist/permissions/modes.js +78 -0
  135. package/dist/permissions/pipeline.d.ts +31 -4
  136. package/dist/permissions/pipeline.js +196 -6
  137. package/dist/permissions/protected.d.ts +19 -0
  138. package/dist/permissions/protected.js +74 -0
  139. package/dist/permissions/rules.js +3 -0
  140. package/dist/permissions/types.d.ts +50 -3
  141. package/dist/permissions/types.js +3 -0
  142. package/dist/sdk.d.ts +9 -3
  143. package/dist/sdk.js +10 -2
  144. package/dist/session/export.d.ts +32 -0
  145. package/dist/session/export.js +187 -0
  146. package/dist/session/redact.d.ts +15 -0
  147. package/dist/session/redact.js +55 -0
  148. package/dist/session/reuse.d.ts +33 -0
  149. package/dist/session/reuse.js +86 -0
  150. package/dist/session/scan.d.ts +34 -0
  151. package/dist/session/scan.js +140 -0
  152. package/dist/session/search.d.ts +52 -0
  153. package/dist/session/search.js +211 -0
  154. package/dist/session/stats-aggregate.d.ts +63 -0
  155. package/dist/session/stats-aggregate.js +163 -0
  156. package/dist/session/stats-index.d.ts +26 -0
  157. package/dist/session/stats-index.js +91 -0
  158. package/dist/session/stats-scan.d.ts +54 -0
  159. package/dist/session/stats-scan.js +236 -0
  160. package/dist/tools/presets.d.ts +35 -8
  161. package/dist/tools/presets.js +56 -17
  162. package/dist/tui/component.d.ts +8 -2
  163. package/dist/tui/component.js +3 -1
  164. package/dist/tui/components/box.d.ts +6 -1
  165. package/dist/tui/components/box.js +16 -6
  166. package/dist/tui/components/card.d.ts +23 -0
  167. package/dist/tui/components/card.js +37 -0
  168. package/dist/tui/components/editor-history.d.ts +6 -0
  169. package/dist/tui/components/editor-history.js +45 -0
  170. package/dist/tui/components/editor-paste.d.ts +1 -1
  171. package/dist/tui/components/editor-paste.js +4 -4
  172. package/dist/tui/components/editor.d.ts +11 -5
  173. package/dist/tui/components/editor.js +52 -58
  174. package/dist/tui/components/key-value.d.ts +3 -0
  175. package/dist/tui/components/key-value.js +16 -6
  176. package/dist/tui/components/loader.d.ts +37 -7
  177. package/dist/tui/components/loader.js +84 -21
  178. package/dist/tui/components/markdown.d.ts +5 -1
  179. package/dist/tui/components/markdown.js +45 -21
  180. package/dist/tui/components/meter.d.ts +3 -3
  181. package/dist/tui/components/meter.js +13 -11
  182. package/dist/tui/components/select-list.d.ts +23 -1
  183. package/dist/tui/components/select-list.js +76 -13
  184. package/dist/tui/glyphs.d.ts +68 -0
  185. package/dist/tui/glyphs.js +114 -0
  186. package/dist/tui/theme.d.ts +30 -6
  187. package/dist/tui/theme.js +103 -17
  188. package/dist/tui.d.ts +4 -2
  189. package/dist/tui.js +3 -1
  190. package/docs/codemode.md +23 -9
  191. package/docs/hooks.md +10 -10
  192. package/docs/permissions.md +148 -0
  193. package/docs/providers.md +33 -10
  194. package/docs/rpc.md +36 -36
  195. package/docs/session-format.md +2 -1
  196. package/docs/sessions.md +134 -0
  197. package/docs/tui.md +139 -62
  198. package/package.json +3 -1
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `ama providers add|refresh --probe` 的逐渠道探测:任务是「模型 × 渠道」,有界并发执行(见
3
+ * probe-runner.ts);每个模型的所有渠道探完后按模型顺序打印一行,不按完成顺序。
4
+ */
5
+ import type { ProviderRegistryApi } from "../../ai/types.js";
6
+ import type { CliIo } from "../deps.js";
7
+ import type { CandidateChannel } from "./providers-plan.js";
8
+ export interface ChannelProbeResult {
9
+ /** 探测成功的渠道(按渠道尝试顺序)。 */
10
+ ok: string[];
11
+ /** 最后一个失败渠道的原因(首行,≤ 120 字符)。 */
12
+ error?: string;
13
+ }
14
+ export interface ChannelProbeInput {
15
+ io: Pick<CliIo, "stdout" | "stderr" | "stdoutIsTTY">;
16
+ providerId: string;
17
+ ids: readonly string[];
18
+ /** 每个模型要试的渠道名(顺序即输出顺序)。 */
19
+ channelsFor: (id: string) => readonly string[];
20
+ candidates: readonly CandidateChannel[];
21
+ registry: ProviderRegistryApi;
22
+ apiKey: string | undefined;
23
+ concurrency: number;
24
+ timeoutMs: number;
25
+ /** 测试用:429 重试前的退避。 */
26
+ retryDelayMs?: number;
27
+ }
28
+ /**
29
+ * 探测全部「模型 × 渠道」。返回有结论的模型(全部渠道探完,或因停止没探完但已有成功渠道)与停止原因。
30
+ */
31
+ export declare function probeChannels(input: ChannelProbeInput): Promise<{
32
+ results: Map<string, ChannelProbeResult>;
33
+ stopped?: string;
34
+ }>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `ama providers add|refresh --probe` 的逐渠道探测:任务是「模型 × 渠道」,有界并发执行(见
3
+ * probe-runner.ts);每个模型的所有渠道探完后按模型顺序打印一行,不按完成顺序。
4
+ */
5
+ import { withCustomDefaults } from "../../ai/providers/catalog.js";
6
+ import { materializeModel } from "../../ai/providers/registry.js";
7
+ import { ProbeProgress, ProbeScheduler } from "./probe-runner.js";
8
+ /** 渠道的探测用供应商数据(key 由调用方传入)。 */
9
+ function probeProvider(id, channel) {
10
+ return {
11
+ id,
12
+ name: id,
13
+ api: channel.api,
14
+ baseUrl: channel.baseUrl,
15
+ envKeys: [],
16
+ models: [],
17
+ requiresApiKey: true,
18
+ builtin: false,
19
+ };
20
+ }
21
+ const firstLine = (error) => error.split("\n")[0]?.slice(0, 120) ?? error;
22
+ /**
23
+ * 探测全部「模型 × 渠道」。返回有结论的模型(全部渠道探完,或因停止没探完但已有成功渠道)与停止原因。
24
+ */
25
+ export async function probeChannels(input) {
26
+ const { io } = input;
27
+ const tasks = input.ids.flatMap((id) => input.channelsFor(id).map((name) => ({ id, name })));
28
+ const pending = new Map(input.ids.map((id) => [id, input.channelsFor(id).length]));
29
+ const outcomes = new Map();
30
+ const results = new Map();
31
+ const progress = new ProbeProgress(io.stdout, io.stdoutIsTTY, tasks.length);
32
+ const scheduler = new ProbeScheduler(input.registry, input.apiKey, {
33
+ concurrency: input.concurrency,
34
+ timeoutMs: input.timeoutMs,
35
+ retryDelayMs: input.retryDelayMs,
36
+ onThrottle: (n) => progress.line(` 遇到 429 限流,并发降到 ${n},稍后重试一次\n`),
37
+ onRecover: (n) => progress.line(` 一段时间没再限流,并发回升到 ${n}\n`),
38
+ });
39
+ let printed = 0;
40
+ /** 按模型顺序输出:前面的模型都探完才输出后面的。 */
41
+ const flush = (final) => {
42
+ while (printed < input.ids.length) {
43
+ const id = input.ids[printed];
44
+ if (!final && (pending.get(id) ?? 0) > 0)
45
+ return;
46
+ printed++;
47
+ const result = { ok: [] };
48
+ const failed = [];
49
+ let complete = true;
50
+ for (const name of input.channelsFor(id)) {
51
+ const outcome = outcomes.get(`${id}\n${name}`);
52
+ if (outcome === undefined || outcome.aborted)
53
+ complete = false;
54
+ else if (outcome.error === undefined)
55
+ result.ok.push(name);
56
+ else {
57
+ result.error = firstLine(outcome.error);
58
+ failed.push(`${name}:${result.error}`);
59
+ }
60
+ }
61
+ if (!complete && result.ok.length === 0)
62
+ continue;
63
+ results.set(id, result);
64
+ progress.line(` ${id} ${result.ok.length > 0 ? result.ok.join(", ") : "全部失败"}` +
65
+ `${failed.length > 0 ? `(失败 ${failed.join(";")})` : ""}` +
66
+ `${complete ? "" : "(探测提前停止,部分渠道未探)"}\n`);
67
+ }
68
+ };
69
+ await scheduler.run(tasks.length, async (index) => {
70
+ const { id, name } = tasks[index];
71
+ const channel = input.candidates.find((c) => c.name === name);
72
+ const provider = probeProvider(input.providerId, channel);
73
+ const model = materializeModel(withCustomDefaults({ id }, input.providerId, channel.api), provider);
74
+ return scheduler.probe(model);
75
+ }, (index, outcome) => {
76
+ const { id, name } = tasks[index];
77
+ outcomes.set(`${id}\n${name}`, outcome);
78
+ pending.set(id, (pending.get(id) ?? 1) - 1);
79
+ if (!outcome.aborted)
80
+ progress.tick();
81
+ flush(false);
82
+ });
83
+ flush(true);
84
+ progress.finish();
85
+ const stopped = scheduler.stopped;
86
+ return { results, ...(stopped !== undefined ? { stopped } : {}) };
87
+ }
@@ -7,14 +7,15 @@
7
7
  * - refresh:重拉 `/models` 与 models.dev,只追加新模型;已下架的只提示。
8
8
  * - list / channels:供应商 → 渠道 → 模型数与 key 来源(从不显示 key)。remove:删配置与 auth.json 条目。
9
9
  *
10
- * 计费:`--probe` 每模型每渠道一次最小请求;执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
10
+ * 计费:`--probe` 每模型每渠道一次最小请求(并发执行,见到首个流事件即判可用并断开,见
11
+ * probe-runner.ts);执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
11
12
  */
12
13
  import type { AmaConfig } from "../../config/types.js";
13
14
  import type { CliIo, RuntimeDeps } from "../deps.js";
14
15
  import { type UserLevel } from "./context.js";
15
16
  export declare const DEFAULT_MAX_REQUESTS = 60;
16
17
  export declare const DEFAULT_LIMIT = 30;
17
- export declare const PROVIDERS_USAGE = "\u7528\u6CD5\uFF1Aama providers add <id> --base-url <url> [--channel <\u540D\u5B57>=<\u534F\u8BAE>@<\u5730\u5740> \u2026]\n [--api openai-completions|openai-responses|anthropic-messages|auto]\n [--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,\u2026]\n [--max-requests N] [--prefer chat,responses,messages]\n [--include-no-tools] [--yes]\n ama providers list\n ama providers channels <id>\n ama providers remove <id>\n ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,\u2026] [--max-requests N] [--yes]\n";
18
+ export declare const PROVIDERS_USAGE = "\u7528\u6CD5\uFF1Aama providers add <id> --base-url <url> [--channel <\u540D\u5B57>=<\u534F\u8BAE>@<\u5730\u5740> \u2026]\n [--api openai-completions|openai-responses|anthropic-messages|auto]\n [--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,\u2026]\n [--max-requests N] [--concurrency N] [--probe-timeout ms]\n [--prefer chat,responses,messages] [--include-no-tools] [--yes]\n ama providers list\n ama providers channels <id>\n ama providers remove <id>\n ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,\u2026] [--max-requests N]\n [--concurrency N] [--probe-timeout ms] [--yes]\n";
18
19
  export interface Ctx {
19
20
  io: CliIo;
20
21
  deps: Pick<RuntimeDeps, "providers">;
@@ -7,36 +7,39 @@
7
7
  * - refresh:重拉 `/models` 与 models.dev,只追加新模型;已下架的只提示。
8
8
  * - list / channels:供应商 → 渠道 → 模型数与 key 来源(从不显示 key)。remove:删配置与 auth.json 条目。
9
9
  *
10
- * 计费:`--probe` 每模型每渠道一次最小请求;执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
10
+ * 计费:`--probe` 每模型每渠道一次最小请求(并发执行,见到首个流事件即判可用并断开,见
11
+ * probe-runner.ts);执行前打印预估,TTY 问 y/N,非 TTY 必须 `--yes`。
11
12
  */
12
13
  import { existsSync } from "node:fs";
13
14
  import { createInterface } from "node:readline/promises";
14
- import { withCustomDefaults } from "../../ai/providers/catalog.js";
15
15
  import { describeRefresh, refreshModelsDev } from "../../ai/providers/models-dev-cache.js";
16
16
  import { modelsDevFields } from "../../ai/providers/models-dev.js";
17
- import { materializeModel } from "../../ai/providers/registry.js";
18
17
  import { PROVIDER_ID_PATTERN, setAuthKey } from "../../config/auth-file.js";
19
18
  import { loadConfigFile } from "../../config/load.js";
20
19
  import { CONFIG_FILE_VERSION } from "../../config/types.js";
21
20
  import { writeConfigFile } from "../../config/write.js";
22
21
  import { parseSubArgs, UsageError } from "../args.js";
22
+ import { pickByPrice } from "../default-model.js";
23
23
  import { ExitCode } from "../exit-codes.js";
24
24
  import { extractKey } from "./auth.js";
25
25
  import { listChannels, listProviders, removeProvider } from "./providers-list.js";
26
26
  import { buildRegistry, loadUserLevel } from "./context.js";
27
- import { attempt, DISCOVER_TIMEOUT_MS, FATAL_STATUS } from "./models-discover.js";
27
+ import { DISCOVER_TIMEOUT_MS } from "./models-discover.js";
28
+ import { describeProbePlan, parseProbeTuning } from "./probe-runner.js";
29
+ import { probeChannels } from "./providers-probe.js";
28
30
  import { DEFAULT_PREFER, capRequests, defaultChannels, hintedChannels, listingTarget, mergeProvider, orderChannels, parseChannelSpec, parseModelList, probeSelection, renderTable, } from "./providers-plan.js";
29
31
  export const DEFAULT_MAX_REQUESTS = 60;
30
32
  export const DEFAULT_LIMIT = 30;
31
33
  export const PROVIDERS_USAGE = `用法:ama providers add <id> --base-url <url> [--channel <名字>=<协议>@<地址> …]
32
34
  [--api openai-completions|openai-responses|anthropic-messages|auto]
33
35
  [--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,…]
34
- [--max-requests N] [--prefer chat,responses,messages]
35
- [--include-no-tools] [--yes]
36
+ [--max-requests N] [--concurrency N] [--probe-timeout ms]
37
+ [--prefer chat,responses,messages] [--include-no-tools] [--yes]
36
38
  ama providers list
37
39
  ama providers channels <id>
38
40
  ama providers remove <id>
39
- ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,…] [--max-requests N] [--yes]
41
+ ama providers refresh <id> [--probe] [--limit N] [--probe-models a,b,…] [--max-requests N]
42
+ [--concurrency N] [--probe-timeout ms] [--yes]
40
43
  `;
41
44
  const VALUE_OPTIONS = [
42
45
  "base-url",
@@ -45,6 +48,8 @@ const VALUE_OPTIONS = [
45
48
  "limit",
46
49
  "probe-models",
47
50
  "max-requests",
51
+ "concurrency",
52
+ "probe-timeout",
48
53
  "prefer",
49
54
  "name",
50
55
  "profile",
@@ -119,19 +124,6 @@ function channelsOfConfig(config) {
119
124
  const api = config.api ?? "openai-completions";
120
125
  return [{ name: "default", api, baseUrl: config.baseUrl }];
121
126
  }
122
- /** 渠道的探测用供应商数据(key 由调用方传入)。 */
123
- function probeProvider(id, channel) {
124
- return {
125
- id,
126
- name: id,
127
- api: channel.api,
128
- baseUrl: channel.baseUrl,
129
- envKeys: [],
130
- models: [],
131
- requiresApiKey: true,
132
- builtin: false,
133
- };
134
- }
135
127
  /** 逐模型定渠道(含探测);返回各模型的计划与是否因鉴权 / 限流提前停止。 */
136
128
  async function plan(ctx, input) {
137
129
  const { io } = ctx;
@@ -151,36 +143,28 @@ async function plan(ctx, input) {
151
143
  const selected = probeSelection(fresh.map((m) => m.id).filter(probeable), intValue(ctx, "limit", DEFAULT_LIMIT), only);
152
144
  const max = intValue(ctx, "max-requests", DEFAULT_MAX_REQUESTS);
153
145
  const capped = capRequests(selected, tryChannels, max);
146
+ const tuning = parseProbeTuning(ctx.values);
154
147
  io.stdout(`\n探测:${capped.ids.length} 个模型、${capped.requests} 次最小请求(每模型每渠道 1 次,上限 ${max})` +
155
- `${capped.dropped > 0 ? `;另有 ${capped.dropped} 个超出上限未探测` : ""}\n`);
148
+ `${capped.dropped > 0 ? `;另有 ${capped.dropped} 个超出上限未探测` : ""}\n` +
149
+ `${describeProbePlan(capped.requests, tuning.concurrency, tuning.timeoutMs)}\n`);
156
150
  const ok = await approved(ctx, `将发 ${capped.requests} 次计费请求并写入 ${ctx.level.userConfigPath}`);
157
151
  if (ok === "usage")
158
152
  return { plans: [], cancelled: ExitCode.Usage };
159
153
  if (!ok)
160
154
  return { plans: [], cancelled: ExitCode.Ok };
161
- outer: for (const id of capped.ids) {
162
- const result = { ok: [] };
163
- const failed = [];
155
+ const probed = await probeChannels({
156
+ io,
157
+ providerId: input.id,
158
+ ids: capped.ids,
159
+ channelsFor: tryChannels,
160
+ candidates: input.candidates,
161
+ registry: input.registry,
162
+ apiKey: input.apiKey,
163
+ ...tuning,
164
+ });
165
+ for (const [id, result] of probed.results)
164
166
  results.set(id, result);
165
- for (const name of tryChannels(id)) {
166
- const channel = input.candidates.find((c) => c.name === name);
167
- const provider = probeProvider(input.id, channel);
168
- const model = materializeModel(withCustomDefaults({ id }, input.id, channel.api), provider);
169
- const error = await attempt(input.registry, model, input.apiKey);
170
- if (error === undefined)
171
- result.ok.push(name);
172
- else {
173
- result.error = error.split("\n")[0]?.slice(0, 120) ?? error;
174
- failed.push(`${name}:${result.error}`);
175
- if (FATAL_STATUS.test(error)) {
176
- stopped = error;
177
- break outer;
178
- }
179
- }
180
- }
181
- io.stdout(` ${id} ${result.ok.length > 0 ? result.ok.join(", ") : "全部失败"}` +
182
- `${failed.length > 0 ? `(失败 ${failed.join(";")})` : ""}\n`);
183
- }
167
+ stopped = probed.stopped;
184
168
  }
185
169
  const includeNoTools = ctx.flags.has("include-no-tools");
186
170
  const plans = input.listed.map((listed) => {
@@ -314,9 +298,25 @@ async function add(ctx, id, refresh) {
314
298
  prefer: list(ctx.values.get("prefer")) ?? [...DEFAULT_PREFER],
315
299
  });
316
300
  const keyNote = key.store !== undefined ? `;key → ${level.authFile}(0600)` : "";
301
+ // 还没有缺省模型时按价格规则挑一个写进 defaultModel(default-model.ts 的 pickByPrice);
302
+ // 探测过时只在探测通过的模型里挑
303
+ const verified = writable.filter((p) => p.status === "ok");
304
+ const defaultPick = level.merged.config.defaultModel === undefined && config.defaultModel === undefined
305
+ ? pickByPrice((verified.length > 0 ? verified : writable).map((p) => ({
306
+ id: p.id,
307
+ contextWindow: p.fields.contextWindow,
308
+ inputCost: p.fields.cost?.input,
309
+ toolCall: p.fields.toolCall,
310
+ })))
311
+ : undefined;
312
+ const defaultNote = defaultPick !== undefined ? `;defaultModel → ${id}/${defaultPick.id}` : "";
317
313
  const summary = `\n将写入 ${level.userConfigPath}:${id} 新增渠道 ${merged.addedChannels.join(", ") || "无"},` +
318
- `新增模型 ${merged.addedModels.length} 个${keyNote}`;
314
+ `新增模型 ${merged.addedModels.length} 个${keyNote}${defaultNote}`;
319
315
  io.stdout(`${summary}\n`);
316
+ if (defaultPick !== undefined)
317
+ io.stdout(` 选 ${defaultPick.id}:${defaultPick.reason}\n`);
318
+ else if (level.merged.config.defaultModel === undefined && writable.length > 0)
319
+ io.stdout(" 未设置 defaultModel:没有同时支持工具调用、上下文 ≥ 64k 且有价格的模型;用 ama config edit 设置\n");
320
320
  if (merged.addedModels.length === 0 &&
321
321
  merged.addedChannels.length === 0 &&
322
322
  key.store === undefined) {
@@ -334,12 +334,15 @@ async function add(ctx, id, refresh) {
334
334
  }
335
335
  const providers = (config.providers ??= {});
336
336
  providers[id] = merged.config;
337
+ if (defaultPick !== undefined)
338
+ config.defaultModel = `${id}/${defaultPick.id}`;
337
339
  const backup = existsSync(level.userConfigPath);
338
340
  writeConfigFile(level.userConfigPath, config, { backup: true });
339
341
  if (key.store !== undefined)
340
342
  setAuthKey(level.authFile, id, key.store);
341
343
  io.stdout(`已写入 ${level.userConfigPath}${backup ? "(原文件备份为 config.json.bak)" : ""}\n`);
342
- const sample = result.plans.find((p) => p.status === "ok" && merged.addedModels.includes(p.id))?.id ??
344
+ const sample = defaultPick?.id ??
345
+ result.plans.find((p) => p.status === "ok" && merged.addedModels.includes(p.id))?.id ??
343
346
  merged.addedModels[0];
344
347
  if (sample !== undefined)
345
348
  io.stdout(`试试:ama -p "hi" --model ${id}/${sample}\n`);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `ama sessions export <id>`:导出会话(只读,不加锁;正在运行的会话也能导)。[W4-D]
3
+ *
4
+ * - `--format md|json|jsonl`(缺省 md);`--branch leaf|all`(缺省 leaf = 当前分支);
5
+ * - `--output <文件>`:写文件(0600,已存在则覆盖),否则写 stdout;
6
+ * - 导出内容先脱敏(session/redact.ts),格式说明见 docs/sessions.md「导出」。
7
+ */
8
+ import type { CliIo } from "../deps.js";
9
+ export declare const SESSIONS_EXPORT_USAGE = "\u7528\u6CD5\uFF1Aama sessions export <id> [--format md|json|jsonl] [--output <\u6587\u4EF6>] [--branch leaf|all]\n [--session-dir <\u76EE\u5F55>]\n";
10
+ export declare function runSessionsExport(argv: readonly string[], io: CliIo): Promise<number>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `ama sessions export <id>`:导出会话(只读,不加锁;正在运行的会话也能导)。[W4-D]
3
+ *
4
+ * - `--format md|json|jsonl`(缺省 md);`--branch leaf|all`(缺省 leaf = 当前分支);
5
+ * - `--output <文件>`:写文件(0600,已存在则覆盖),否则写 stdout;
6
+ * - 导出内容先脱敏(session/redact.ts),格式说明见 docs/sessions.md「导出」。
7
+ */
8
+ import { writeFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { expandHome, resolveDataDir } from "../../config/paths.js";
11
+ import { renderExport } from "../../session/export.js";
12
+ import { findSessionFileReadOnly, readSessionReadOnly } from "../../session/scan.js";
13
+ import { parseSubArgs, UsageError } from "../args.js";
14
+ import { ExitCode } from "../exit-codes.js";
15
+ export const SESSIONS_EXPORT_USAGE = `用法:ama sessions export <id> [--format md|json|jsonl] [--output <文件>] [--branch leaf|all]
16
+ [--session-dir <目录>]
17
+ `;
18
+ const FORMATS = ["md", "json", "jsonl"];
19
+ const BRANCHES = ["leaf", "all"];
20
+ export async function runSessionsExport(argv, io) {
21
+ const { positionals, values, flags } = parseSubArgs(argv, [
22
+ "format",
23
+ "output",
24
+ "branch",
25
+ "session-dir",
26
+ ]);
27
+ if (flags.has("help")) {
28
+ io.stdout(SESSIONS_EXPORT_USAGE);
29
+ return ExitCode.Ok;
30
+ }
31
+ const [id, extra] = positionals;
32
+ if (id === undefined)
33
+ throw new UsageError("ama sessions export 需要 <id>");
34
+ if (extra !== undefined)
35
+ throw new UsageError(`多余的参数:${extra}`);
36
+ const format = (values.get("format") ?? "md");
37
+ if (!FORMATS.includes(format)) {
38
+ throw new UsageError(`--format 的取值应为 ${FORMATS.join(" | ")}(收到 ${format})`);
39
+ }
40
+ const branch = (values.get("branch") ?? "leaf");
41
+ if (!BRANCHES.includes(branch)) {
42
+ throw new UsageError(`--branch 的取值应为 ${BRANCHES.join(" | ")}(收到 ${branch})`);
43
+ }
44
+ const dirFlag = values.get("session-dir");
45
+ const root = dirFlag !== undefined
46
+ ? resolve(io.cwd, expandHome(dirFlag, { env: io.env }))
47
+ : join(resolveDataDir({ env: io.env }), "sessions");
48
+ const session = readSessionReadOnly(findSessionFileReadOnly(root, id, io.cwd));
49
+ const text = renderExport(session, format, branch);
50
+ const output = values.get("output");
51
+ if (output === undefined) {
52
+ io.stdout(text);
53
+ return ExitCode.Ok;
54
+ }
55
+ const target = resolve(io.cwd, expandHome(output, { env: io.env }));
56
+ writeFileSync(target, text, { mode: 0o600 });
57
+ io.stderr(`已导出到 ${target}\n`);
58
+ return ExitCode.Ok;
59
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `ama sessions search <关键词|/正则/>`:跨会话全文检索(只读)。[W4-D]
3
+ *
4
+ * - 范围:缺省当前目录的会话,`--all` 全部;`--role user|assistant|tool`(可逗号分隔多个);
5
+ * `--since 7d|today|YYYY-MM-DD`;`--limit N`(缺省 20);`--json` 每条命中一行 JSON。
6
+ * - 输出一行一条:会话 id 前 8 位、条目时间、项目、角色与编号(user 是 `#n`,可直接给 `--from <id>#n`;
7
+ * 其它角色是条目序号 `@k`)、命中片段。stdout 是 TTY 且没设 NO_COLOR 时用 ANSI 高亮,否则纯文本。
8
+ */
9
+ import { type SearchHit } from "../../session/search.js";
10
+ import type { CliIo } from "../deps.js";
11
+ export declare const SESSIONS_SEARCH_USAGE = "\u7528\u6CD5\uFF1Aama sessions search <\u5173\u952E\u8BCD|/\u6B63\u5219/\u6807\u5FD7> [--all] [--role user|assistant|tool]\n [--since 7d|today|YYYY-MM-DD] [--limit N] [--json] [--session-dir <\u76EE\u5F55>]\n";
12
+ export declare function renderHit(hit: SearchHit, color: boolean): string;
13
+ export declare function runSessionsSearch(argv: readonly string[], io: CliIo): Promise<number>;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `ama sessions search <关键词|/正则/>`:跨会话全文检索(只读)。[W4-D]
3
+ *
4
+ * - 范围:缺省当前目录的会话,`--all` 全部;`--role user|assistant|tool`(可逗号分隔多个);
5
+ * `--since 7d|today|YYYY-MM-DD`;`--limit N`(缺省 20);`--json` 每条命中一行 JSON。
6
+ * - 输出一行一条:会话 id 前 8 位、条目时间、项目、角色与编号(user 是 `#n`,可直接给 `--from <id>#n`;
7
+ * 其它角色是条目序号 `@k`)、命中片段。stdout 是 TTY 且没设 NO_COLOR 时用 ANSI 高亮,否则纯文本。
8
+ */
9
+ import { join, resolve } from "node:path";
10
+ import { expandHome, resolveDataDir } from "../../config/paths.js";
11
+ import { sessionFilesInScope } from "../../session/scan.js";
12
+ import { compileQuery, searchSessions, } from "../../session/search.js";
13
+ import { parseSubArgs, UsageError } from "../args.js";
14
+ import { ExitCode } from "../exit-codes.js";
15
+ import { parseDaySpec } from "./stats.js";
16
+ export const SESSIONS_SEARCH_USAGE = `用法:ama sessions search <关键词|/正则/标志> [--all] [--role user|assistant|tool]
17
+ [--since 7d|today|YYYY-MM-DD] [--limit N] [--json] [--session-dir <目录>]
18
+ `;
19
+ const ROLES = ["user", "assistant", "tool"];
20
+ function highlight(text, ranges, color) {
21
+ if (!color || ranges.length === 0)
22
+ return text;
23
+ let out = "";
24
+ let at = 0;
25
+ for (const [a, b] of ranges) {
26
+ if (a < at)
27
+ continue;
28
+ out += text.slice(at, a) + `\x1b[1;33m${text.slice(a, b)}\x1b[0m`;
29
+ at = b;
30
+ }
31
+ return out + text.slice(at);
32
+ }
33
+ function shortTime(iso) {
34
+ return iso
35
+ .replace("T", " ")
36
+ .replace(/\.\d+Z$|Z$/, "")
37
+ .slice(0, 16);
38
+ }
39
+ export function renderHit(hit, color) {
40
+ const ref = hit.userN !== undefined ? `#${hit.userN}` : `@${hit.entryIndex}`;
41
+ const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
42
+ return `${hit.sessionId.slice(0, 8)}${ref.padEnd(4)} ${dim(shortTime(hit.timestamp))} ${dim(hit.cwd)} ${hit.role.padEnd(9)} ${highlight(hit.snippet, hit.ranges, color)}\n`;
43
+ }
44
+ export async function runSessionsSearch(argv, io) {
45
+ const { positionals, values, flags } = parseSubArgs(argv, ["role", "since", "limit", "session-dir"], ["all", "json"]);
46
+ if (flags.has("help")) {
47
+ io.stdout(SESSIONS_SEARCH_USAGE);
48
+ return ExitCode.Ok;
49
+ }
50
+ const pattern = positionals.join(" ");
51
+ if (pattern.trim() === "")
52
+ throw new UsageError("ama sessions search 需要关键词或 /正则/");
53
+ let query;
54
+ try {
55
+ query = compileQuery(pattern);
56
+ }
57
+ catch (error) {
58
+ throw new UsageError(`正则无效:${error.message}`);
59
+ }
60
+ let roles;
61
+ const roleRaw = values.get("role");
62
+ if (roleRaw !== undefined) {
63
+ roles = new Set();
64
+ for (const role of roleRaw.split(",").map((r) => r.trim())) {
65
+ if (!ROLES.includes(role)) {
66
+ throw new UsageError(`--role 的取值应为 ${ROLES.join(" | ")}(收到 ${role})`);
67
+ }
68
+ roles.add(role);
69
+ }
70
+ }
71
+ const limit = Number(values.get("limit") ?? "20");
72
+ if (!Number.isInteger(limit) || limit < 1)
73
+ throw new UsageError("--limit 应为正整数");
74
+ const sinceRaw = values.get("since");
75
+ const sinceIso = sinceRaw === undefined
76
+ ? undefined
77
+ : new Date(`${parseDaySpec("since", sinceRaw)}T00:00:00`).toISOString();
78
+ const dirFlag = values.get("session-dir");
79
+ const root = dirFlag !== undefined
80
+ ? resolve(io.cwd, expandHome(dirFlag, { env: io.env }))
81
+ : join(resolveDataDir({ env: io.env }), "sessions");
82
+ const files = sessionFilesInScope(root, flags.has("all") ? undefined : io.cwd);
83
+ const { hits, truncated } = searchSessions(files, query, {
84
+ limit,
85
+ ...(roles !== undefined ? { roles } : {}),
86
+ ...(sinceIso !== undefined ? { sinceIso } : {}),
87
+ });
88
+ if (flags.has("json")) {
89
+ for (const hit of hits)
90
+ io.stdout(`${JSON.stringify(hit)}\n`);
91
+ return ExitCode.Ok;
92
+ }
93
+ if (hits.length === 0) {
94
+ io.stdout(flags.has("all") ? "没有命中\n" : "没有命中(只搜了当前目录的会话,--all 搜全部)\n");
95
+ return ExitCode.Ok;
96
+ }
97
+ const color = io.stdoutIsTTY && (io.env["NO_COLOR"] ?? "") === "";
98
+ for (const hit of hits)
99
+ io.stdout(renderHit(hit, color));
100
+ if (truncated)
101
+ io.stdout(`(已到 --limit ${limit},可能还有更多)\n`);
102
+ return ExitCode.Ok;
103
+ }
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * 会话存储是 B2 的实现,经 RuntimeDeps.sessions 注入;这里只做参数、目录解析与输出格式。
5
5
  * - list [--all]:缺省只列当前目录的会话;`--all` 列全部。
6
- * - show <id>:头信息 + 条目类型统计 + 首条提示。
6
+ * - show <id>:头信息 + 条目类型统计 + 首条提示 + 用户消息编号(`--from <id>#<编号>` 复用)。
7
+ * - search / export:见 sessions-search.ts、sessions-export.ts(只读扫描,不经会话存储)。
7
8
  * - prune [--older-than <天>] [--dry-run]:缺省 30 天,移到 trash(不删除)。
8
9
  */
9
10
  import type { CliIo, RuntimeDeps } from "../deps.js";
10
- export declare const SESSIONS_USAGE = "\u7528\u6CD5\uFF1Aama sessions list [--all] [--session-dir <\u76EE\u5F55>]\n ama sessions show <id> [--session-dir <\u76EE\u5F55>]\n ama sessions prune [--older-than <\u5929>] [--dry-run] [--all] [--session-dir <\u76EE\u5F55>]\n";
11
+ export declare const SESSIONS_USAGE = "\u7528\u6CD5\uFF1Aama sessions list [--all] [--session-dir <\u76EE\u5F55>]\n ama sessions show <id> [--session-dir <\u76EE\u5F55>]\n ama sessions prune [--older-than <\u5929>] [--dry-run] [--all] [--session-dir <\u76EE\u5F55>]\n ama sessions search <\u5173\u952E\u8BCD|/\u6B63\u5219/> [--all] [--role user|assistant|tool] [--since 7d] [--limit N]\n ama sessions export <id> [--format md|json|jsonl] [--output <\u6587\u4EF6>] [--branch leaf|all]\n";
11
12
  export declare function runSessions(argv: readonly string[], io: CliIo, deps: Pick<RuntimeDeps, "sessions"> | undefined): Promise<number>;
@@ -3,16 +3,22 @@
3
3
  *
4
4
  * 会话存储是 B2 的实现,经 RuntimeDeps.sessions 注入;这里只做参数、目录解析与输出格式。
5
5
  * - list [--all]:缺省只列当前目录的会话;`--all` 列全部。
6
- * - show <id>:头信息 + 条目类型统计 + 首条提示。
6
+ * - show <id>:头信息 + 条目类型统计 + 首条提示 + 用户消息编号(`--from <id>#<编号>` 复用)。
7
+ * - search / export:见 sessions-search.ts、sessions-export.ts(只读扫描,不经会话存储)。
7
8
  * - prune [--older-than <天>] [--dry-run]:缺省 30 天,移到 trash(不删除)。
8
9
  */
9
10
  import { resolve } from "node:path";
10
11
  import { expandHome, resolveDataDir } from "../../config/paths.js";
11
12
  import { parseSubArgs, UsageError } from "../args.js";
12
13
  import { ExitCode } from "../exit-codes.js";
14
+ import { numberUserMessages } from "../../session/reuse.js";
15
+ import { runSessionsExport } from "./sessions-export.js";
16
+ import { runSessionsSearch } from "./sessions-search.js";
13
17
  export const SESSIONS_USAGE = `用法:ama sessions list [--all] [--session-dir <目录>]
14
18
  ama sessions show <id> [--session-dir <目录>]
15
19
  ama sessions prune [--older-than <天>] [--dry-run] [--all] [--session-dir <目录>]
20
+ ama sessions search <关键词|/正则/> [--all] [--role user|assistant|tool] [--since 7d] [--limit N]
21
+ ama sessions export <id> [--format md|json|jsonl] [--output <文件>] [--branch leaf|all]
16
22
  `;
17
23
  function shortTime(iso) {
18
24
  return iso.replace("T", " ").replace(/\.\d+Z$|Z$/, "");
@@ -24,6 +30,10 @@ function oneLine(text, max = 60) {
24
30
  return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
25
31
  }
26
32
  export async function runSessions(argv, io, deps) {
33
+ if (argv[0] === "search")
34
+ return runSessionsSearch(argv.slice(1), io);
35
+ if (argv[0] === "export")
36
+ return runSessionsExport(argv.slice(1), io);
27
37
  const { positionals, values, flags } = parseSubArgs(argv, ["session-dir", "older-than"], ["all", "dry-run"]);
28
38
  const action = positionals[0];
29
39
  if (flags.has("help") || action === undefined) {
@@ -73,6 +83,17 @@ export async function runSessions(argv, io, deps) {
73
83
  ? [`首条提示:${oneLine(item.firstPrompt, 200)}`]
74
84
  : []),
75
85
  ].join("\n") + "\n");
86
+ const users = numberUserMessages(entries);
87
+ if (users.length > 0) {
88
+ io.stdout(`用户消息(ama --from ${item.id.slice(0, 8)}#<编号> 复用):\n`);
89
+ for (const user of users) {
90
+ const tags = [
91
+ ...(user.origin !== undefined ? [`[${user.origin}]`] : []),
92
+ ...(user.images.length > 0 ? [`[图片 ${user.images.length}]`] : []),
93
+ ];
94
+ io.stdout(` #${String(user.n).padEnd(3)} ${shortTime(user.timestamp)} ${[...tags, oneLine(user.text, 100)].join(" ")}\n`);
95
+ }
96
+ }
76
97
  return ExitCode.Ok;
77
98
  }
78
99
  case "prune": {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `ama stats`:跨会话的用量统计(只读扫描会话 JSONL)。[W4-D]
3
+ *
4
+ * - 范围:缺省当前目录的会话;`--project <目录>` 指定项目;`--all` 全部(`--by project` 未指定范围时也看全部)。
5
+ * - 时间:`--since` / `--until` 接受 `7d`(含今天的最近 7 天)、`today`、`YYYY-MM-DD`(本地日期,含当天)。
6
+ * - 分组:`--by day|week|month|provider|channel|model|project`。
7
+ * - 缓存:`<数据目录>/stats-index.json`,按文件 mtime / size 失效;`--no-cache` 不读也不写。
8
+ * - 口径见 session/stats-aggregate.ts 与 docs/sessions.md「统计」。
9
+ */
10
+ import { type StatsReport } from "../../session/stats-aggregate.js";
11
+ import type { CliIo } from "../deps.js";
12
+ export declare const STATS_USAGE = "\u7528\u6CD5\uFF1Aama stats [--since 7d|30d|today|YYYY-MM-DD] [--until \u2026]\n [--by day|week|month|provider|channel|model|project]\n [--project <\u76EE\u5F55> | --all] [--top N] [--json] [--no-cache] [--session-dir <\u76EE\u5F55>]\n";
13
+ /** `7d` / `today` / `YYYY-MM-DD` → 本地日期。 */
14
+ export declare function parseDaySpec(option: string, raw: string, now?: Date): string;
15
+ export declare function formatTokens(count: number): string;
16
+ export declare function renderStats(report: StatsReport, scope: string): string;
17
+ export declare function runStats(argv: readonly string[], io: CliIo): Promise<number>;