mocode-ai 1.0.2 → 1.0.3

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.
@@ -50,10 +50,12 @@ function writeChangeOverview() {
50
50
  const changes = [...merged.values()];
51
51
  const added = changes.reduce((n, c) => n + c.added, 0);
52
52
  const removed = changes.reduce((n, c) => n + c.removed, 0);
53
- layout.contentWrite(` ${ui.dim}├─${ui.reset} ${ui.bold}${ui.green}◆${ui.reset} ${t('agent.changes')} ${t('agent.files', { count: changes.length })} ${ui.green}+${added}${ui.reset} ${ui.red}−${removed}${ui.reset}\n`);
53
+ layout.contentWrite(` ${ui.bold}${ui.green}◆${ui.reset} ${t('agent.changes')} ${t('agent.files', { count: changes.length })} ${ui.green}+${added}${ui.reset} ${ui.red}−${removed}${ui.reset}\n`);
54
54
  for (const change of changes) {
55
- layout.contentWrite(` ${ui.dim}│ ${change.kind}${ui.reset} ${change.path} ${ui.green}+${change.added}${ui.reset} ${ui.red}−${change.removed}${ui.reset}\n`);
55
+ const kindLabel = change.kind === 'A' ? t('agent.changeAdded') : t('agent.changeModified');
56
+ layout.contentWrite(` ${kindLabel} ${change.path} ${ui.green}+${change.added}${ui.reset} ${ui.red}−${change.removed}${ui.reset}\n`);
56
57
  }
58
+ layout.contentWrite('\n');
57
59
  }
58
60
  /** 取 userInput 的首行:字符串直接 split;多模态 parts 找首个 text part 再 split。 */
59
61
  function firstLineOf(ui) {
@@ -253,7 +255,7 @@ onContextUpdate) {
253
255
  ? `${validation.status}: ${validation.skipReason}`
254
256
  : validation.status;
255
257
  const symbol = validation.status === 'passed' ? '◆' : validation.status === 'failed' ? '×' : '!';
256
- layout.contentWrite(` ${ui.dim}├─${ui.reset} ${color}${symbol}${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n\n`);
258
+ layout.contentWrite(` ${color}${symbol}${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n\n`);
257
259
  },
258
260
  onDone: (elapsedMs, usage) => {
259
261
  flushToolBatch();
@@ -174,6 +174,8 @@ const zhCN = {
174
174
  'agent.toolsFailed': '探索失败',
175
175
  'agent.changes': '文件变更',
176
176
  'agent.files': '{count} 个文件',
177
+ 'agent.changeAdded': ' 新增',
178
+ 'agent.changeModified': ' 修改',
177
179
  'agent.complete': '完成',
178
180
  'toolSummary.lines': '{count} 行',
179
181
  'toolSummary.files': '{count} 个文件',
@@ -406,6 +408,8 @@ const en = {
406
408
  'agent.toolsFailed': 'Exploration failed',
407
409
  'agent.changes': 'Changes',
408
410
  'agent.files': '{count} file(s)',
411
+ 'agent.changeAdded': ' Added',
412
+ 'agent.changeModified': ' Modified',
409
413
  'agent.complete': 'Complete',
410
414
  'toolSummary.lines': '{count} lines',
411
415
  'toolSummary.files': '{count} files',
@@ -6,7 +6,7 @@ function optionToChoice(o) {
6
6
  if (o === null || o === undefined)
7
7
  return { label: '' };
8
8
  if (typeof o === 'string')
9
- return { label: o };
9
+ return { label: o.trim() };
10
10
  if (typeof o === 'number' || typeof o === 'boolean')
11
11
  return { label: String(o) };
12
12
  if (typeof o === 'object') {
@@ -89,14 +89,31 @@ export function coerceOptions(raw) {
89
89
  }
90
90
  return [];
91
91
  }
92
+ /**
93
+ * 在通用 JSON Schema 校验前统一模型的常见错参。
94
+ * - 兼容历史字符串选项和 description/detail 等别名;
95
+ * - 丢弃 `{}` 等无可读内容的项;
96
+ * - 少于两个有效选项时统一为 `options: []`,明确表示自由文本面板。
97
+ */
98
+ export function normalizeAskHumanArguments(args) {
99
+ const options = coerceOptions(args.options);
100
+ if (options.length < 2) {
101
+ args.options = [];
102
+ return;
103
+ }
104
+ args.options = options.slice(0, 4).map((option) => ({
105
+ label: option.label,
106
+ ...(option.detail ? { description: option.detail } : {}),
107
+ }));
108
+ }
92
109
  // ---------- ask_human ----------
93
110
  export const askHumanTool = {
94
111
  name: 'ask_human',
95
112
  description: [
96
- 'Present the user with a menu of choices to pick from.',
97
- ' DEFAULT: pass 2-4 concrete options via `options`; each is a string, or { label, description } when the choice has a non-obvious tradeoff.',
98
- ' STRICT: object form requires a non-empty `label` never emit {} or omit label (renders as a literal "{}" menu item). Only `label` and `description` are recognized — no extra fields.',
99
- ' FREE-TEXT (omit `options`): only when the answer truly cannot be reduced to a few choices (e.g. "paste the error message").',
113
+ 'Ask the user a question and wait for their response.',
114
+ ' CHOICES: pass 2-4 concrete options via `options`, each as { label, description? }.',
115
+ ' FREE-TEXT: pass `options: []` when the answer cannot be reduced to choices (e.g. "paste the error message").',
116
+ ' Every non-empty option requires a non-empty `label`; never emit [{}], omit label, or omit `options`.',
100
117
  ' DO NOT call when the task is clear and you can pick a sensible default.',
101
118
  ].join(' '),
102
119
  parameters: {
@@ -104,39 +121,39 @@ export const askHumanTool = {
104
121
  properties: {
105
122
  question: {
106
123
  type: 'string',
124
+ minLength: 1,
107
125
  description: 'The question to ask the user; keep it concise (shown as the panel title)',
108
126
  },
109
127
  options: {
110
128
  type: 'array',
129
+ minItems: 0,
130
+ maxItems: 4,
111
131
  items: {
112
- anyOf: [
113
- { type: 'string' },
114
- {
115
- type: 'object',
116
- properties: {
117
- label: {
118
- type: 'string',
119
- minLength: 1,
120
- description: 'Short option title (1-5 words), shown as the choice itself.',
121
- },
122
- description: {
123
- type: 'string',
124
- description: 'What picking this option means or implies; explain the tradeoff when the label alone leaves the user unsure.',
125
- },
126
- },
127
- required: ['label'],
132
+ type: 'object',
133
+ properties: {
134
+ label: {
135
+ type: 'string',
136
+ minLength: 1,
137
+ description: 'Short option title (1-5 words), shown as the choice itself.',
138
+ },
139
+ description: {
140
+ type: 'string',
141
+ description: 'What picking this option means or implies; explain the tradeoff when the label alone leaves the user unsure.',
128
142
  },
129
- ],
143
+ },
144
+ required: ['label'],
145
+ additionalProperties: false,
130
146
  },
131
- description: '2-4 concrete choices. Each is a plain string, or { label, description } when the tradeoff is non-obvious. Required in most cases; omit only for free-form input.',
147
+ description: 'Pass [] for free-text input, or 2-4 concrete choices with non-empty labels.',
132
148
  },
133
149
  context: {
134
150
  type: 'string',
135
151
  description: 'Background explanation shown under the title; may be multiline.',
136
152
  },
137
153
  },
138
- required: ['question'],
154
+ required: ['question', 'options'],
139
155
  },
156
+ normalizeArguments: normalizeAskHumanArguments,
140
157
  async execute(args) {
141
158
  const question = String(args.question ?? '');
142
159
  const options = coerceOptions(args.options);
@@ -53,7 +53,7 @@ function formatErrors(errors) {
53
53
  return `${location} ${error.message ?? error.keyword}`;
54
54
  }).join('; ');
55
55
  }
56
- /** Validate without coercing, defaulting, removing, or otherwise mutating model arguments. */
56
+ /** Validate after tool-local normalization; AJV itself never coerces or mutates arguments. */
57
57
  export function validateToolArguments(tool, args) {
58
58
  if (!args || typeof args !== 'object' || Array.isArray(args)) {
59
59
  return {
@@ -62,6 +62,16 @@ export function validateToolArguments(tool, args) {
62
62
  message: '参数根节点必须是 JSON object',
63
63
  };
64
64
  }
65
+ try {
66
+ tool.normalizeArguments?.(args);
67
+ }
68
+ catch (error) {
69
+ return {
70
+ valid: false,
71
+ code: 'INVALID_ARGUMENTS',
72
+ message: `参数规范化失败: ${error instanceof Error ? error.message : String(error)}`,
73
+ };
74
+ }
65
75
  const compiled = compile(tool.parameters);
66
76
  if (!compiled.valid) {
67
77
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {