@kienntpixon/dieu-phoi 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +101 -2
- package/bin/dieu-phoi.cjs +82 -0
- package/hook.cjs +834 -0
- package/lib/aggregate.cjs +337 -0
- package/lib/args.cjs +55 -0
- package/lib/choose.cjs +466 -0
- package/lib/commands.cjs +626 -0
- package/lib/setup.cjs +266 -0
- package/package.json +34 -4
- package/pricing.json +25 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Canonical stats (CONTRACT "Thước đo"). The sink implements the same rules; a parity test
|
|
4
|
+
* compares both against fixtures/parity-expected.json.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const core = require('../hook.cjs');
|
|
8
|
+
|
|
9
|
+
const MIN_N = 5;
|
|
10
|
+
const FAIL_OUTCOMES = new Set(['fail', 'timeout']);
|
|
11
|
+
const HEAVY_STEERS = new Set(['plan', 'redirect']);
|
|
12
|
+
|
|
13
|
+
function tsOf(ev) {
|
|
14
|
+
const t = Date.parse(ev && ev.ts);
|
|
15
|
+
return isNaN(t) ? 0 : t;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sumOrNull(values) {
|
|
19
|
+
if (!values.length) return null;
|
|
20
|
+
let s = 0;
|
|
21
|
+
for (const v of values) {
|
|
22
|
+
if (typeof v !== 'number' || !isFinite(v)) return null;
|
|
23
|
+
s += v;
|
|
24
|
+
}
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mean(values) {
|
|
29
|
+
const v = values.filter((x) => typeof x === 'number' && isFinite(x));
|
|
30
|
+
if (!v.length) return null;
|
|
31
|
+
return v.reduce((a, b) => a + b, 0) / v.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Linear interpolation between closest ranks (same as numpy default / Sheets PERCENTILE). */
|
|
35
|
+
function percentile(values, q) {
|
|
36
|
+
const v = values.filter((x) => typeof x === 'number' && isFinite(x)).sort((a, b) => a - b);
|
|
37
|
+
if (!v.length) return null;
|
|
38
|
+
const pos = (v.length - 1) * q;
|
|
39
|
+
const lo = Math.floor(pos);
|
|
40
|
+
const hi = Math.ceil(pos);
|
|
41
|
+
return v[lo] + (v[hi] - v[lo]) * (pos - lo);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function round6(v) {
|
|
45
|
+
return v === null || v === undefined ? null : Math.round(v * 1e6) / 1e6;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a verdict to the two-layer model. Legacy `rework` (no `by`) = user reject/quality;
|
|
50
|
+
* missing `by` otherwise = agent layer.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeVerdict(ev) {
|
|
53
|
+
if (ev.outcome === 'rework' && !ev.by) {
|
|
54
|
+
return { by: 'user', outcome: 'reject', cause: 'quality', round: Number(ev.round) || null };
|
|
55
|
+
}
|
|
56
|
+
const by = ev.by === 'user' ? 'user' : 'agent';
|
|
57
|
+
let cause = ev.cause || null;
|
|
58
|
+
if (by === 'user' && ev.outcome === 'reject' && cause !== 'scope') cause = 'quality';
|
|
59
|
+
return { by, outcome: ev.outcome, cause, round: Number(ev.round) || null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function steerIdOf(ev) {
|
|
63
|
+
return typeof ev.steer_id === 'string' && ev.steer_id ? ev.steer_id : `${ev.agent_id}|${ev.ts}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Join events. Returns agents (one per real agent), userVerdicts (task layer), overrides, steers.
|
|
68
|
+
*/
|
|
69
|
+
function buildAgents(events) {
|
|
70
|
+
const map = new Map();
|
|
71
|
+
const userVerdicts = [];
|
|
72
|
+
const overrides = [];
|
|
73
|
+
const steers = [];
|
|
74
|
+
const get = (id) => {
|
|
75
|
+
if (!map.has(id)) map.set(id, { agent_id: id, dispatch: null, completes: [], verdicts: [], steers: [] });
|
|
76
|
+
return map.get(id);
|
|
77
|
+
};
|
|
78
|
+
events.forEach((ev, idx) => {
|
|
79
|
+
if (!ev || typeof ev !== 'object') return;
|
|
80
|
+
const e = Object.assign({ _idx: idx }, ev);
|
|
81
|
+
if (ev.kind === 'override') { overrides.push(e); return; }
|
|
82
|
+
if (ev.kind === 'verdict') {
|
|
83
|
+
const v = Object.assign(e, normalizeVerdict(ev));
|
|
84
|
+
if (v.by === 'user') { userVerdicts.push(v); return; }
|
|
85
|
+
}
|
|
86
|
+
if (ev.kind === 'steer') steers.push(e);
|
|
87
|
+
if (typeof ev.agent_id !== 'string' || !ev.agent_id) return;
|
|
88
|
+
const a = get(ev.agent_id);
|
|
89
|
+
if (ev.kind === 'dispatch') { if (!a.dispatch) a.dispatch = e; } else if (ev.kind === 'complete') a.completes.push(e);
|
|
90
|
+
else if (ev.kind === 'verdict') a.verdicts.push(e);
|
|
91
|
+
else if (ev.kind === 'steer') a.steers.push(e);
|
|
92
|
+
});
|
|
93
|
+
const agents = [];
|
|
94
|
+
for (const a of map.values()) {
|
|
95
|
+
if (!a.dispatch && !a.completes.length && !a.verdicts.length) continue; // task-level steer ids etc.
|
|
96
|
+
const d = a.dispatch || {};
|
|
97
|
+
const c0 = a.completes[0] || {};
|
|
98
|
+
const cl = a.completes[a.completes.length - 1] || {};
|
|
99
|
+
const v = a.verdicts.slice().sort((x, y) => tsOf(x) - tsOf(y) || x._idx - y._idx);
|
|
100
|
+
const verdict = v.length ? v[v.length - 1] : null;
|
|
101
|
+
const firstVerdictTs = v.length ? tsOf(v[0]) : Infinity;
|
|
102
|
+
const pick = (k) => (d[k] !== undefined && d[k] !== null ? d[k] : (c0[k] !== undefined && c0[k] !== null ? c0[k] : null));
|
|
103
|
+
const taskType = (d.task_type && d.task_type !== 'untagged' ? d.task_type : null)
|
|
104
|
+
|| (c0.task_type && c0.task_type !== 'untagged' ? c0.task_type : null) || 'untagged';
|
|
105
|
+
// A heavy steer only removes the attempt if it came before the attempt's first agent verdict:
|
|
106
|
+
// steers never erase an already-recorded fail.
|
|
107
|
+
const heavy = a.steers.some((s) => HEAVY_STEERS.has(s.steer_kind) && tsOf(s) < firstVerdictTs);
|
|
108
|
+
agents.push({
|
|
109
|
+
agent_id: a.agent_id,
|
|
110
|
+
task_type: taskType,
|
|
111
|
+
task_id: pick('task_id') || (verdict && verdict.task_id) || (a.steers[0] && a.steers[0].task_id) || null,
|
|
112
|
+
try: Number(pick('try')) || 1,
|
|
113
|
+
role: pick('role') || 'exec',
|
|
114
|
+
score: d.score !== undefined ? d.score : null,
|
|
115
|
+
urgency: d.urgency || null,
|
|
116
|
+
model: core.normalizeModel(cl.model) || core.normalizeModel(d.model) || null,
|
|
117
|
+
effort: d.effort || c0.effort || null,
|
|
118
|
+
dispatched: !!a.dispatch,
|
|
119
|
+
completed: a.completes.length > 0,
|
|
120
|
+
cost_usd: a.completes.length ? sumOrNull(a.completes.map((x) => x.cost_usd)) : null,
|
|
121
|
+
duration_s: a.completes.length ? sumOrNull(a.completes.map((x) => x.duration_s)) : null,
|
|
122
|
+
tool_uses: a.completes.length ? sumOrNull(a.completes.map((x) => x.tool_uses)) : null,
|
|
123
|
+
outcome: verdict ? verdict.outcome : null,
|
|
124
|
+
first_outcome: v.length ? v[0].outcome : null,
|
|
125
|
+
verify: verdict ? verdict.verify : null,
|
|
126
|
+
reason_code: verdict ? verdict.reason_code : null,
|
|
127
|
+
steers: a.steers.map((s) => s.steer_kind).filter(Boolean),
|
|
128
|
+
steered: heavy,
|
|
129
|
+
ts: tsOf(a.dispatch || a.completes[0] || verdict || {}),
|
|
130
|
+
_idx: (a.dispatch || a.completes[0] || verdict || { _idx: 0 })._idx,
|
|
131
|
+
last_complete_ts: a.completes.length ? Math.max(...a.completes.map(tsOf)) : 0,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return { agents, userVerdicts, overrides, steers };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Group agents into tasks (sample unit = task_id; agents without task_id are their own task). */
|
|
138
|
+
function buildTasks(agents, userVerdicts) {
|
|
139
|
+
const map = new Map();
|
|
140
|
+
const byAgent = new Map();
|
|
141
|
+
for (const a of agents) {
|
|
142
|
+
const key = a.task_id ? `task:${a.task_id}` : `agent:${a.agent_id}`;
|
|
143
|
+
if (!map.has(key)) map.set(key, { key, task_id: a.task_id, agents: [], user: [] });
|
|
144
|
+
map.get(key).agents.push(a);
|
|
145
|
+
byAgent.set(a.agent_id, key);
|
|
146
|
+
}
|
|
147
|
+
for (const v of userVerdicts || []) {
|
|
148
|
+
let key = v.task_id ? `task:${v.task_id}` : null;
|
|
149
|
+
if (!key && v.agent_id) key = byAgent.get(v.agent_id) || null; // legacy rework without task_id
|
|
150
|
+
if (!key) continue;
|
|
151
|
+
if (!map.has(key)) map.set(key, { key, task_id: v.task_id, agents: [], user: [] });
|
|
152
|
+
map.get(key).user.push(v);
|
|
153
|
+
}
|
|
154
|
+
const tasks = [];
|
|
155
|
+
for (const t of map.values()) {
|
|
156
|
+
const byTry = (x, y) => (x.try - y.try) || (x.ts - y.ts) || (x._idx - y._idx);
|
|
157
|
+
const attempts = t.agents.filter((a) => a.role !== 'verify').sort(byTry);
|
|
158
|
+
const verifies = t.agents.filter((a) => a.role === 'verify');
|
|
159
|
+
// First exec attempt: lowest try among role=exec (ties: earliest). No exec attempt -> no sample.
|
|
160
|
+
const first = t.agents.filter((a) => a.role === 'exec').sort(byTry)[0] || null;
|
|
161
|
+
const user = t.user.slice().sort((x, y) => ((x.round || 0) - (y.round || 0)) || (tsOf(x) - tsOf(y)) || (x._idx - y._idx));
|
|
162
|
+
|
|
163
|
+
// Effective per-attempt outcome for escalation: agent verdict, overridden by a user verdict
|
|
164
|
+
// landing on the latest attempt dispatched before it (reject/quality = fail, accept = pass).
|
|
165
|
+
for (const a of attempts) a.eff_outcome = a.outcome === 'pass' ? 'pass' : (FAIL_OUTCOMES.has(a.outcome) ? 'fail' : null);
|
|
166
|
+
for (const v of user) {
|
|
167
|
+
if (v.cause === 'scope') continue;
|
|
168
|
+
const vt = tsOf(v);
|
|
169
|
+
let target = null;
|
|
170
|
+
for (const a of attempts) if (a.ts <= vt && (!target || a.ts >= target.ts)) target = a;
|
|
171
|
+
if (!target) target = attempts[attempts.length - 1] || null;
|
|
172
|
+
if (target) target.eff_outcome = v.outcome === 'accept' ? 'pass' : 'fail';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const firstUser = user[0] || null;
|
|
176
|
+
let userOutcome = null;
|
|
177
|
+
if (firstUser) {
|
|
178
|
+
if (firstUser.outcome === 'accept') userOutcome = 'accept';
|
|
179
|
+
else userOutcome = firstUser.cause === 'scope' ? 'reject_scope' : 'reject_quality';
|
|
180
|
+
}
|
|
181
|
+
// Agent layer of the first exec attempt: its first agent verdict.
|
|
182
|
+
const agentFirst = first && first.first_outcome && (first.first_outcome === 'pass' || FAIL_OUTCOMES.has(first.first_outcome))
|
|
183
|
+
? first.first_outcome === 'pass' : null;
|
|
184
|
+
let success = null;
|
|
185
|
+
let source = null;
|
|
186
|
+
if (userOutcome === 'accept') { success = true; source = 'user'; } else if (userOutcome === 'reject_quality') { success = false; source = 'user'; } else if (!userOutcome && agentFirst !== null) {
|
|
187
|
+
success = agentFirst; source = 'agent';
|
|
188
|
+
}
|
|
189
|
+
const lastUser = user[user.length - 1] || null;
|
|
190
|
+
const accepted = lastUser ? lastUser.outcome === 'accept' : attempts.some((a) => a.outcome === 'pass');
|
|
191
|
+
const costed = t.agents.filter((a) => a.completed || a.outcome);
|
|
192
|
+
tasks.push({
|
|
193
|
+
key: t.key,
|
|
194
|
+
task_id: t.task_id,
|
|
195
|
+
task_type: first ? first.task_type : (t.agents[0] && t.agents[0].task_type) || 'untagged',
|
|
196
|
+
first,
|
|
197
|
+
attempts,
|
|
198
|
+
verifies,
|
|
199
|
+
user_verdicts: user,
|
|
200
|
+
user_outcome: userOutcome,
|
|
201
|
+
success,
|
|
202
|
+
success_source: source,
|
|
203
|
+
excluded: !!(first && first.steered) || userOutcome === 'reject_scope',
|
|
204
|
+
accepted,
|
|
205
|
+
agent_first_pass: agentFirst,
|
|
206
|
+
// agents with a verdict but no complete event (hook missed) make cost/time unknown (null)
|
|
207
|
+
total_cost: sumOrNull(costed.map((a) => a.cost_usd)),
|
|
208
|
+
total_time_s: sumOrNull(costed.map((a) => a.duration_s)),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return tasks;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cellKey(type, model, effort) {
|
|
215
|
+
return `${type}|${model || '?'}|${effort || '?'}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Cells task_type x model x effort (tier of the first exec attempt). Canonical fields:
|
|
220
|
+
* n, n_excluded, accept_rate, agent_pass_rate, cost_{mean,p50,p75}_usd, time_{mean,p50,p75}_min.
|
|
221
|
+
* CLI-only extras (not part of the parity contract): dispatched, passes, fails, attempt_*, score, enough, note.
|
|
222
|
+
*/
|
|
223
|
+
function buildCells(tasks, opts) {
|
|
224
|
+
const o = opts || {};
|
|
225
|
+
const lambda = typeof o.lambda === 'number' ? o.lambda : 0.1;
|
|
226
|
+
const cells = new Map();
|
|
227
|
+
for (const t of tasks) {
|
|
228
|
+
const f = t.first;
|
|
229
|
+
if (!f) continue;
|
|
230
|
+
const key = cellKey(t.task_type, f.model, f.effort);
|
|
231
|
+
if (!cells.has(key)) {
|
|
232
|
+
cells.set(key, { task_type: t.task_type, model: f.model, effort: f.effort, dispatched: 0, excluded: 0, tasks: [], agentTasks: [] });
|
|
233
|
+
}
|
|
234
|
+
const c = cells.get(key);
|
|
235
|
+
c.dispatched++;
|
|
236
|
+
if (t.excluded) { c.excluded++; continue; }
|
|
237
|
+
if (t.success !== null) c.tasks.push(t);
|
|
238
|
+
if (t.agent_first_pass !== null) c.agentTasks.push(t);
|
|
239
|
+
}
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const c of cells.values()) {
|
|
242
|
+
const n = c.tasks.length;
|
|
243
|
+
const passes = c.tasks.filter((t) => t.success).length;
|
|
244
|
+
const costs = c.tasks.map((t) => t.total_cost);
|
|
245
|
+
const timesMin = c.tasks.map((t) => (t.total_time_s === null ? null : t.total_time_s / 60));
|
|
246
|
+
const attemptTimes = c.tasks.map((t) => (t.first.duration_s === null ? null : t.first.duration_s / 60));
|
|
247
|
+
const attemptCosts = c.tasks.map((t) => t.first.cost_usd);
|
|
248
|
+
const costMean = mean(costs);
|
|
249
|
+
const timeMean = mean(timesMin);
|
|
250
|
+
out.push({
|
|
251
|
+
task_type: c.task_type,
|
|
252
|
+
model: c.model,
|
|
253
|
+
effort: c.effort,
|
|
254
|
+
n,
|
|
255
|
+
n_excluded: c.excluded,
|
|
256
|
+
accept_rate: n ? round6(passes / n) : null,
|
|
257
|
+
agent_pass_rate: c.agentTasks.length
|
|
258
|
+
? round6(c.agentTasks.filter((t) => t.agent_first_pass).length / c.agentTasks.length) : null,
|
|
259
|
+
cost_mean_usd: round6(costMean),
|
|
260
|
+
cost_p50_usd: round6(percentile(costs, 0.5)),
|
|
261
|
+
cost_p75_usd: round6(percentile(costs, 0.75)),
|
|
262
|
+
time_mean_min: round6(timeMean),
|
|
263
|
+
time_p50_min: round6(percentile(timesMin, 0.5)),
|
|
264
|
+
time_p75_min: round6(percentile(timesMin, 0.75)),
|
|
265
|
+
// CLI-only extras
|
|
266
|
+
dispatched: c.dispatched,
|
|
267
|
+
passes,
|
|
268
|
+
fails: n - passes,
|
|
269
|
+
n_user: c.tasks.filter((t) => t.success_source === 'user').length,
|
|
270
|
+
n_agent: c.agentTasks.length,
|
|
271
|
+
attempt_cost_usd: mean(attemptCosts),
|
|
272
|
+
attempt_cost_p50: percentile(attemptCosts, 0.5),
|
|
273
|
+
attempt_time_min: mean(attemptTimes),
|
|
274
|
+
attempt_p50_min: percentile(attemptTimes, 0.5),
|
|
275
|
+
attempt_p75_min: percentile(attemptTimes, 0.75),
|
|
276
|
+
score: costMean === null && timeMean === null ? null : round6((costMean || 0) + lambda * (timeMean || 0)),
|
|
277
|
+
enough: n >= MIN_N,
|
|
278
|
+
note: n >= MIN_N ? '' : 'chưa đủ dữ liệu',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
out.sort((a, b) => a.task_type.localeCompare(b.task_type)
|
|
282
|
+
|| String(a.model).localeCompare(String(b.model)) || String(a.effort).localeCompare(String(b.effort)));
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const CANONICAL_CELL_FIELDS = [
|
|
287
|
+
'task_type', 'model', 'effort', 'n', 'n_excluded', 'accept_rate', 'agent_pass_rate',
|
|
288
|
+
'cost_mean_usd', 'cost_p50_usd', 'cost_p75_usd', 'time_mean_min', 'time_p50_min', 'time_p75_min',
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
function canonicalCells(cells) {
|
|
292
|
+
return cells.map((c) => Object.fromEntries(CANONICAL_CELL_FIELDS.map((k) => [k, c[k] === undefined ? null : c[k]])));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Steering per task_type, counted in distinct user actions (steer_id), not events. */
|
|
296
|
+
function buildSteers(tasks, steers, agents) {
|
|
297
|
+
const typeOfTask = new Map(tasks.map((t) => [t.task_id, t.task_type]));
|
|
298
|
+
const agentById = new Map(agents.map((a) => [a.agent_id, a]));
|
|
299
|
+
const by = {};
|
|
300
|
+
const row = (type) => by[type] || (by[type] = {
|
|
301
|
+
task_type: type, tasks: 0, steered_tasks: 0, actions: 0, minor: 0, plan: 0, redirect: 0, _tasks: new Set(), _ids: new Set(),
|
|
302
|
+
});
|
|
303
|
+
for (const t of tasks) if (t.first) row(t.task_type).tasks++;
|
|
304
|
+
for (const s of steers) {
|
|
305
|
+
const a = agentById.get(s.agent_id);
|
|
306
|
+
const taskId = s.task_id || (a && a.task_id) || null;
|
|
307
|
+
const type = (taskId && typeOfTask.get(taskId)) || s.task_type || (a && a.task_type) || 'untagged';
|
|
308
|
+
const r = row(type);
|
|
309
|
+
const id = steerIdOf(s);
|
|
310
|
+
if (r._ids.has(id)) continue;
|
|
311
|
+
r._ids.add(id);
|
|
312
|
+
r.actions++;
|
|
313
|
+
if (r[s.steer_kind] !== undefined) r[s.steer_kind]++;
|
|
314
|
+
r._tasks.add(taskId || `agent:${s.agent_id}`);
|
|
315
|
+
}
|
|
316
|
+
return Object.values(by).map((r) => {
|
|
317
|
+
const steered = r._tasks.size;
|
|
318
|
+
const outRow = {
|
|
319
|
+
task_type: r.task_type, tasks: r.tasks, steered_tasks: steered, actions: r.actions,
|
|
320
|
+
minor: r.minor, plan: r.plan, redirect: r.redirect,
|
|
321
|
+
steer_rate: r.tasks ? round6(Math.min(1, steered / r.tasks)) : 0,
|
|
322
|
+
};
|
|
323
|
+
return outRow;
|
|
324
|
+
}).sort((a, b) => b.steer_rate - a.steer_rate || a.task_type.localeCompare(b.task_type));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function analyze(events, opts) {
|
|
328
|
+
const { agents, userVerdicts, overrides, steers } = buildAgents(events);
|
|
329
|
+
const tasks = buildTasks(agents, userVerdicts);
|
|
330
|
+
const cells = buildCells(tasks, opts);
|
|
331
|
+
return { agents, tasks, cells, overrides, userVerdicts, steerEvents: steers, steers: buildSteers(tasks, steers, agents) };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, buildAgents, buildTasks,
|
|
336
|
+
buildCells, buildSteers, canonicalCells, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
337
|
+
};
|
package/lib/args.cjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const core = require('../hook.cjs');
|
|
5
|
+
|
|
6
|
+
const BOOLEAN_FLAGS = new Set(['json', 'help', 'h', 'purge', 'version', 'v', 'dry-run', 'no-sync']);
|
|
7
|
+
|
|
8
|
+
/** Minimal argv parser: positionals, --k v, --k=v, boolean flags. */
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const out = { _: [] };
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const a = argv[i];
|
|
13
|
+
if (a === '--') { out._.push(...argv.slice(i + 1)); break; }
|
|
14
|
+
if (a.startsWith('--') || (a.length === 2 && a[0] === '-' && a !== '-')) {
|
|
15
|
+
let key = a.replace(/^--?/, '');
|
|
16
|
+
let val;
|
|
17
|
+
const eq = key.indexOf('=');
|
|
18
|
+
if (eq >= 0) { val = key.slice(eq + 1); key = key.slice(0, eq); }
|
|
19
|
+
if (val === undefined) {
|
|
20
|
+
if (BOOLEAN_FLAGS.has(key)) val = true;
|
|
21
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) val = argv[++i];
|
|
22
|
+
else val = true;
|
|
23
|
+
}
|
|
24
|
+
out[key] = val;
|
|
25
|
+
} else {
|
|
26
|
+
out._.push(a);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function claudeDirOf(opts) {
|
|
33
|
+
if (opts && typeof opts['claude-dir'] === 'string') return path.resolve(opts['claude-dir']);
|
|
34
|
+
if (process.env.DIEU_PHOI_CLAUDE_DIR) return path.resolve(process.env.DIEU_PHOI_CLAUDE_DIR);
|
|
35
|
+
return core.defaultClaudeDir();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Context (home/paths/config) for CLI commands. */
|
|
39
|
+
function ctxOf(opts) {
|
|
40
|
+
const explicit = opts && typeof opts['claude-dir'] === 'string'
|
|
41
|
+
? path.join(path.resolve(opts['claude-dir']), 'dieu-phoi')
|
|
42
|
+
: (process.env.DIEU_PHOI_HOME || process.env.DIEU_PHOI_CLAUDE_DIR ? null
|
|
43
|
+
: path.join(core.defaultClaudeDir(), 'dieu-phoi'));
|
|
44
|
+
return core.makeCtx(explicit || undefined);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function yesNo(v, dflt) {
|
|
48
|
+
if (v === undefined || v === null || v === true) return v === true ? true : dflt;
|
|
49
|
+
const s = String(v).toLowerCase();
|
|
50
|
+
if (['y', 'yes', 'true', '1', 'c', 'co', 'có'].includes(s)) return true;
|
|
51
|
+
if (['n', 'no', 'false', '0', 'k', 'khong', 'không'].includes(s)) return false;
|
|
52
|
+
throw new Error(`Giá trị y/n không hợp lệ: ${v}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { parseArgs, claudeDirOf, ctxOf, yesNo };
|