@noobdemon/noob-cli 1.12.14 → 1.12.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/agent.js +24 -2
- package/src/api.js +7 -3
- package/src/diff.js +13 -4
- package/src/i18n.js +2 -1
- package/src/repl/agent-dispatch.js +30 -21
- package/src/repl/complete.js +1 -0
- package/src/repl/permission.js +1 -2
- package/src/repl/state.js +1 -0
- package/src/repl/stream-printer.js +2 -11
- package/src/repl/workflow-commands.js +3 -1
- package/src/repl.js +220 -56
- package/src/tokens.js +13 -6
- package/src/tools.js +47 -8
- package/src/tui.js +173 -22
- package/src/ui.js +8 -5
- package/src/workflow-bg.js +33 -5
- package/src/workflow-runs.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
|
|
4
4
|
|
|
5
|
+
## [1.12.15] - 2026-06-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Todo display nuốt thanh status + đè ô nhập** (`src/tui.js` `topRow()`): todo branch return sớm vô điều kiện — lúc rảnh vẫn vẽ full progress + task + ctx (2-3 dòng) đè vùng trên ô nhập; lúc busy mất animated spinner. Fix: gỡ early-return, dựng `progress` (1 dòng) + `taskLine` sẵn rồi ghép theo trạng thái. Rảnh → 1 dòng progress gọn (📋 done/total bar %), không task/ctx line → không nuốt ô nhập. Busy → spinner + progress chung dòng 1, task line ▸ dòng 2 → giữ cả spinner LẪN tiến độ. Smoke `smoke-tui-render` tách 4a (rảnh: progress, KHÔNG task line) + 4b (busy: task line hiện) — 8/8 pass.
|
|
9
|
+
- **Test stale `tests/models.test.js` + leak isolation `tests/tools.test.js`**: models test hardcode catalog cũ (`gateway-claude-opus-4-7`, provider `google`) → cập nhật theo catalog hiện tại (`gateway-claude-opus-4-8`, `deepseek`). grep test fail do `extraRoots` module-global leak từ `loadWorkspaceRoots()` (nạp `.noob/dirs.json` chứa Temp dir) → thêm export `clearRoots()` gọi trong `beforeEach`. grep 17s → 997ms. 98/98 vitest pass.
|
|
10
|
+
|
|
5
11
|
## [1.12.14] - 2026-06-14
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -170,6 +170,25 @@ function goalBlock(goal) {
|
|
|
170
170
|
].join('\n');
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Mode hint (build/plan/compose) — chỉ NHẮC mềm, KHÔNG hard-gate tool. build =
|
|
174
|
+
// default (full, không thêm khối). plan/compose chèn 1 đoạn hướng dẫn nhẹ vào
|
|
175
|
+
// system prompt để model điều chỉnh phong cách làm việc.
|
|
176
|
+
function modeBlock(uiMode) {
|
|
177
|
+
if (uiMode === 'plan') {
|
|
178
|
+
return [
|
|
179
|
+
'# CHẾ ĐỘ PLAN',
|
|
180
|
+
'Ưu tiên phân tích & đọc code trước. Đề xuất kế hoạch rõ ràng TRƯỚC khi sửa file. Tránh edit_file/write_file/run_command (thao tác thay đổi) trừ khi user đã xác nhận hướng đi. Mục tiêu: hiểu sâu & lên kế hoạch, không vội thực thi.',
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
if (uiMode === 'compose') {
|
|
184
|
+
return [
|
|
185
|
+
'# CHẾ ĐỘ COMPOSE',
|
|
186
|
+
'Làm việc theo workflow có cấu trúc: (1) làm rõ specs/yêu cầu → (2) lập kế hoạch → (3) thực thi từng bước → (4) báo cáo kết quả. Giữ kỷ luật từng giai đoạn, không nhảy cóc.',
|
|
187
|
+
].join('\n');
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
|
|
173
192
|
// Môi trường chạy thực: model cần biết OS + shell để emit lệnh ĐÚNG. Không có
|
|
174
193
|
// khối này, trên Windows model hay emit lệnh Unix (wc/ls/cat/grep) → run_command
|
|
175
194
|
// (PowerShell) báo lỗi.
|
|
@@ -417,7 +436,7 @@ function relTime(ts) {
|
|
|
417
436
|
// chèn ngay sau SYSTEM để model biết và dùng được.
|
|
418
437
|
// recentSessions: breadcrumbs các phiên trước cùng workspace (repl.js cung cấp)
|
|
419
438
|
// → chèn ngay sau memoryBlock() để model "thấy" lịch sử dù chưa /resume.
|
|
420
|
-
export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
|
|
439
|
+
export function buildSystem(history, extraToolsDoc, goal, recentSessions, uiMode) {
|
|
421
440
|
const parts = [SYSTEM, '', memoryBlock()];
|
|
422
441
|
// Auto-active skills (frontmatter `auto: true`) — luôn ON, không cần slash.
|
|
423
442
|
// Đặt sau memoryBlock để model thấy convention skill TRƯỚC khi vào tool/goal.
|
|
@@ -427,6 +446,8 @@ export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
|
|
|
427
446
|
parts.push('', recentSessionsBlock(recentSessions));
|
|
428
447
|
}
|
|
429
448
|
if (goal && goal.trim()) parts.push('', goalBlock(goal));
|
|
449
|
+
const mode = modeBlock(uiMode);
|
|
450
|
+
if (mode) parts.push('', mode);
|
|
430
451
|
if (extraToolsDoc) parts.push('', extraToolsDoc);
|
|
431
452
|
parts.push('', runtimeContext());
|
|
432
453
|
return parts.join('\n');
|
|
@@ -545,6 +566,7 @@ export async function runAgent({
|
|
|
545
566
|
goal,
|
|
546
567
|
recentSessions,
|
|
547
568
|
pendingTasks,
|
|
569
|
+
uiMode,
|
|
548
570
|
}) {
|
|
549
571
|
// [GỠ BUDGET 2026-06-06] Không còn token budget enforcement. Agent/loop/sub-agent
|
|
550
572
|
// chạy không giới hạn token. Dừng theo: GOAL đạt, <<LOOP_DONE>>, <<ULTRA_DONE>>,
|
|
@@ -573,7 +595,7 @@ export async function runAgent({
|
|
|
573
595
|
await maybeSummarize(history, { model, signal });
|
|
574
596
|
} catch {}
|
|
575
597
|
|
|
576
|
-
const system = buildSystem(history, extraToolsDoc, goal, recentSessions);
|
|
598
|
+
const system = buildSystem(history, extraToolsDoc, goal, recentSessions, uiMode);
|
|
577
599
|
const message = buildUserMessage(history);
|
|
578
600
|
tokenMeter?.addInput(countTokens(system) + countTokens(message));
|
|
579
601
|
tokenMeter?.setContext(tokenMeter.total);
|
package/src/api.js
CHANGED
|
@@ -391,8 +391,7 @@ async function streamOnce({
|
|
|
391
391
|
// UND_ERR_*). Tự throw nguyên dạng → streamWithRetry KHÔNG retry (vì check
|
|
392
392
|
// err.name === 'ApiError'). Bọc thành ApiError retryable để được backoff.
|
|
393
393
|
const causeCode = err?.cause?.code || err?.code;
|
|
394
|
-
const isNetworkDrop =
|
|
395
|
-
err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
|
|
394
|
+
const isNetworkDrop = err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
|
|
396
395
|
const isSocketErr =
|
|
397
396
|
typeof causeCode === 'string' &&
|
|
398
397
|
/^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ENOTFOUND|EPIPE|UND_ERR_)/i.test(
|
|
@@ -422,7 +421,12 @@ const USAGE_CACHE_MS = 90_000;
|
|
|
422
421
|
export async function usage({ force = false } = {}) {
|
|
423
422
|
const now = Date.now();
|
|
424
423
|
const key = config.apiKey || '';
|
|
425
|
-
if (
|
|
424
|
+
if (
|
|
425
|
+
!force &&
|
|
426
|
+
_usageCache.data &&
|
|
427
|
+
_usageCache.key === key &&
|
|
428
|
+
now - _usageCache.at < USAGE_CACHE_MS
|
|
429
|
+
) {
|
|
426
430
|
return _usageCache.data;
|
|
427
431
|
}
|
|
428
432
|
const resp = await fetch(config.gatewayUrl + '/api/usage', { headers: authHeaders() });
|
package/src/diff.js
CHANGED
|
@@ -32,7 +32,8 @@ function diffLines(a, b) {
|
|
|
32
32
|
while (i > 0 && j > 0) {
|
|
33
33
|
if (a[i - 1] === b[j - 1]) {
|
|
34
34
|
ops.push({ type: 'eq', text: a[i - 1] });
|
|
35
|
-
i--;
|
|
35
|
+
i--;
|
|
36
|
+
j--;
|
|
36
37
|
} else if (t[i - 1][j] >= t[i][j - 1]) {
|
|
37
38
|
ops.push({ type: 'del', text: a[i - 1] });
|
|
38
39
|
i--;
|
|
@@ -41,8 +42,12 @@ function diffLines(a, b) {
|
|
|
41
42
|
j--;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
while (i > 0) {
|
|
45
|
-
|
|
45
|
+
while (i > 0) {
|
|
46
|
+
ops.push({ type: 'del', text: a[--i] });
|
|
47
|
+
}
|
|
48
|
+
while (j > 0) {
|
|
49
|
+
ops.push({ type: 'add', text: b[--j] });
|
|
50
|
+
}
|
|
46
51
|
return ops.reverse();
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -98,7 +103,11 @@ function groupHunks(ops, context = 3) {
|
|
|
98
103
|
|
|
99
104
|
// Render unified diff với màu. `label` là tiêu đề (vd path file).
|
|
100
105
|
// `maxLines` cap tổng số dòng render (mặc định 60) — diff quá dài hiện cuối `... +N dòng`.
|
|
101
|
-
export function renderUnifiedDiff(
|
|
106
|
+
export function renderUnifiedDiff(
|
|
107
|
+
oldText,
|
|
108
|
+
newText,
|
|
109
|
+
{ label = '', context = 3, maxLines = 60 } = {}
|
|
110
|
+
) {
|
|
102
111
|
const a = oldText.split('\n');
|
|
103
112
|
const b = newText.split('\n');
|
|
104
113
|
const ops = diffLines(a, b);
|
package/src/i18n.js
CHANGED
|
@@ -95,7 +95,8 @@ export const t = {
|
|
|
95
95
|
` noob.md : ${lines} dòng · ${bytes} · ${rules} rules · ${notes} notes · cập nhật ${ago}`,
|
|
96
96
|
memoryStatsArchive: (lines, bytes, ago) =>
|
|
97
97
|
` noob-archive.md : ${lines} dòng · ${bytes} · cập nhật ${ago} (không inject vào prompt)`,
|
|
98
|
-
memoryStatsArchiveMissing:
|
|
98
|
+
memoryStatsArchiveMissing:
|
|
99
|
+
' noob-archive.md : (chưa có — tạo bằng tay nếu cần lưu Notes lịch sử)',
|
|
99
100
|
memoryStatsOk: ' ✓ trong ngưỡng khuyến nghị (≤ 200 dòng / ≤ 20KB)',
|
|
100
101
|
memoryStatsWarnLines: (n) =>
|
|
101
102
|
` ⚠ noob.md ${n} dòng > 200 — cân nhắc archive Notes cũ sang noob-archive.md để giảm token bloat mỗi turn`,
|
|
@@ -33,6 +33,22 @@ import { t } from '../i18n.js';
|
|
|
33
33
|
*/
|
|
34
34
|
export function createAgentDispatcher(deps) {
|
|
35
35
|
const { state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c } = deps;
|
|
36
|
+
// bgAgentLog: true → log sub-agent đi vào ring buffer (tui.pushAgentLog, xem
|
|
37
|
+
// bằng Ctrl+O), KHÔNG in ra UI chính. Dùng cho workflow nền (headless). False/
|
|
38
|
+
// thiếu → behavior cũ: in console.log ra UI chính (sub-agent foreground).
|
|
39
|
+
const bgAgentLog = !!deps.bgAgentLog;
|
|
40
|
+
// Route 1 dòng log sub-agent: nền → ring buffer; foreground → console.log.
|
|
41
|
+
const emitAgentLog = (line) => {
|
|
42
|
+
if (bgAgentLog) {
|
|
43
|
+
try {
|
|
44
|
+
tui?.pushAgentLog?.(line);
|
|
45
|
+
} catch {}
|
|
46
|
+
} else {
|
|
47
|
+
stopSpin?.();
|
|
48
|
+
console.log(chalk.hex('#8b5cf6')(' ' + line));
|
|
49
|
+
startSpin?.(t.thinking);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
36
52
|
// bgRegistry: tuỳ chọn — chỉ wire ở dispatcher của TURN (repl.js), KHÔNG ở bg
|
|
37
53
|
// dispatcher (tránh đệ quy: workflow nền spawn workflow nền). Thiếu → tool báo lỗi.
|
|
38
54
|
const bgRegistry = deps.bgRegistry || null;
|
|
@@ -68,7 +84,9 @@ export function createAgentDispatcher(deps) {
|
|
|
68
84
|
// Flag: lượt này model đã dùng write_todos → repl skip parse markdown để
|
|
69
85
|
// không overwrite structured state bằng parser fragile. Reset đầu mỗi turn.
|
|
70
86
|
state._todosFromTool = true;
|
|
71
|
-
try {
|
|
87
|
+
try {
|
|
88
|
+
tui?.setTodos?.(todos);
|
|
89
|
+
} catch {}
|
|
72
90
|
const done = todos.filter((t) => t.done).length;
|
|
73
91
|
// In compact: lần đầu (prev rỗng) hoặc list thay đổi tập text → in full.
|
|
74
92
|
// Nếu cùng tập text + chỉ khác trạng thái done → in diff (dòng vừa toggle).
|
|
@@ -105,7 +123,8 @@ export function createAgentDispatcher(deps) {
|
|
|
105
123
|
if (!bgRegistry)
|
|
106
124
|
return {
|
|
107
125
|
allow: true,
|
|
108
|
-
result:
|
|
126
|
+
result:
|
|
127
|
+
'ERROR: workflow nền không khả dụng ở ngữ cảnh này (vd đang trong 1 workflow nền khác — không lồng).',
|
|
109
128
|
};
|
|
110
129
|
if (!state.agentMode)
|
|
111
130
|
return {
|
|
@@ -122,13 +141,14 @@ export function createAgentDispatcher(deps) {
|
|
|
122
141
|
result: `ERROR: đã đạt ${bgRegistry.MAX_BG} workflow nền song song — chờ một run xong hoặc /workflow stop <id>.`,
|
|
123
142
|
};
|
|
124
143
|
if (r.error === 'empty_prompt')
|
|
125
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
allow: true,
|
|
146
|
+
result: 'ERROR: workflow cần field "prompt" (yêu cầu cho workflow).',
|
|
147
|
+
};
|
|
126
148
|
return { allow: true, result: 'ERROR khởi tạo workflow nền: ' + r.error };
|
|
127
149
|
}
|
|
128
150
|
stopSpin();
|
|
129
|
-
console.log(
|
|
130
|
-
chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`)
|
|
131
|
-
);
|
|
151
|
+
console.log(chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`));
|
|
132
152
|
startSpin(t.thinking);
|
|
133
153
|
return {
|
|
134
154
|
allow: true,
|
|
@@ -172,8 +192,7 @@ export function createAgentDispatcher(deps) {
|
|
|
172
192
|
if (m) {
|
|
173
193
|
subModel = m.id;
|
|
174
194
|
modelTag = ` [${m.name}]`;
|
|
175
|
-
} else
|
|
176
|
-
modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
195
|
+
} else modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
177
196
|
}
|
|
178
197
|
const taskBody = task?.task || task?.prompt || '';
|
|
179
198
|
const taskCtx = task?.context || '';
|
|
@@ -190,9 +209,7 @@ export function createAgentDispatcher(deps) {
|
|
|
190
209
|
)
|
|
191
210
|
);
|
|
192
211
|
startSpin(t.thinking);
|
|
193
|
-
return Promise.resolve(
|
|
194
|
-
`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
|
|
195
|
-
);
|
|
212
|
+
return Promise.resolve(`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`);
|
|
196
213
|
}
|
|
197
214
|
recordWorkflowTaskStart(runData, {
|
|
198
215
|
hash,
|
|
@@ -209,11 +226,7 @@ export function createAgentDispatcher(deps) {
|
|
|
209
226
|
signal: abort.signal,
|
|
210
227
|
tokenMeter,
|
|
211
228
|
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
212
|
-
onLog: (msg) =>
|
|
213
|
-
stopSpin();
|
|
214
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
215
|
-
startSpin(t.thinking);
|
|
216
|
-
},
|
|
229
|
+
onLog: (msg) => emitAgentLog(msg + modelTag),
|
|
217
230
|
})
|
|
218
231
|
.then((r) => {
|
|
219
232
|
recordWorkflowTaskDone(runData, hash, r);
|
|
@@ -233,11 +246,7 @@ export function createAgentDispatcher(deps) {
|
|
|
233
246
|
signal: abort.signal,
|
|
234
247
|
tokenMeter,
|
|
235
248
|
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
236
|
-
onLog: (msg) =>
|
|
237
|
-
stopSpin();
|
|
238
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
239
|
-
startSpin(t.thinking);
|
|
240
|
-
},
|
|
249
|
+
onLog: (msg) => emitAgentLog(msg + modelTag),
|
|
241
250
|
})
|
|
242
251
|
.then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
|
|
243
252
|
.catch(
|
package/src/repl/complete.js
CHANGED
|
@@ -22,6 +22,7 @@ export const SLASH = [
|
|
|
22
22
|
{ name: '/improve', desc: 'phân tích workspace & gợi ý tính năng cải thiện' },
|
|
23
23
|
{ name: '/ultra', desc: 'tự hành: tự nghĩ & làm nhiệm vụ' },
|
|
24
24
|
{ name: '/agent', desc: 'bật/tắt agent mode (spawn sub-agent)' },
|
|
25
|
+
{ name: '/mode', desc: 'build|plan|compose — đổi chế độ agent (Ctrl+T cycle)' },
|
|
25
26
|
{ name: '/goal', desc: 'đặt HARD GOAL — model phải hướng tới tới khi /goal clear' },
|
|
26
27
|
{ name: '/loop', desc: 'chạy task định kỳ (vd: /loop 5m kiểm tra log) · /loop stop để dừng' },
|
|
27
28
|
{ name: '/tokens', desc: 'xem số token đã dùng phiên này' },
|
package/src/repl/permission.js
CHANGED
|
@@ -102,8 +102,7 @@ export async function askWorkflowAgentMode({ tui, ask, pending, c, t, truncate }
|
|
|
102
102
|
console.log(
|
|
103
103
|
c.tool(
|
|
104
104
|
' ' +
|
|
105
|
-
(t.workflowAgentAskHint ||
|
|
106
|
-
'🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
|
|
105
|
+
(t.workflowAgentAskHint || '🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
|
|
107
106
|
)
|
|
108
107
|
);
|
|
109
108
|
try {
|
package/src/repl/state.js
CHANGED
|
@@ -18,6 +18,7 @@ export function createState(opts = {}, config) {
|
|
|
18
18
|
return {
|
|
19
19
|
model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
|
|
20
20
|
mode: 'chat', // chat | merge | search
|
|
21
|
+
agentUiMode: 'build', // build | plan | compose — UI hint mode (Ctrl+T / /mode). KHÔNG đụng `mode`.
|
|
21
22
|
history: [],
|
|
22
23
|
autoApprove: new Set(), // tool name → 'a' (always, phiên)
|
|
23
24
|
autoApproveTurn: new Set(), // tool name → 't' (this turn, reset sau mỗi runAgent)
|
|
@@ -15,12 +15,7 @@
|
|
|
15
15
|
// đa dòng — flushCompleteLines phải HOLD các dòng cuối là table-row/sep
|
|
16
16
|
// candidate để chờ đủ data trước khi parse.
|
|
17
17
|
import chalk from 'chalk';
|
|
18
|
-
import {
|
|
19
|
-
renderMarkdown,
|
|
20
|
-
renderInline,
|
|
21
|
-
renderHeadingLine,
|
|
22
|
-
renderBulletPrefix,
|
|
23
|
-
} from '../ui.js';
|
|
18
|
+
import { renderMarkdown, renderInline, renderHeadingLine, renderBulletPrefix } from '../ui.js';
|
|
24
19
|
|
|
25
20
|
function renderStreamLine(line, inCodeFence) {
|
|
26
21
|
if (inCodeFence) return line; // code fence body: in raw, không parse markdown.
|
|
@@ -62,11 +57,7 @@ export function renderStreamBlock(lines, fenceState) {
|
|
|
62
57
|
continue;
|
|
63
58
|
}
|
|
64
59
|
// Detect table: dòng hiện tại là row + dòng kế tiếp là separator.
|
|
65
|
-
if (
|
|
66
|
-
looksLikeTableRow(ln) &&
|
|
67
|
-
i + 1 < lines.length &&
|
|
68
|
-
TABLE_SEP_RE.test(lines[i + 1])
|
|
69
|
-
) {
|
|
60
|
+
if (looksLikeTableRow(ln) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])) {
|
|
70
61
|
// Gom hết các dòng row liên tiếp.
|
|
71
62
|
const tableLines = [ln, lines[i + 1]];
|
|
72
63
|
let j = i + 2;
|
|
@@ -55,7 +55,9 @@ export function workflowHelp({ c, t }) {
|
|
|
55
55
|
console.log(' /workflow run <name> [extra] chạy (built-in HOẶC saved, có thể thêm ngữ cảnh)');
|
|
56
56
|
console.log(' /workflow runs list run journal đã chạy (resume-able)');
|
|
57
57
|
console.log(' /workflow log <id> xem chi tiết 1 run (sub-agent task + kết quả)');
|
|
58
|
-
console.log(
|
|
58
|
+
console.log(
|
|
59
|
+
' /workflow resume <id> chạy lại run, skip task đã done → tiết kiệm token'
|
|
60
|
+
);
|
|
59
61
|
console.log(' /workflow active workflow nền đang chạy');
|
|
60
62
|
console.log(' /workflow stop <id> | all huỷ workflow nền (resume được)');
|
|
61
63
|
console.log(' /workflow delete|rm <name> xoá workflow đã lưu');
|