@noobdemon/noob-cli 1.11.0 → 1.12.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.
- package/CHANGELOG.md +41 -0
- package/package.json +1 -1
- package/src/agent.js +7 -4
- package/src/api.js +87 -14
- package/src/diff.js +152 -0
- package/src/repl/commands/prompts.js +45 -0
- package/src/repl/permission.js +116 -0
- package/src/repl/state.js +33 -0
- package/src/repl/utils.js +64 -0
- package/src/repl/workflow-commands.js +3 -0
- package/src/repl.js +372 -202
- package/src/tokens.js +2 -2
- package/src/tools.js +11 -3
- package/src/ui.js +92 -5
- package/src/workflow-runs.js +222 -0
package/src/tokens.js
CHANGED
|
@@ -57,8 +57,8 @@ export function countMessages(messages = []) {
|
|
|
57
57
|
// window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của cl100k/o200k
|
|
58
58
|
// (token dài nhất ~ vài chục byte).
|
|
59
59
|
const TAIL_WINDOW = 256;
|
|
60
|
-
// Context window tối đa của model (
|
|
61
|
-
export const CONTEXT_WINDOW =
|
|
60
|
+
// Context window tối đa của model (2M tokens). Dùng để tính % usage realtime.
|
|
61
|
+
export const CONTEXT_WINDOW = 2_000_000;
|
|
62
62
|
|
|
63
63
|
export class TokenMeter {
|
|
64
64
|
constructor() {
|
package/src/tools.js
CHANGED
|
@@ -217,11 +217,19 @@ function killBgTree(child) {
|
|
|
217
217
|
function cleanupBg() {
|
|
218
218
|
for (const p of bg.values()) killBgTree(p.child);
|
|
219
219
|
}
|
|
220
|
-
|
|
221
|
-
process.on('SIGTERM', () => {
|
|
220
|
+
function onSigterm() {
|
|
222
221
|
cleanupBg();
|
|
223
222
|
process.exit(143);
|
|
224
|
-
}
|
|
223
|
+
}
|
|
224
|
+
// Guard chống đăng ký kép: vitest hot-reload / dynamic import có thể re-evaluate
|
|
225
|
+
// module này → nhiều listener → MaxListenersExceededWarning + leak. Chỉ add khi
|
|
226
|
+
// chưa có handler chính xác này.
|
|
227
|
+
if (!process.listeners('exit').includes(cleanupBg)) {
|
|
228
|
+
process.on('exit', cleanupBg);
|
|
229
|
+
}
|
|
230
|
+
if (!process.listeners('SIGTERM').includes(onSigterm)) {
|
|
231
|
+
process.on('SIGTERM', onSigterm);
|
|
232
|
+
}
|
|
225
233
|
|
|
226
234
|
// Helper: throw nếu signal đã abort. Dùng ở đầu mỗi tool + giữa các vòng walk dài
|
|
227
235
|
// để tool fs (glob/grep) cũng phản ứng với Ctrl+C, không chỉ run_command.
|
package/src/ui.js
CHANGED
|
@@ -58,6 +58,24 @@ export function modelBadge(model) {
|
|
|
58
58
|
// * option `href` không nên bọc ngoặc — wrapper tự thêm `(...)`.
|
|
59
59
|
const BULLET = c.accent('•');
|
|
60
60
|
|
|
61
|
+
// Heading scale theo level — marked-terminal gọi `heading` với (text, level).
|
|
62
|
+
// h1 → brand gradient (chỉ dùng cho firstHeading thực ra, nhưng giữ fallback).
|
|
63
|
+
// h2 → tím đậm (section chính, vd `## Đề xuất ưu tiên hàng đầu`).
|
|
64
|
+
// h3 → cyan đậm (sub-section, vd `### 10. Sub-project ...`).
|
|
65
|
+
// h4+ → xám sáng đậm để vẫn nổi nhưng không cướp spotlight của h2/h3.
|
|
66
|
+
const HEADING_BY_LEVEL = {
|
|
67
|
+
1: (s) => brand(s),
|
|
68
|
+
2: chalk.hex('#a78bfa').bold,
|
|
69
|
+
3: chalk.hex('#06b6d4').bold,
|
|
70
|
+
4: chalk.hex('#9ca3af').bold,
|
|
71
|
+
5: chalk.hex('#9ca3af').bold,
|
|
72
|
+
6: chalk.hex('#9ca3af').bold,
|
|
73
|
+
};
|
|
74
|
+
function headingStyler(text, level) {
|
|
75
|
+
const fn = HEADING_BY_LEVEL[level] || HEADING_BY_LEVEL[2];
|
|
76
|
+
return fn(text);
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
marked.use(
|
|
62
80
|
markedTerminal(
|
|
63
81
|
{
|
|
@@ -66,11 +84,15 @@ marked.use(
|
|
|
66
84
|
tab: 2,
|
|
67
85
|
showSectionPrefix: false,
|
|
68
86
|
firstHeading: (s) => brand(s),
|
|
69
|
-
heading:
|
|
87
|
+
heading: headingStyler,
|
|
70
88
|
blockquote: chalk.hex('#6b7280').italic,
|
|
71
|
-
|
|
89
|
+
// **bold** → vàng đậm để các nhãn `**Vấn đề/cơ hội:**`, `**Đề xuất:**`,
|
|
90
|
+
// `**Lợi ích:**`, `**P0**`, `**#1 ...**` đập vào mắt thay vì lẫn prose.
|
|
91
|
+
strong: chalk.hex('#fbbf24').bold,
|
|
72
92
|
em: chalk.italic,
|
|
73
|
-
|
|
93
|
+
// `inline code` (backtick): xanh lá pastel không bg — tách rõ khỏi prose
|
|
94
|
+
// mà không chói như highlight bg cũ. Dùng cho path:line, tên file, tên hàm.
|
|
95
|
+
codespan: chalk.hex('#34d399'),
|
|
74
96
|
hr: () => rule(),
|
|
75
97
|
link: chalk.hex('#06b6d4').underline,
|
|
76
98
|
href: chalk.hex('#9ca3af'),
|
|
@@ -80,17 +102,82 @@ marked.use(
|
|
|
80
102
|
)
|
|
81
103
|
);
|
|
82
104
|
|
|
83
|
-
// Post-process:
|
|
105
|
+
// Post-process:
|
|
106
|
+
// 1. Bullet `*` → `•` màu accent.
|
|
107
|
+
// 2. Block code (4-space indent) → viền trái `│`.
|
|
108
|
+
// 3. Inline `**bold**` và backtick `code` BÊN TRONG list item: marked-terminal
|
|
109
|
+
// v7 + marked v15 không chạy inline parser cho list children → ta tự parse
|
|
110
|
+
// sau cùng. Chạy SAU bước 1 để bullet đã đổi rồi.
|
|
111
|
+
const INLINE_BOLD = chalk.hex('#fbbf24').bold;
|
|
112
|
+
const INLINE_CODE = chalk.hex('#34d399');
|
|
113
|
+
export function renderInline(text) {
|
|
114
|
+
// `code` trước (để `**foo**` trong code không bị parse). Backtick không được
|
|
115
|
+
// chứa newline; cho phép escape \` nếu cần.
|
|
116
|
+
let out = text.replace(/`([^`\n]+)`/g, (_, code) => INLINE_CODE(code));
|
|
117
|
+
// **bold** — non-greedy, không chứa newline. Cho phép nội dung là `code` đã ANSI.
|
|
118
|
+
out = out.replace(/\*\*([^*\n]+?)\*\*/g, (_, b) => INLINE_BOLD(b));
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Style heading prefix theo level cho stream printer (không qua marked).
|
|
123
|
+
// `## foo` → ANSI styled foo, `### bar` → khác cấp. Trả về null nếu không phải heading.
|
|
124
|
+
const HEADING_STYLE_BY_LEVEL = {
|
|
125
|
+
1: brand,
|
|
126
|
+
2: chalk.hex('#a78bfa').bold,
|
|
127
|
+
3: chalk.hex('#06b6d4').bold,
|
|
128
|
+
4: chalk.hex('#9ca3af').bold,
|
|
129
|
+
5: chalk.hex('#9ca3af').bold,
|
|
130
|
+
6: chalk.hex('#9ca3af').bold,
|
|
131
|
+
};
|
|
132
|
+
export function renderHeadingLine(line) {
|
|
133
|
+
const m = line.match(/^(#{1,6})\s+(.+)$/);
|
|
134
|
+
if (!m) return null;
|
|
135
|
+
const level = m[1].length;
|
|
136
|
+
const text = m[2];
|
|
137
|
+
const fn = HEADING_STYLE_BY_LEVEL[level] || HEADING_STYLE_BY_LEVEL[2];
|
|
138
|
+
return fn(renderInline(text));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Đổi bullet đầu dòng `* `, `- `, `+ ` → `• ` màu accent. Numbered list giữ nguyên.
|
|
142
|
+
export function renderBulletPrefix(line) {
|
|
143
|
+
return line.replace(/^( *)[*\-+] /, (_, sp) => sp + BULLET + ' ');
|
|
144
|
+
}
|
|
84
145
|
function prettify(s) {
|
|
85
146
|
return s
|
|
86
147
|
.replace(/^( *)\* /gm, (_, sp) => sp + BULLET + ' ')
|
|
87
|
-
.replace(/^ {4}(.*)$/gm, (_, rest) => c.dim('│ ') + rest)
|
|
148
|
+
.replace(/^ {4}(.*)$/gm, (_, rest) => c.dim('│ ') + rest)
|
|
149
|
+
.split('\n')
|
|
150
|
+
.map(renderInline)
|
|
151
|
+
.join('\n');
|
|
88
152
|
}
|
|
89
153
|
|
|
90
154
|
export function renderMarkdown(md) {
|
|
91
155
|
return prettify(marked.parse(md || '')).trimEnd();
|
|
92
156
|
}
|
|
93
157
|
|
|
158
|
+
// Format quota usage cho status bar: "4.7k/5k req" với màu theo % used.
|
|
159
|
+
// < 70% → dim (xám)
|
|
160
|
+
// 70-85% → tool (vàng cam)
|
|
161
|
+
// > 85% → err (đỏ)
|
|
162
|
+
// Plan admin (limit = -1 hoặc 0 với unlimited flag) → trả null (không hiện).
|
|
163
|
+
function fmtK(n) {
|
|
164
|
+
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
|
|
165
|
+
return String(n);
|
|
166
|
+
}
|
|
167
|
+
export function formatQuota(u) {
|
|
168
|
+
if (!u || !u.ok) return null;
|
|
169
|
+
if (u.plan === 'admin') return null; // unlimited — đỡ rác status bar.
|
|
170
|
+
const limit = Number(u.limit);
|
|
171
|
+
const remaining = Number(u.remaining);
|
|
172
|
+
if (!Number.isFinite(limit) || !Number.isFinite(remaining) || limit <= 0) return null;
|
|
173
|
+
const used = Math.max(0, limit - remaining);
|
|
174
|
+
const pct = (used / limit) * 100;
|
|
175
|
+
const label = `${fmtK(remaining)}/${fmtK(limit)} req`;
|
|
176
|
+
if (pct > 85) return c.err(label);
|
|
177
|
+
if (pct > 70) return c.tool(label);
|
|
178
|
+
return c.dim(label);
|
|
179
|
+
}
|
|
180
|
+
|
|
94
181
|
export function box(content, title, color = '#a78bfa') {
|
|
95
182
|
return boxen(content, {
|
|
96
183
|
title,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Workflow run journal — resume sau interrupt + per-run log.
|
|
2
|
+
//
|
|
3
|
+
// Mục đích: khi workflow fan-out N sub-agent rồi user Ctrl+C / mất mạng giữa
|
|
4
|
+
// chừng, lần chạy lại (/workflow resume <id>) skip sub-agent đã DONE, chỉ chạy
|
|
5
|
+
// lại sub-agent PENDING/FAILED → tiết kiệm token đã trả.
|
|
6
|
+
//
|
|
7
|
+
// Vị trí: <cwd>/.noob/workflow-runs/<timestamp>-<name>.json (per-workspace,
|
|
8
|
+
// không global — workflow context phụ thuộc repo đang làm).
|
|
9
|
+
//
|
|
10
|
+
// Shape:
|
|
11
|
+
// {
|
|
12
|
+
// id: "2026-06-11T15-30-22-deep-research",
|
|
13
|
+
// name: "deep-research" | null, // builtin/saved name nếu có
|
|
14
|
+
// workflowPrompt: "...", // prompt đã gửi cho parent agent
|
|
15
|
+
// startedAt: ISO, updatedAt: ISO,
|
|
16
|
+
// status: "running" | "done" | "interrupted" | "failed",
|
|
17
|
+
// tasks: [ // mỗi sub-agent là 1 entry
|
|
18
|
+
// {
|
|
19
|
+
// hash: "crc32 hex", // crc32(task + context + model)
|
|
20
|
+
// task: str, context: str, model: str,
|
|
21
|
+
// status: "pending" | "done" | "failed",
|
|
22
|
+
// startedAt: ISO?, finishedAt: ISO?,
|
|
23
|
+
// result: str?, // text trả về (cached cho resume)
|
|
24
|
+
// error: str?,
|
|
25
|
+
// },
|
|
26
|
+
// ...
|
|
27
|
+
// ]
|
|
28
|
+
// }
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
|
|
32
|
+
const DIR_NAME = path.join('.noob', 'workflow-runs');
|
|
33
|
+
|
|
34
|
+
function runsDir(cwd = process.cwd()) {
|
|
35
|
+
return path.join(cwd, DIR_NAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureDir(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(runsDir(cwd), { recursive: true });
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// crc32 stdlib-free — đủ tốt cho dedup task hash (không phải crypto).
|
|
45
|
+
// Polynomial 0xEDB88320, table dựng 1 lần.
|
|
46
|
+
let _crcTable = null;
|
|
47
|
+
function crcTable() {
|
|
48
|
+
if (_crcTable) return _crcTable;
|
|
49
|
+
const t = new Uint32Array(256);
|
|
50
|
+
for (let i = 0; i < 256; i++) {
|
|
51
|
+
let c = i;
|
|
52
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
53
|
+
t[i] = c >>> 0;
|
|
54
|
+
}
|
|
55
|
+
_crcTable = t;
|
|
56
|
+
return t;
|
|
57
|
+
}
|
|
58
|
+
export function crc32(str) {
|
|
59
|
+
const t = crcTable();
|
|
60
|
+
let c = 0xffffffff;
|
|
61
|
+
for (let i = 0; i < str.length; i++) c = t[(c ^ str.charCodeAt(i)) & 0xff] ^ (c >>> 8);
|
|
62
|
+
return ((c ^ 0xffffffff) >>> 0).toString(16).padStart(8, '0');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Hash 1 sub-agent task. Bao gồm task + context + model — đổi bất kỳ field nào
|
|
66
|
+
// → hash khác → resume sẽ chạy lại thay vì hit cache cũ.
|
|
67
|
+
export function hashTask({ task, context, model }) {
|
|
68
|
+
return crc32(JSON.stringify({ task: task || '', context: context || '', model: model || '' }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tên file an toàn — chỉ [a-z0-9-_], thay ký tự không hợp lệ bằng `_`.
|
|
72
|
+
function sanitizeId(s) {
|
|
73
|
+
return String(s || '').toLowerCase().replace(/[^a-z0-9_-]/g, '_').slice(0, 80);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeId(name) {
|
|
77
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, '');
|
|
78
|
+
const safeName = sanitizeId(name || 'adhoc');
|
|
79
|
+
return `${ts}-${safeName}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function filePathFor(id, cwd) {
|
|
83
|
+
return path.join(runsDir(cwd), id + '.json');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Tạo run mới. Trả về { id, path, data }.
|
|
87
|
+
export function createRun({ name = null, workflowPrompt = '', cwd = process.cwd() } = {}) {
|
|
88
|
+
ensureDir(cwd);
|
|
89
|
+
const id = makeId(name);
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
const data = {
|
|
92
|
+
id,
|
|
93
|
+
name,
|
|
94
|
+
workflowPrompt,
|
|
95
|
+
startedAt: now,
|
|
96
|
+
updatedAt: now,
|
|
97
|
+
status: 'running',
|
|
98
|
+
tasks: [],
|
|
99
|
+
};
|
|
100
|
+
const fp = filePathFor(id, cwd);
|
|
101
|
+
fs.writeFileSync(fp, JSON.stringify(data, null, 2), 'utf8');
|
|
102
|
+
return { id, path: fp, data };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Đọc run theo id. Trả về { ok, data, path } hoặc { ok: false, error }.
|
|
106
|
+
export function loadRun(id, cwd = process.cwd()) {
|
|
107
|
+
const fp = filePathFor(sanitizeId(id), cwd);
|
|
108
|
+
if (!fs.existsSync(fp)) return { ok: false, error: 'not_found' };
|
|
109
|
+
try {
|
|
110
|
+
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
111
|
+
return { ok: true, data, path: fp };
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { ok: false, error: e.message };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Ghi đè run. Internal — caller phải đã có data đầy đủ.
|
|
118
|
+
function writeRun(data, cwd = process.cwd()) {
|
|
119
|
+
data.updatedAt = new Date().toISOString();
|
|
120
|
+
const fp = filePathFor(data.id, cwd);
|
|
121
|
+
try {
|
|
122
|
+
fs.writeFileSync(fp, JSON.stringify(data, null, 2), 'utf8');
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Tra hash task trong journal hiện tại. Nếu đã DONE → return cached result.
|
|
130
|
+
// Nếu PENDING/FAILED hoặc chưa có → return null (caller phải chạy thật).
|
|
131
|
+
export function lookupTaskResult(data, hash) {
|
|
132
|
+
if (!data || !Array.isArray(data.tasks)) return null;
|
|
133
|
+
const entry = data.tasks.find((tk) => tk.hash === hash);
|
|
134
|
+
if (!entry) return null;
|
|
135
|
+
if (entry.status === 'done' && typeof entry.result === 'string') return entry.result;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Ghi nhận task BẮT ĐẦU chạy. Tạo entry mới với status=pending nếu chưa có.
|
|
140
|
+
// Trả về entry (mutable reference vào data.tasks).
|
|
141
|
+
export function recordTaskStart(data, { hash, task, context, model }, cwd) {
|
|
142
|
+
let entry = data.tasks.find((tk) => tk.hash === hash);
|
|
143
|
+
if (!entry) {
|
|
144
|
+
entry = { hash, task, context: context || '', model: model || '', status: 'pending' };
|
|
145
|
+
data.tasks.push(entry);
|
|
146
|
+
}
|
|
147
|
+
entry.status = 'pending';
|
|
148
|
+
entry.startedAt = new Date().toISOString();
|
|
149
|
+
delete entry.error;
|
|
150
|
+
writeRun(data, cwd);
|
|
151
|
+
return entry;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Ghi nhận task XONG. Lưu result để resume hit cache.
|
|
155
|
+
export function recordTaskDone(data, hash, result, cwd) {
|
|
156
|
+
const entry = data.tasks.find((tk) => tk.hash === hash);
|
|
157
|
+
if (!entry) return;
|
|
158
|
+
entry.status = 'done';
|
|
159
|
+
entry.finishedAt = new Date().toISOString();
|
|
160
|
+
entry.result = String(result == null ? '' : result);
|
|
161
|
+
delete entry.error;
|
|
162
|
+
writeRun(data, cwd);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Ghi nhận task FAILED. Resume sẽ chạy lại.
|
|
166
|
+
export function recordTaskFailed(data, hash, error, cwd) {
|
|
167
|
+
const entry = data.tasks.find((tk) => tk.hash === hash);
|
|
168
|
+
if (!entry) return;
|
|
169
|
+
entry.status = 'failed';
|
|
170
|
+
entry.finishedAt = new Date().toISOString();
|
|
171
|
+
entry.error = String(error?.message || error || 'unknown');
|
|
172
|
+
writeRun(data, cwd);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Đóng run với status cuối. 'done' / 'interrupted' / 'failed'.
|
|
176
|
+
export function closeRun(data, status, cwd) {
|
|
177
|
+
data.status = status;
|
|
178
|
+
writeRun(data, cwd);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Liệt kê run gần đây nhất (mặc định 20). Sort theo updatedAt desc.
|
|
182
|
+
export function listRuns(limit = 20, cwd = process.cwd()) {
|
|
183
|
+
const dir = runsDir(cwd);
|
|
184
|
+
if (!fs.existsSync(dir)) return [];
|
|
185
|
+
let files;
|
|
186
|
+
try {
|
|
187
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const f of files) {
|
|
193
|
+
try {
|
|
194
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
195
|
+
const total = data.tasks?.length || 0;
|
|
196
|
+
const done = (data.tasks || []).filter((tk) => tk.status === 'done').length;
|
|
197
|
+
out.push({
|
|
198
|
+
id: data.id,
|
|
199
|
+
name: data.name,
|
|
200
|
+
status: data.status,
|
|
201
|
+
updatedAt: data.updatedAt,
|
|
202
|
+
startedAt: data.startedAt,
|
|
203
|
+
total,
|
|
204
|
+
done,
|
|
205
|
+
});
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
out.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
209
|
+
return out.slice(0, limit);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Xoá run theo id. Trả về true nếu xoá được.
|
|
213
|
+
export function deleteRun(id, cwd = process.cwd()) {
|
|
214
|
+
const fp = filePathFor(sanitizeId(id), cwd);
|
|
215
|
+
if (!fs.existsSync(fp)) return false;
|
|
216
|
+
try {
|
|
217
|
+
fs.unlinkSync(fp);
|
|
218
|
+
return true;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|