magic-builder 1.2.0 → 1.3.1

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.
@@ -3,26 +3,82 @@
3
3
  const { existsSync, readFileSync } = require('fs');
4
4
  const { resolve } = require('path');
5
5
  const { success, fail } = require('../lib/output');
6
+ const { getPerformanceConfigPath, loadPerformanceConfig } = require('../lib/config');
6
7
 
7
8
  const DEFAULTS = {
8
9
  endpoint: 'https://people.bytedance.net/perf/api/foundation/v2/draft',
10
+ submitEndpoint: 'https://people.bytedance.net/perf/api/review/v2/stage',
9
11
  settingsEndpoint: 'https://people.bytedance.net/perf/api/user/settings',
10
12
  draftEndpoint: 'https://people.bytedance.net/perf/api/foundation/draft',
11
13
  };
12
14
 
13
15
  async function run(args, opts) {
14
- if (args.length) fail('Usage: magic-builder performance --cookie <content|file> --markdown <content|file|url>', 'E_INVALID_ARGS');
16
+ const action = args[0] || 'draft';
17
+ if (args.length > 1 || !['draft', 'submit', 'self-review', 'key-works', 'review-users', 'invite-review'].includes(action)) {
18
+ fail('Usage: magic-builder performance [submit|self-review|key-works|review-users|invite-review] --review-url <url> --cookie <content|file>', 'E_INVALID_ARGS');
19
+ }
15
20
 
16
21
  const cookie = readCookie(opts.cookie, opts.cookieFile);
17
22
  if (!cookie) fail('Missing Cookie. Use --cookie <content|file> or --cookie-file <file>.', 'E_INVALID_ARGS');
18
23
 
19
- const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
20
- if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
21
-
22
24
  const csrf = getCookie(cookie, 'x-f-csrf');
23
25
  if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
24
26
 
25
- const config = await resolveConfig(opts, cookie);
27
+ if (action === 'key-works') {
28
+ const data = await getKeyWorks(opts, cookie, csrf);
29
+ success(data, opts);
30
+ return;
31
+ }
32
+ if (action === 'review-users') {
33
+ const data = await getReviewUsers(opts, cookie, csrf);
34
+ success(data, opts);
35
+ return;
36
+ }
37
+ if (action === 'invite-review') {
38
+ const payload = readInviteReviewPayload(opts);
39
+ const endpoint = String(opts.endpoint || DEFAULTS.endpoint);
40
+ if (opts.dryRun) {
41
+ success({ dryRun: true, action: 'invite-review', endpoint, payload }, opts);
42
+ return;
43
+ }
44
+ const review = parseReviewLocation(opts.reviewUrl || opts.referer);
45
+ const data = await postPerformance(endpoint, payload, cookie, csrf, {
46
+ referer: String(opts.referer || review.referer || 'https://people.bytedance.net/performance/perf/review'),
47
+ tenantId: String(opts.tenantId || ''),
48
+ });
49
+ success(data, opts);
50
+ return;
51
+ }
52
+
53
+ const config = await resolveConfig(opts, cookie, action === 'self-review' ? 'self_review' : 'confirm_invitation');
54
+ if (action === 'self-review') {
55
+ const updates = readSelfReviewInputs(opts);
56
+ const payload = buildSelfReviewPayloadFromDraft(config.draft, updates);
57
+ if (opts.dryRun) {
58
+ success({ dryRun: true, action: 'self-review', endpoint: config.endpoint, payload }, opts);
59
+ return;
60
+ }
61
+ const data = await postPerformance(config.endpoint, payload, cookie, csrf, config);
62
+ success(data, opts);
63
+ return;
64
+ }
65
+ if (action === 'submit') {
66
+ const payload = buildStagePayloadFromDraft(config.draft, config);
67
+ if (opts.dryRun) {
68
+ success({ dryRun: true, action: 'submit', endpoint: config.submitEndpoint, payload }, opts);
69
+ return;
70
+ }
71
+ if (!opts.yes) {
72
+ fail('Formal performance submission requires --yes. Run with --dry-run first to review the payload.', 'E_CONFIRM_REQUIRED');
73
+ }
74
+ const data = await postPerformance(config.submitEndpoint, payload, cookie, csrf, config);
75
+ success(data, opts);
76
+ return;
77
+ }
78
+
79
+ const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
80
+ if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
81
+
26
82
  const payload = config.draft
27
83
  ? buildPayloadFromDraft(markdown, config.draft, config)
28
84
  : buildPayload(markdown, config);
@@ -31,7 +87,143 @@ async function run(args, opts) {
31
87
  return;
32
88
  }
33
89
 
34
- const response = await fetch(config.endpoint, {
90
+ const data = await postPerformance(config.endpoint, payload, cookie, csrf, config);
91
+ success(data, opts);
92
+ }
93
+
94
+ function readInviteReviewPayload(opts) {
95
+ if (opts.payload && opts.payloadFile) {
96
+ fail('Pass either --payload or --payload-file, not both.', 'E_INVALID_ARGS');
97
+ }
98
+ const input = opts.payloadFile
99
+ ? readFileSync(resolve(String(opts.payloadFile)), 'utf8')
100
+ : String(opts.payload || '');
101
+ if (!input.trim()) fail('invite-review requires --payload <json> or --payload-file <file>.', 'E_INVALID_ARGS');
102
+ const payload = parseJson(input);
103
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
104
+ fail('Invite-review payload must be a valid JSON object.', 'E_INVALID_ARGS');
105
+ }
106
+ const missing = ['key', 'data', 'version'].filter(key => payload[key] === undefined || payload[key] === null);
107
+ if (missing.length) fail(`Invite-review payload is missing ${missing.join(', ')}.`, 'E_INVALID_ARGS');
108
+ if (!String(payload.key).includes('__invite_review__')) {
109
+ fail('Invite-review payload key must contain __invite_review__.', 'E_INVALID_ARGS');
110
+ }
111
+ return normalizePerformancePayload(payload);
112
+ }
113
+
114
+ function normalizePerformancePayload(payload) {
115
+ const normalized = structuredClone(payload);
116
+ normalizePerformanceNode(normalized);
117
+ return normalized;
118
+ }
119
+
120
+ function normalizePerformanceNode(node) {
121
+ if (!node || typeof node !== 'object') return;
122
+ if (Array.isArray(node)) {
123
+ node.forEach(normalizePerformanceNode);
124
+ return;
125
+ }
126
+ for (const [key, value] of Object.entries(node)) {
127
+ if (key === 'insert' && typeof value === 'string') {
128
+ node[key] = value.replace(/\\n(?=\r?\n|$)/g, '\n');
129
+ continue;
130
+ }
131
+ if (typeof value === 'string' && ['text', 'value', 'rich_text'].includes(key)) {
132
+ const embedded = parseJson(value);
133
+ if (embedded && typeof embedded === 'object') {
134
+ normalizePerformanceNode(embedded);
135
+ node[key] = JSON.stringify(embedded);
136
+ }
137
+ continue;
138
+ }
139
+ normalizePerformanceNode(value);
140
+ }
141
+ }
142
+
143
+ async function getKeyWorks(opts, cookie, csrf) {
144
+ if (!opts.rootReviewId) {
145
+ fail('key-works requires --root-review-id <id>.', 'E_INVALID_ARGS');
146
+ }
147
+ const personalConfig = opts.performanceConfig || loadPerformanceConfig();
148
+ const templateId = String(opts.templateId || personalConfig.invite_review_template_id || personalConfig.inviteReviewTemplateId || '');
149
+ if (!templateId) {
150
+ fail(`Missing template id. Use --template-id <id> or set invite_review_template_id in ${getPerformanceConfigPath()}.`, 'E_INVALID_ARGS');
151
+ }
152
+ const endpoint = assertPeoplePerfEndpoint(opts.stageEndpoint || DEFAULTS.submitEndpoint);
153
+ endpoint.searchParams.set('template_id', templateId);
154
+ endpoint.searchParams.set('root_review_id', String(opts.rootReviewId));
155
+ const referer = String(opts.referer || opts.reviewUrl || 'https://people.bytedance.net/performance/perf/review');
156
+ const headers = {
157
+ accept: 'application/json, text/plain, */*',
158
+ 'accept-language': 'zh-CN,zh;q=0.9',
159
+ cookie,
160
+ referer,
161
+ 'x-f-csrf': csrf,
162
+ 'x-f-lang': 'zh-CN',
163
+ 'x-f-timezone': 'Asia/Shanghai',
164
+ };
165
+ if (opts.tenantId) headers['rpc-persist-lane-c-perfx-tenant-id'] = String(opts.tenantId);
166
+ const response = await fetch(endpoint, { method: 'GET', headers });
167
+ const text = await response.text();
168
+ const data = parseJson(text);
169
+ if (!response.ok) {
170
+ const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
171
+ error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
172
+ throw error;
173
+ }
174
+ if (data === null) {
175
+ const error = new Error('Performance response is not valid JSON.');
176
+ error.code = 'E_REQUEST_FAILED';
177
+ throw error;
178
+ }
179
+ return data;
180
+ }
181
+
182
+ async function getReviewUsers(opts, cookie, csrf) {
183
+ const location = opts.reviewUrl ? parseReviewLocation(opts.reviewUrl) : {};
184
+ const reviewId = String(opts.reviewId || location.reviewId || '');
185
+ const stageId = String(opts.stageId || location.formId || '');
186
+ if (!reviewId || !stageId) {
187
+ fail('review-users requires --review-url <url> or both --review-id <id> and --stage-id <id>.', 'E_INVALID_ARGS');
188
+ }
189
+ const base = assertPeoplePerfEndpoint(opts.reviewUsersEndpoint || DEFAULTS.submitEndpoint);
190
+ const endpoint = new URL(`${base.origin}/perf/api/review/v2/stage/${encodeURIComponent(reviewId)}/list_invite_review`);
191
+ const referer = String(opts.referer || location.referer || `https://people.bytedance.net/performance/perf/review/${reviewId}/${stageId}/members`);
192
+ const headers = {
193
+ accept: 'application/json, text/plain, */*',
194
+ 'accept-language': 'zh-CN,zh;q=0.9',
195
+ 'content-type': 'application/json;charset=UTF-8',
196
+ cookie,
197
+ origin: 'https://people.bytedance.net',
198
+ referer,
199
+ 'x-f-csrf': csrf,
200
+ 'x-f-lang': 'zh-CN',
201
+ 'x-f-timezone': 'Asia/Shanghai',
202
+ };
203
+ if (opts.tenantId) headers['rpc-persist-lane-c-perfx-tenant-id'] = String(opts.tenantId);
204
+ const response = await fetch(endpoint, {
205
+ method: 'POST',
206
+ headers,
207
+ body: JSON.stringify({ stage_id: stageId }),
208
+ });
209
+ const text = await response.text();
210
+ const data = parseJson(text);
211
+ if (!response.ok) {
212
+ const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
213
+ error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
214
+ throw error;
215
+ }
216
+ if (data === null) {
217
+ const error = new Error('Performance response is not valid JSON.');
218
+ error.code = 'E_REQUEST_FAILED';
219
+ throw error;
220
+ }
221
+ return data;
222
+ }
223
+
224
+ async function postPerformance(endpoint, payload, cookie, csrf, config) {
225
+ assertPeoplePerfEndpoint(endpoint);
226
+ const response = await fetch(endpoint, {
35
227
  method: 'POST',
36
228
  headers: {
37
229
  accept: 'application/json, text/plain, */*',
@@ -55,7 +247,7 @@ async function run(args, opts) {
55
247
  error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
56
248
  throw error;
57
249
  }
58
- success(data === null ? text : data, opts);
250
+ return data === null ? text : data;
59
251
  }
60
252
 
61
253
  function readCookie(value, file) {
@@ -105,20 +297,21 @@ function getCookie(cookie, name) {
105
297
  return '';
106
298
  }
107
299
 
108
- async function resolveConfig(opts, cookie) {
300
+ async function resolveConfig(opts, cookie, draftKind = 'confirm_invitation') {
109
301
  const review = parseReviewLocation(opts.reviewUrl || opts.referer, opts.reviewId, opts.formId);
110
302
  if (review.reviewId && review.formId) {
111
303
  const settings = await requestJson(opts.settingsEndpoint || DEFAULTS.settingsEndpoint, cookie, review.referer);
112
304
  const operatorId = String(opts.operatorId || settings?.data?.user?.id || '');
113
305
  const tenantId = String(opts.tenantId || settings?.data?.tenant?.id || '');
114
306
  if (!operatorId) fail('Unable to resolve operator id from /perf/api/user/settings.', 'E_REQUEST_FAILED');
115
- const key = `${review.reviewId}__confirm_invitation__${operatorId}__${review.formId}`;
307
+ const key = buildDraftKey(review.reviewId, draftKind, operatorId, review.formId);
116
308
  const draftUrl = new URL(opts.draftEndpoint || DEFAULTS.draftEndpoint);
117
309
  draftUrl.searchParams.set('key', key);
118
310
  const draftResponse = await requestJson(draftUrl.toString(), cookie, review.referer);
119
311
  if (!draftResponse?.success || !draftResponse?.data) fail('Unable to load the current performance draft.', 'E_REQUEST_FAILED');
120
312
  return {
121
313
  endpoint: String(opts.endpoint || DEFAULTS.endpoint),
314
+ submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
122
315
  reviewId: review.reviewId,
123
316
  formId: review.formId,
124
317
  operatorId,
@@ -126,6 +319,7 @@ async function resolveConfig(opts, cookie) {
126
319
  referer: review.referer,
127
320
  uid: String(opts.uid || `magic-builder-${Date.now()}`),
128
321
  version: opts.draftVersion === undefined ? undefined : Number(opts.draftVersion),
322
+ templateGroupId: String(opts.templateGroupId || ''),
129
323
  draft: draftResponse.data,
130
324
  };
131
325
  }
@@ -137,6 +331,7 @@ async function resolveConfig(opts, cookie) {
137
331
  }
138
332
  return {
139
333
  endpoint: String(opts.endpoint || DEFAULTS.endpoint),
334
+ submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
140
335
  reviewId: String(opts.reviewId),
141
336
  formId: String(opts.formId),
142
337
  templateId: String(opts.templateId),
@@ -147,6 +342,7 @@ async function resolveConfig(opts, cookie) {
147
342
  sourceId: String(opts.sourceId),
148
343
  fieldSourceId: String(opts.fieldSourceId),
149
344
  rootReviewId: String(opts.rootReviewId),
345
+ templateGroupId: String(opts.templateGroupId || ''),
150
346
  version: Number(opts.draftVersion || opts.version),
151
347
  tenantId: String(opts.tenantId || ''),
152
348
  referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${opts.reviewId}/${opts.formId}?mode=editable`),
@@ -154,6 +350,36 @@ async function resolveConfig(opts, cookie) {
154
350
  };
155
351
  }
156
352
 
353
+ function buildDraftKey(reviewId, draftKind, operatorId, formId) {
354
+ return `${reviewId}__${draftKind}__${operatorId}__${formId}`;
355
+ }
356
+
357
+ function readSelfReviewInputs(opts) {
358
+ const inputs = [
359
+ ['good', 'goodFile'],
360
+ ['improve', 'improveFile'],
361
+ ['valuesComment', 'valuesCommentFile'],
362
+ ];
363
+ const updates = {};
364
+ for (const [valueKey, fileKey] of inputs) {
365
+ const hasValue = Object.prototype.hasOwnProperty.call(opts, valueKey);
366
+ const hasFile = Object.prototype.hasOwnProperty.call(opts, fileKey);
367
+ if (hasValue && hasFile) {
368
+ fail(`Pass either --${toKebab(valueKey)} or --${toKebab(fileKey)}, not both.`, 'E_INVALID_ARGS');
369
+ }
370
+ if (hasFile) updates[valueKey] = readFileSync(resolve(String(opts[fileKey])), 'utf8');
371
+ else if (hasValue) updates[valueKey] = String(opts[valueKey]);
372
+ }
373
+ if (!Object.keys(updates).length) {
374
+ fail('Pass at least one of --good, --improve, or --values-comment (or the matching --*-file option).', 'E_INVALID_ARGS');
375
+ }
376
+ return updates;
377
+ }
378
+
379
+ function toKebab(value) {
380
+ return String(value).replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
381
+ }
382
+
157
383
  function parseReviewLocation(value, reviewId, formId) {
158
384
  if (reviewId && formId) {
159
385
  return {
@@ -172,6 +398,7 @@ function parseReviewLocation(value, reviewId, formId) {
172
398
  }
173
399
 
174
400
  async function requestJson(url, cookie, referer) {
401
+ assertPeoplePerfEndpoint(url);
175
402
  const response = await fetch(url, {
176
403
  headers: { accept: 'application/json, text/plain, */*', cookie, referer },
177
404
  });
@@ -181,6 +408,16 @@ async function requestJson(url, cookie, referer) {
181
408
  return data;
182
409
  }
183
410
 
411
+ function assertPeoplePerfEndpoint(value) {
412
+ const url = new URL(String(value || ''));
413
+ if (url.origin !== 'https://people.bytedance.net' || !url.pathname.startsWith('/perf/api/')) {
414
+ const error = new Error('Performance API endpoint must use https://people.bytedance.net/perf/api/.');
415
+ error.code = 'E_INVALID_ARGS';
416
+ throw error;
417
+ }
418
+ return url;
419
+ }
420
+
184
421
  function buildPayload(markdown, config) {
185
422
  const now = Date.now();
186
423
  const sections = splitMarkdownSections(markdown);
@@ -256,6 +493,92 @@ function buildPayloadFromDraft(markdown, draft, config) {
256
493
  };
257
494
  }
258
495
 
496
+ function buildSelfReviewPayloadFromDraft(draft, updates) {
497
+ const snapshot = structuredClone(draft);
498
+ const units = snapshot?.data?.units;
499
+ if (!Array.isArray(units)) throw selfReviewSchemaError('Current self-review draft does not contain data.units.');
500
+ const fields = units.flatMap(unit => Array.isArray(unit?.fields) ? unit.fields : []);
501
+ const textFields = fields.filter(field => field?.entityType === 'text');
502
+ const tagTextFields = fields.filter(field => field?.entityType === 'tag_text');
503
+ if (textFields.length !== 2 || tagTextFields.length !== 1) {
504
+ throw selfReviewSchemaError('Unable to identify the self-review text fields from the current template.');
505
+ }
506
+ if (Object.prototype.hasOwnProperty.call(updates, 'good')) {
507
+ textFields[0].value = selfReviewTextValue(updates.good);
508
+ }
509
+ if (Object.prototype.hasOwnProperty.call(updates, 'improve')) {
510
+ textFields[1].value = selfReviewTextValue(updates.improve);
511
+ }
512
+ if (Object.prototype.hasOwnProperty.call(updates, 'valuesComment')) {
513
+ tagTextFields[0].json_value = selfReviewTextValue(updates.valuesComment);
514
+ }
515
+ return {
516
+ key: snapshot.key,
517
+ data: snapshot.data,
518
+ version: Number(snapshot.version),
519
+ };
520
+ }
521
+
522
+ function selfReviewSchemaError(message) {
523
+ const error = new Error(message);
524
+ error.code = 'E_REQUEST_FAILED';
525
+ return error;
526
+ }
527
+
528
+ function selfReviewTextValue(text) {
529
+ return { '0': { ops: markdownToDelta(String(text)), zoneId: '0', zoneType: 'Z' } };
530
+ }
531
+
532
+ function buildStagePayloadFromDraft(draft, config = {}) {
533
+ if (!draft?.data?.keyWorks) fail('Current draft does not contain keyWorks.', 'E_REQUEST_FAILED');
534
+ const keyWorks = structuredClone(draft.data.keyWorks);
535
+ const stageId = String(config.stageId || config.formId || '');
536
+ const rootReviewId = String(config.rootReviewId || draft.data.rootReviewId || draft.data.root_review_id || '');
537
+ const templateId = String(config.templateId || keyWorks.template_id || keyWorks.templateId || '');
538
+ const templateGroupId = String(
539
+ config.templateGroupId
540
+ || draft.data.templateGroupId
541
+ || draft.data.template_group_id
542
+ || keyWorks.templateGroupId
543
+ || keyWorks.template_group_id
544
+ || ''
545
+ );
546
+ const missing = [
547
+ ['root_review_id', rootReviewId],
548
+ ['template_id', templateId],
549
+ ['template_group_id', templateGroupId],
550
+ ['stage_id', stageId],
551
+ ].filter(([, value]) => !value).map(([name]) => name);
552
+ if (missing.length) {
553
+ fail(`Unable to build formal submission payload; missing ${missing.join(', ')}. Pass --template-group-id when it is absent from the draft.`, 'E_INVALID_ARGS');
554
+ }
555
+ if (!Array.isArray(keyWorks.units) || !keyWorks.units.length) {
556
+ fail('Current draft does not contain keyWorks.units.', 'E_REQUEST_FAILED');
557
+ }
558
+ return {
559
+ root_review_id: rootReviewId,
560
+ template_id: templateId,
561
+ template_group_id: templateGroupId,
562
+ stage_id: stageId,
563
+ stage_ids: [stageId],
564
+ data: { units: keyWorks.units.filter(unit => unit?.type === 'object').map(serializeStageUnit) },
565
+ };
566
+ }
567
+
568
+ function serializeStageUnit(unit) {
569
+ const output = structuredClone(unit);
570
+ for (const field of output.fields || []) {
571
+ delete field.json_value;
572
+ for (const group of field.sub_units || []) {
573
+ for (const subUnit of group || []) {
574
+ if (subUnit?.value && typeof subUnit.value !== 'string') subUnit.value = JSON.stringify(subUnit.value);
575
+ delete subUnit.json_value;
576
+ }
577
+ }
578
+ }
579
+ return output;
580
+ }
581
+
259
582
  function createSubUnits(markdown, textFieldId, sourceId, now = Date.now()) {
260
583
  return splitMarkdownSections(markdown).map((section, index) => {
261
584
  const value = { '0': { ops: markdownToDelta(section), zoneId: '0', zoneType: 'Z' } };
@@ -273,17 +596,28 @@ function splitMarkdownSections(markdown) {
273
596
  for (const line of lines) {
274
597
  if (/^##\s+/.test(line)) {
275
598
  if (current.some(item => item.trim()) && !current.every(item => /^#\s+/.test(item) || !item.trim())) {
276
- sections.push(current.join('\n').trim());
599
+ sections.push(cleanMarkdownSection(current.join('\n')));
277
600
  }
278
601
  current = [`# ${line.replace(/^##\s+/, '')}`];
279
602
  } else if (current.length || !/^#\s+/.test(line)) {
280
603
  current.push(line);
281
604
  }
282
605
  }
283
- if (current.some(item => item.trim())) sections.push(current.join('\n').trim());
606
+ if (current.some(item => item.trim())) sections.push(cleanMarkdownSection(current.join('\n')));
284
607
  return sections.length ? sections : [text];
285
608
  }
286
609
 
610
+ function cleanMarkdownSection(value) {
611
+ const cleaned = String(value || '').trim().replace(/(?:\n\s*(?:---+|\*\*\*+|___+)\s*)+$/u, '').trim();
612
+ return cleaned.split('\n').map((line, index) => {
613
+ if (index === 0) return line.replace(/^#{1,6}\s+/, '# ');
614
+ if (!line.trim() || /^\s*(?:[-*+]\s+|\d+[.)]\s+)/u.test(line)) return line;
615
+ if (/^\s*[-*+]\S/u.test(line)) return line.replace(/^(\s*[-*+])\s*/u, '$1 ');
616
+ if (/^\s*\d+[.)]\S/u.test(line)) return line.replace(/^(\s*\d+[.)])\s*/u, '$1 ');
617
+ return `- ${line.trim()}`;
618
+ }).join('\n');
619
+ }
620
+
287
621
  function markdownToDelta(markdown) {
288
622
  const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
289
623
  const ops = [];
@@ -298,18 +632,29 @@ function markdownToDelta(markdown) {
298
632
  ops.push({ insert: '\n', attributes: { 'code-block': true } });
299
633
  continue;
300
634
  }
635
+ if (!line.trim()) continue;
301
636
  const heading = line.match(/^(#{1,6})\s+(.+)$/);
302
- const unordered = line.match(/^\s*[-*+]\s+(.+)$/);
303
- const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
637
+ const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
638
+ const ordered = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
304
639
  const quote = line.match(/^\s*>\s?(.*)$/);
305
- const content = heading?.[2] ?? unordered?.[1] ?? ordered?.[1] ?? quote?.[1] ?? line;
306
- appendInline(ops, content);
307
- const attributes = heading ? { header: heading[1].length }
308
- : unordered ? { list: 'bullet' }
309
- : ordered ? { list: 'ordered' }
310
- : quote ? { blockquote: true }
311
- : undefined;
312
- ops.push(attributes ? { insert: '\n', attributes } : { insert: '\n' });
640
+ if (heading) {
641
+ ops.push({ insert: `${heading[1]} ${heading[2]}`, attributes: { bold: true } });
642
+ ops.push({ insert: '\n' });
643
+ continue;
644
+ }
645
+ if (unordered || ordered) {
646
+ const indent = (unordered?.[1] || ordered?.[1] || '').replace(/\t/g, ' ').length;
647
+ const level = Math.max(1, Math.floor(indent / 2) + 1);
648
+ const list = `${unordered ? 'bullet' : 'ordered'}${level}`;
649
+ const marker = unordered ? '*' : `${ordered[2]}.`;
650
+ const content = unordered?.[2] ?? ordered?.[3] ?? '';
651
+ ops.push({ insert: marker, attributes: { list, lmkr: '1' } });
652
+ appendInline(ops, content);
653
+ ops.push({ insert: '\n' });
654
+ continue;
655
+ }
656
+ appendInline(ops, quote?.[1] ?? line);
657
+ ops.push(quote ? { insert: '\n', attributes: { blockquote: true } } : { insert: '\n' });
313
658
  }
314
659
  if (ops.length > 1 && lines.at(-1) === '') ops.pop();
315
660
  return ops.length ? ops : [{ insert: '\n' }];
@@ -334,4 +679,4 @@ function parseJson(text) {
334
679
  try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
335
680
  }
336
681
 
337
- module.exports = { run, readCookie, normalizeCookie, getCookie, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft };
682
+ module.exports = { run, getKeyWorks, getReviewUsers, readInviteReviewPayload, normalizePerformancePayload, readCookie, normalizeCookie, getCookie, readSelfReviewInputs, buildDraftKey, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft, buildSelfReviewPayloadFromDraft, buildStagePayloadFromDraft, assertPeoplePerfEndpoint };
@@ -24,7 +24,7 @@ async function run(args, opts) {
24
24
  if (sub !== 'check-update' && sub !== 'update' && sub !== 'install') {
25
25
  fail('Usage: magic-builder skill <check-update|update|install>', 'E_INVALID_ARGS');
26
26
  }
27
- const skillsRoot = path.resolve(opts.skillsRoot || path.join(os.homedir(), '.codex', 'skills'));
27
+ const skillsRoot = path.resolve(opts.skillsRoot || defaultSkillsRoot());
28
28
  const environment = resolveEnvironment(String(opts.environment || 'auto'), skillsRoot);
29
29
  const remote = await prepareRemotePackage();
30
30
  const report = buildReport(remote, skillsRoot, environment);
@@ -106,7 +106,7 @@ function buildCloudUpdateDescriptor(report, options = {}) {
106
106
  }] : [],
107
107
  instructions: [
108
108
  'Use the cloud runtime managed skill update/install mechanism.',
109
- 'Do not copy files into ~/.codex/skills or overwrite local filesystem paths in cloud mode.',
109
+ 'Do not copy files into ~/.agents/skills or overwrite local filesystem paths in cloud mode.',
110
110
  'If no managed cloud updater is exposed, report this descriptor and do not claim the update was applied.',
111
111
  ],
112
112
  };
@@ -142,7 +142,7 @@ function readSkillMetadata(skillDir) {
142
142
  if (!fs.existsSync(skillPath)) return { exists: false, name: null, version: null, path: skillPath };
143
143
  const text = fs.readFileSync(skillPath, 'utf8');
144
144
  const nameMatch = text.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
145
- const versionMatch = text.match(/^version:\s*["']?([^"'\n]+)["']?\s*$/m);
145
+ const versionMatch = text.match(/^\s*version:\s*["']?([^"'\n]+)["']?\s*$/m);
146
146
  return {
147
147
  exists: true,
148
148
  name: nameMatch ? nameMatch[1].trim() : null,
@@ -189,4 +189,8 @@ function ensureSafeSkillId(id) {
189
189
  if (!/^[a-z0-9][a-z0-9-]*$/i.test(id)) throw new Error(`Unsafe skill id in update package: ${id}`);
190
190
  }
191
191
 
192
- module.exports = { run };
192
+ function defaultSkillsRoot(homeDir = os.homedir()) {
193
+ return path.join(homeDir, '.agents', 'skills');
194
+ }
195
+
196
+ module.exports = { run, defaultSkillsRoot };