@goodandready/dsh-goal 0.1.7 → 0.1.9

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,7 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { GoalState } from './goal-engine.js';
2
+ import { GoalState, detectLanguage } from './goal-engine.js';
3
3
 
4
- export const USAGE = 'Использование: /goal [<цель>|clear|pause|resume]';
4
+ export const USAGE_RU = 'Использование: /goal [<цель>|clear|pause|resume]';
5
+ export const USAGE_EN = 'Usage: /goal [<goal>|clear|pause|resume]';
6
+ export const USAGE = USAGE_RU;
5
7
 
6
8
  /**
7
9
  * Парсинг строки ввода для команды /goal
@@ -39,15 +41,26 @@ export function createGoalUserMessage(text) {
39
41
  /**
40
42
  * Формирование стартового промпта с жестким требованием составить план работ
41
43
  * @param {string} goalText
44
+ * @param {string} [explicitLang]
42
45
  * @returns {string}
43
46
  */
44
- export function formatGoalStartPrompt(goalText) {
45
- return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
46
- `СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
47
- `1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
48
- `2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
49
- `3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
50
- `4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
47
+ export function formatGoalStartPrompt(goalText, explicitLang) {
48
+ const lang = explicitLang || detectLanguage(goalText);
49
+ if (lang === 'ru') {
50
+ return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
51
+ `СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
52
+ `1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
53
+ `2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
54
+ `3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
55
+ `4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
56
+ }
57
+
58
+ return `🎯 Goal Mode activated: "${goalText}"\n\n` +
59
+ `STRICT AUTONOMOUS CONTRACT:\n` +
60
+ `1. MANDATORY STEP 1: Your FIRST action BEFORE doing any other work or reply MUST be calling tool goal_set_milestones with an array of 3-7 concrete, sequential milestones. The user sees this plan in real time.\n` +
61
+ `2. STEP 2: After establishing the plan, proceed to execute milestones. Before starting each milestone, mark it as in_progress via goal_update_progress(milestone_id, "in_progress").\n` +
62
+ `3. STEP 3: Upon completing a milestone, mark it as completed via goal_update_progress(milestone_id, "completed", "brief milestone outcome").\n` +
63
+ `4. STEP 4: When all milestones are completed, call tool goal_finish(summary) with a comprehensive summary of achieved results.`;
51
64
  }
52
65
 
53
66
  /**
@@ -63,27 +76,49 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
63
76
  const sid = sessionId || 'default';
64
77
  const snap = engine.getSnapshot(sid);
65
78
 
79
+ const lang = (parsed.text ? detectLanguage(parsed.text) : (snap.lang || (snap.title ? detectLanguage(snap.title) : 'en'))) || 'en';
80
+ const isRu = lang === 'ru';
81
+ const usage = isRu ? USAGE_RU : USAGE_EN;
82
+
66
83
  switch (parsed.action) {
67
84
  case 'show': {
68
85
  if (!snap.hasActiveGoal) {
69
86
  return {
70
87
  kind: 'success',
71
- text: `🎯 Режим цели (Goal Mode): цель не установлена.\n${USAGE}`,
88
+ text: isRu
89
+ ? `🎯 Режим цели (Goal Mode): цель не установлена.\n${usage}`
90
+ : `🎯 Goal Mode: no active goal set.\n${usage}`,
72
91
  };
73
92
  }
74
93
 
75
94
  const milestonesInfo = snap.milestones.length > 0
76
- ? `\nПлан работ:\n` + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
95
+ ? (isRu ? `\nПлан работ:\n` : `\nWork plan:\n`) + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
96
+ : '';
97
+
98
+ const etaText = snap.formattedETA ? ` (ETA: ${snap.formattedETA})` : '';
99
+ const tokensInfo = (snap.tokensUsage && snap.tokensUsage.totalTokens > 0)
100
+ ? (isRu
101
+ ? `\nТокены: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`
102
+ : `\nTokens: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`)
77
103
  : '';
78
104
 
79
105
  return {
80
106
  kind: 'success',
81
- text: `🎯 Цель: «${snap.title}»\n` +
82
- `Статус: ${snap.state}\n` +
83
- `Время: ${snap.formattedElapsed}\n` +
84
- `Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
85
- milestonesInfo +
86
- `\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`,
107
+ text: isRu
108
+ ? `🎯 Цель: «${snap.title}»\n` +
109
+ `Статус: ${snap.state}\n` +
110
+ `Время: ${snap.formattedElapsed}${etaText}\n` +
111
+ `Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
112
+ tokensInfo +
113
+ milestonesInfo +
114
+ `\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`
115
+ : `🎯 Goal: "${snap.title}"\n` +
116
+ `Status: ${snap.state}\n` +
117
+ `Duration: ${snap.formattedElapsed}${etaText}\n` +
118
+ `Iterations: ${snap.iterationsCount}/${snap.maxIterations}` +
119
+ tokensInfo +
120
+ milestonesInfo +
121
+ `\n\nCommands: /goal pause, /goal resume, /goal clear, /goal <new goal>`,
87
122
  };
88
123
  }
89
124
 
@@ -91,7 +126,7 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
91
126
  if (!snap.hasActiveGoal) {
92
127
  return {
93
128
  kind: 'success',
94
- text: 'Цель не была установлена.',
129
+ text: isRu ? 'Цель не была установлена.' : 'No active goal set.',
95
130
  };
96
131
  }
97
132
  engine.clear(sid);
@@ -104,7 +139,7 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
104
139
  }
105
140
  return {
106
141
  kind: 'success',
107
- text: '🎯 Цель сброшена.',
142
+ text: isRu ? '🎯 Цель сброшена.' : '🎯 Goal cleared.',
108
143
  };
109
144
  }
110
145
 
@@ -112,16 +147,18 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
112
147
  if (!snap.hasActiveGoal) {
113
148
  return {
114
149
  kind: 'error',
115
- text: `Нельзя приостановить: активная цель отсутствует.\n${USAGE}`,
150
+ text: isRu
151
+ ? `Нельзя приостановить: активная цель отсутствует.\n${usage}`
152
+ : `Cannot pause: no active goal.\n${usage}`,
116
153
  };
117
154
  }
118
155
  if (snap.state === GoalState.PAUSED) {
119
156
  return {
120
157
  kind: 'success',
121
- text: '⏸ Цель уже на паузе.',
158
+ text: isRu ? '⏸ Цель уже на паузе.' : '⏸ Goal is already paused.',
122
159
  };
123
160
  }
124
- engine.pause('Пауза по команде пользователя (/goal pause)', sid);
161
+ engine.pause(isRu ? 'Пауза по команде пользователя (/goal pause)' : 'Paused by user command (/goal pause)', sid);
125
162
  if (agent && typeof agent.cancel === 'function') {
126
163
  try {
127
164
  agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
@@ -131,7 +168,9 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
131
168
  }
132
169
  return {
133
170
  kind: 'success',
134
- text: `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`,
171
+ text: isRu
172
+ ? `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`
173
+ : `⏸ Goal paused: "${snap.title}". Agent stopped. Use /goal resume or ▶️ button to continue.`,
135
174
  };
136
175
  }
137
176
 
@@ -139,19 +178,23 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
139
178
  if (!snap.hasActiveGoal) {
140
179
  return {
141
180
  kind: 'error',
142
- text: `Нельзя возобновить: активная цель отсутствует.\n${USAGE}`,
181
+ text: isRu
182
+ ? `Нельзя возобновить: активная цель отсутствует.\n${usage}`
183
+ : `Cannot resume: no active goal.\n${usage}`,
143
184
  };
144
185
  }
145
186
  if (snap.state === GoalState.RUNNING) {
146
187
  return {
147
188
  kind: 'success',
148
- text: '▶️ Цель уже выполняется.',
189
+ text: isRu ? '▶️ Цель уже выполняется.' : '▶️ Goal is already running.',
149
190
  };
150
191
  }
151
192
  engine.resume(sid);
152
193
  if (agent && typeof agent.followup === 'function') {
153
194
  try {
154
- const resumePrompt = `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`;
195
+ const resumePrompt = isRu
196
+ ? `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`
197
+ : `▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.`;
155
198
  agent.followup(createGoalUserMessage(resumePrompt));
156
199
  } catch (err) {
157
200
  console.warn('[dsh-goal] Failed to followup agent on resume:', err);
@@ -159,18 +202,20 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
159
202
  }
160
203
  return {
161
204
  kind: 'success',
162
- text: `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`,
205
+ text: isRu
206
+ ? `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`
207
+ : `▶️ Goal resumed: "${snap.title}". Agent continues working.`,
163
208
  };
164
209
  }
165
210
 
166
211
  case 'start': {
167
212
  const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
168
- const newSnap = engine.startGoal(parsed.text, { maxIterations }, sid);
213
+ const newSnap = engine.startGoal(parsed.text, { maxIterations, lang }, sid);
169
214
 
170
215
  // Если команда вызвана в контексте агента, отправляем цель в очередь модели
171
216
  if (agent && typeof agent.followup === 'function') {
172
217
  try {
173
- const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text));
218
+ const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text, lang));
174
219
  agent.followup(userMsg);
175
220
  } catch (err) {
176
221
  console.warn('[dsh-goal] Failed to dispatch goal to agent followup:', err);
@@ -179,15 +224,18 @@ export function executeGoalSlashCommand(engine, parsed, config = {}, agent = nul
179
224
 
180
225
  return {
181
226
  kind: 'success',
182
- text: `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
183
- `Для паузы: /goal pause | Для сброса: /goal clear`,
227
+ text: isRu
228
+ ? `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
229
+ `Для паузы: /goal pause | Для сброса: /goal clear`
230
+ : `🎯 Goal activated: "${newSnap.title}" (max ${newSnap.maxIterations} iterations).\n` +
231
+ `To pause: /goal pause | To clear: /goal clear`,
184
232
  };
185
233
  }
186
234
 
187
235
  default:
188
236
  return {
189
237
  kind: 'error',
190
- text: `Неизвестная команда. ${USAGE}`,
238
+ text: isRu ? `Неизвестная команда. ${usage}` : `Unknown command. ${usage}`,
191
239
  };
192
240
  }
193
241
  }
@@ -42,11 +42,45 @@ export function formatElapsed(totalSeconds) {
42
42
  return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
43
43
  }
44
44
 
45
+ /**
46
+ * Форматирование расчетного оставшегося времени (ETA)
47
+ * @param {number|null} seconds
48
+ * @returns {string|null}
49
+ */
50
+ export function formatETA(seconds) {
51
+ if (seconds == null || isNaN(seconds)) return null;
52
+ const sec = Math.max(0, Math.floor(seconds));
53
+ if (sec < 60) return `~${sec}s`;
54
+ const mins = Math.round(sec / 60);
55
+ if (mins < 60) return `~${mins}m`;
56
+ const hours = Math.floor(mins / 60);
57
+ const remainingMins = mins % 60;
58
+ return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
59
+ }
60
+
61
+ /**
62
+ * Автоматическое определение языка текста (кириллица -> ru, иероглифы -> zh, иначе en)
63
+ * @param {string} text
64
+ * @param {string} [fallback='en']
65
+ * @returns {'ru' | 'en' | 'zh'}
66
+ */
67
+ export function detectLanguage(text, fallback = 'en') {
68
+ if (!text || typeof text !== 'string') return fallback;
69
+ if (/[а-яёА-ЯЁ]/i.test(text)) {
70
+ return 'ru';
71
+ }
72
+ if (/[\u4e00-\u9fa5]/.test(text)) {
73
+ return 'zh';
74
+ }
75
+ return 'en';
76
+ }
77
+
45
78
  export class GoalEngine {
46
79
  constructor(options = {}) {
47
80
  this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
48
81
  this.autoDrive = options.autoDrive ?? true;
49
82
  this.enableSound = options.enableSound ?? true;
83
+ this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
50
84
  this.maxSessions = options.maxSessions ?? 100;
51
85
  this.goals = new Map();
52
86
  this.listeners = new Set();
@@ -96,6 +130,12 @@ export class GoalEngine {
96
130
  if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
97
131
  dirty = true;
98
132
  }
133
+ if (!goal.tokensUsage) {
134
+ goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
135
+ }
136
+ if (!goal.lang) {
137
+ goal.lang = detectLanguage(goal.title);
138
+ }
99
139
  this.goals.set(sid, goal);
100
140
  }
101
141
  }
@@ -112,6 +152,12 @@ export class GoalEngine {
112
152
  if (data.logs.length > 100) data.logs = data.logs.slice(-100);
113
153
  dirty = true;
114
154
  }
155
+ if (!data.tokensUsage) {
156
+ data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
157
+ }
158
+ if (!data.lang) {
159
+ data.lang = detectLanguage(data.title);
160
+ }
115
161
  this.goals.set('default', data);
116
162
  }
117
163
  if (dirty) {
@@ -251,6 +297,9 @@ export class GoalEngine {
251
297
  if (typeof config.enableSound === 'boolean') {
252
298
  this.enableSound = config.enableSound;
253
299
  }
300
+ if (typeof config.showQuickLaunchButton === 'boolean') {
301
+ this.showQuickLaunchButton = config.showQuickLaunchButton;
302
+ }
254
303
  this.emit();
255
304
  }
256
305
 
@@ -304,6 +353,7 @@ export class GoalEngine {
304
353
  sessionId: sid,
305
354
  title: cleanTitle,
306
355
  description: options.description?.trim() || '',
356
+ lang: options.lang || detectLanguage(cleanTitle),
307
357
  state: GoalState.RUNNING,
308
358
  startedAt: now,
309
359
  pausedAt: null,
@@ -312,6 +362,7 @@ export class GoalEngine {
312
362
  iterationsCount: 0,
313
363
  maxIterations: options.maxIterations ?? this.defaultMaxIterations,
314
364
  milestones: [],
365
+ tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
315
366
  logs: [
316
367
  {
317
368
  timestamp: now,
@@ -561,6 +612,57 @@ export class GoalEngine {
561
612
  return Math.floor(elapsedMs / 1000);
562
613
  }
563
614
 
615
+ /**
616
+ * Накопление статистики использования токенов сессии
617
+ * @param {Object} usage
618
+ * @param {string} [sessionId='default']
619
+ */
620
+ addTokenUsage(usage, sessionId = 'default') {
621
+ if (!usage || typeof usage !== 'object') return;
622
+ const sid = sessionId || 'default';
623
+ const goal = this.goals.get(sid);
624
+ if (!goal) return;
625
+
626
+ if (!goal.tokensUsage) {
627
+ goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
628
+ }
629
+
630
+ const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
631
+ const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
632
+ const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
633
+
634
+ goal.tokensUsage.promptTokens += prompt;
635
+ goal.tokensUsage.completionTokens += completion;
636
+ goal.tokensUsage.totalTokens += total;
637
+
638
+ this.emit(sid);
639
+ }
640
+
641
+ /**
642
+ * Интеллектуальный расчет прогноза оставшегося времени (ETA)
643
+ * на основе средней скорости выполнения завершенных вех
644
+ * @param {string} [sessionId='default']
645
+ * @returns {number|null}
646
+ */
647
+ getEstimatedRemainingSeconds(sessionId = 'default') {
648
+ const sid = sessionId || 'default';
649
+ const goal = this.goals.get(sid);
650
+ if (!goal || goal.state !== GoalState.RUNNING) return null;
651
+
652
+ const total = goal.milestones.length;
653
+ if (total === 0) return null;
654
+
655
+ const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
656
+ if (completedCount === 0 || completedCount >= total) return null;
657
+
658
+ const elapsed = this.getElapsedSeconds(sid);
659
+ if (elapsed <= 0) return null;
660
+
661
+ const avgSecPerMilestone = elapsed / completedCount;
662
+ const remainingCount = total - completedCount;
663
+ return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
664
+ }
665
+
564
666
  /**
565
667
  * Снимок состояния для передачи клиенту / API
566
668
  */
@@ -579,12 +681,17 @@ export class GoalEngine {
579
681
  completedAt: null,
580
682
  elapsedSeconds: 0,
581
683
  formattedElapsed: '0s',
684
+ estimatedRemainingSeconds: null,
685
+ formattedETA: null,
686
+ lang: 'en',
687
+ tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
582
688
  milestones: [],
583
689
  progressPercent: 0,
584
690
  iterationsCount: 0,
585
691
  maxIterations: this.defaultMaxIterations,
586
692
  autoDrive: this.autoDrive,
587
693
  enableSound: this.enableSound,
694
+ showQuickLaunchButton: this.showQuickLaunchButton,
588
695
  };
589
696
  }
590
697
 
@@ -592,6 +699,7 @@ export class GoalEngine {
592
699
  const milestones = goal.milestones;
593
700
  const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
594
701
  const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
702
+ const estSec = this.getEstimatedRemainingSeconds(sid);
595
703
 
596
704
  return {
597
705
  sessionId: sid,
@@ -600,12 +708,16 @@ export class GoalEngine {
600
708
  state: goal.state,
601
709
  title: goal.title,
602
710
  description: goal.description,
711
+ lang: goal.lang || detectLanguage(goal.title),
603
712
  startedAt: goal.startedAt,
604
713
  pausedAt: goal.pausedAt,
605
714
  totalPausedDurationMs: goal.totalPausedDurationMs,
606
715
  completedAt: goal.completedAt,
607
716
  elapsedSeconds: elapsed,
608
717
  formattedElapsed: formatElapsed(elapsed),
718
+ estimatedRemainingSeconds: estSec,
719
+ formattedETA: formatETA(estSec),
720
+ tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
609
721
  iterationsCount: goal.iterationsCount,
610
722
  maxIterations: goal.maxIterations,
611
723
  milestones,
@@ -614,6 +726,7 @@ export class GoalEngine {
614
726
  resultSummary: goal.resultSummary,
615
727
  autoDrive: this.autoDrive,
616
728
  enableSound: this.enableSound,
729
+ showQuickLaunchButton: this.showQuickLaunchButton,
617
730
  };
618
731
  }
619
732
 
@@ -628,14 +741,18 @@ export class GoalEngine {
628
741
  }
629
742
 
630
743
  const snapshot = this.getSnapshot(sid);
744
+ const lang = goal.lang || detectLanguage(snapshot.title);
631
745
  const hasMilestones = snapshot.milestones.length > 0;
632
- const milestonesText = hasMilestones
633
- ? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
634
- : ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
746
+ const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
635
747
 
636
- return `\n\n[DSH GOAL MODE ACTIVE]
748
+ if (lang === 'ru') {
749
+ const milestonesText = hasMilestones
750
+ ? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
751
+ : ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
752
+
753
+ return `\n\n[DSH GOAL MODE ACTIVE]
637
754
  Цель: "${snapshot.title}"
638
- Время работы: ${snapshot.formattedElapsed} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
755
+ Время работы: ${snapshot.formattedElapsed}${etaText} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
639
756
  План работ:
640
757
  ${milestonesText}
641
758
 
@@ -643,5 +760,21 @@ ${milestonesText}
643
760
  1. ${hasMilestones ? 'Выполняй текущий активный пункт плана работ.' : 'ТВОЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones со списком пунктов плана работ (3-7 конкретных шагов). Запрещено выполнять работу или завершать turn без вызова goal_set_milestones!'}
644
761
  2. По мере выполнения каждого шага обязательно отмечай его статус через инструмент goal_update_progress (status: "in_progress" перед началом шага, status: "completed" по его завершении с кратким notes).
645
762
  3. Когда все пункты плана будут выполнены, вызови инструмент goal_finish с подробным итоговым резюме достигнутых результатов.`;
763
+ }
764
+
765
+ const milestonesText = hasMilestones
766
+ ? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
767
+ : ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
768
+
769
+ return `\n\n[DSH GOAL MODE ACTIVE]
770
+ Goal: "${snapshot.title}"
771
+ Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}
772
+ Work Plan:
773
+ ${milestonesText}
774
+
775
+ Goal Mode Instructions (MANDATORY TO FOLLOW):
776
+ 1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}
777
+ 2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes).
778
+ 3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`;
646
779
  }
647
780
  }
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import z from '@deepseek-ai/schemastery';
4
- import { GoalEngine, GoalState, MilestoneStatus } from './goal-engine.js';
4
+ import { GoalEngine, GoalState, MilestoneStatus, detectLanguage } from './goal-engine.js';
5
5
  import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
6
6
 
7
7
  export const name = '@goodandready/dsh-goal';
@@ -42,6 +42,7 @@ export const Config = z.object({
42
42
  maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
43
43
  autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
44
44
  enableSound: z.boolean().default(true).description('Play synthesized audio chime on goal completion or failure'),
45
+ showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
45
46
  storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
46
47
  });
47
48
 
@@ -59,6 +60,7 @@ export function apply(ctx, config = {}) {
59
60
  maxIterations: config?.maxIterations ?? 25,
60
61
  autoDrive: config?.autoDrive ?? true,
61
62
  enableSound: config?.enableSound ?? true,
63
+ showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
62
64
  storagePath: config?.storagePath,
63
65
  };
64
66
 
@@ -71,6 +73,7 @@ export function apply(ctx, config = {}) {
71
73
  defaultMaxIterations: currentSettings.maxIterations,
72
74
  autoDrive: currentSettings.autoDrive,
73
75
  enableSound: currentSettings.enableSound,
76
+ showQuickLaunchButton: currentSettings.showQuickLaunchButton,
74
77
  storagePath,
75
78
  });
76
79
 
@@ -163,9 +166,12 @@ export function apply(ctx, config = {}) {
163
166
 
164
167
  const resumeActiveAgent = (promptText, sessionId = 'default') => {
165
168
  const sid = sessionId || 'default';
166
- const resumeMsg = createGoalUserMessage(
167
- promptText || '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.',
168
- );
169
+ const snap = engine.getSnapshot(sid);
170
+ const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
171
+ const defaultResumePrompt = lang === 'ru'
172
+ ? '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.'
173
+ : '▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.';
174
+ const resumeMsg = createGoalUserMessage(promptText || defaultResumePrompt);
169
175
  let target = sessionAgents.get(sid);
170
176
  if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
171
177
  const lastSid = sessionIdOf(lastActiveAgent, null);
@@ -215,6 +221,7 @@ export function apply(ctx, config = {}) {
215
221
  maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
216
222
  autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
217
223
  enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
224
+ showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
218
225
  storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
219
226
  };
220
227
  }
@@ -226,6 +233,7 @@ export function apply(ctx, config = {}) {
226
233
  maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
227
234
  autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
228
235
  enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
236
+ showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
229
237
  storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
230
238
  };
231
239
  }
@@ -240,6 +248,7 @@ export function apply(ctx, config = {}) {
240
248
  defaultMaxIterations: live.maxIterations,
241
249
  autoDrive: live.autoDrive,
242
250
  enableSound: live.enableSound,
251
+ showQuickLaunchButton: live.showQuickLaunchButton,
243
252
  });
244
253
  };
245
254
 
@@ -532,10 +541,16 @@ export function apply(ctx, config = {}) {
532
541
  res.statusCode = 400;
533
542
  return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
534
543
  }
544
+ const detectedLang = data.lang || detectLanguage(cleanTitle);
535
545
  result = engine.startGoal(cleanTitle, {
536
546
  description: typeof description === 'string' ? description.trim() : '',
537
547
  maxIterations: getConfig().maxIterations,
548
+ lang: detectedLang,
538
549
  }, sid);
550
+ const startPrompt = detectedLang === 'ru'
551
+ ? `🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`
552
+ : `🎯 Goal established: "${cleanTitle}". Immediately formulate a work plan (3-7 concrete steps) via tool goal_set_milestones and start executing it.`;
553
+ resumeActiveAgent(startPrompt, sid);
539
554
  break;
540
555
  }
541
556
  case 'pause':
@@ -615,6 +630,13 @@ export function apply(ctx, config = {}) {
615
630
  if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
616
631
 
617
632
  const sid = sessionIdOf(turn, 'default');
633
+
634
+ // Накопление токенов по завершении хода
635
+ const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
636
+ if (usage) {
637
+ engine.addTokenUsage(usage, sid);
638
+ }
639
+
618
640
  const snap = engine.getSnapshot(sid);
619
641
  if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
620
642
  // Проверяем причину завершения хода
@@ -623,9 +645,13 @@ export function apply(ctx, config = {}) {
623
645
  }
624
646
 
625
647
  // Item 3: Smart Progress Guard — detect idle loops without progress
648
+ const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
626
649
  const stallCount = engine.incrementStallCount(sid);
627
650
  if (stallCount >= 2) {
628
- engine.pause('Агент не продвинулся по плану за последние 2 итерации — требуется внимание оператора', sid);
651
+ const stallReason = lang === 'ru'
652
+ ? 'Агент не продвинулся по плану за последние 2 итерации — требуется внимание оператора'
653
+ : 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
654
+ engine.pause(stallReason, sid);
629
655
  return;
630
656
  }
631
657
 
@@ -636,9 +662,11 @@ export function apply(ctx, config = {}) {
636
662
  const triggerNextTurn = () => {
637
663
  const currentSnap = engine.getSnapshot(sid);
638
664
  if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
639
- const promptMsg = createGoalUserMessage(
640
- 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.',
641
- );
665
+ const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
666
+ const promptText = currentLang === 'ru'
667
+ ? 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.'
668
+ : 'Continue autonomous goal execution according to the work plan. Mark each completed step via goal_update_progress, and when all tasks are complete, call goal_finish.';
669
+ const promptMsg = createGoalUserMessage(promptText);
642
670
  let target = sessionAgents.get(sid);
643
671
  if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
644
672
  const lastSid = sessionIdOf(lastActiveAgent, null);
@@ -684,7 +712,11 @@ export function apply(ctx, config = {}) {
684
712
  const sid = sessionIdOf(event, 'default');
685
713
  const snap = engine.getSnapshot(sid);
686
714
  if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
687
- engine.pause('Ожидание подтверждения действия оператором', sid);
715
+ const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
716
+ const pauseReason = lang === 'ru'
717
+ ? 'Ожидание подтверждения действия оператором'
718
+ : 'Waiting for operator confirmation/approval';
719
+ engine.pause(pauseReason, sid);
688
720
  }
689
721
  };
690
722
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.1.7",
4
- "description": "Goal mode & autonomous execution plugin for DeepSeek Harness with sticky top banner",
3
+ "version": "0.1.9",
4
+ "description": "Autonomous Goal Execution & Multi-Turn Task Tracking Engine with Sticky Header for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {
@@ -15,11 +15,9 @@
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
17
  "docs/",
18
- "LICENSE"
18
+ "LICENSE",
19
+ "README.zh.md"
19
20
  ],
20
- "scripts": {
21
- "test": "node --test test/*.test.mjs"
22
- },
23
21
  "keywords": [
24
22
  "dsh",
25
23
  "dsh-plugin",
@@ -57,5 +55,8 @@
57
55
  },
58
56
  "dependencies": {
59
57
  "@deepseek-ai/schemastery": "^3.18.1"
58
+ },
59
+ "scripts": {
60
+ "test": "node --test test/*.test.mjs"
60
61
  }
61
- }
62
+ }