flower-trellis 0.4.0-beta.4 → 0.4.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.
Files changed (25) hide show
  1. package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +17 -8
  2. package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +8 -3
  3. package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +17 -8
  4. package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +8 -3
  5. package/enhancements/0.6/overrides/skills/trellis-finish-work.md +14 -0
  6. package/enhancements/0.6/overrides/workflow-states/in_progress-inline.md +2 -2
  7. package/enhancements/0.6/overrides/workflow-states/in_progress.md +2 -2
  8. package/enhancements/0.6/overrides/workflow-states/no_task.md +2 -2
  9. package/enhancements/0.6/overrides/workflow-states/planning-inline.md +1 -1
  10. package/enhancements/0.6/overrides/workflow-states/planning.md +1 -1
  11. package/enhancements/0.6/overrides/workflow.md +18 -10
  12. package/enhancements/0.6/scripts/push_snapshot.py +387 -0
  13. package/enhancements/0.6/scripts/spec_router.py +244 -33
  14. package/enhancements/MANIFEST.json +3 -4
  15. package/package.json +1 -1
  16. package/src/lib/copy-scripts.js +2 -0
  17. package/src/lib/skill-catalog.js +0 -1
  18. package/enhancements/common/.common/.claude/skills/sub2api-account-json-fix/SKILL.md +0 -47
  19. package/enhancements/common/.common/.claude/skills/sub2api-account-json-fix/env/push.env.example +0 -8
  20. package/enhancements/common/.common/.claude/skills/sub2api-account-json-fix/scripts/run.sh +0 -27
  21. package/enhancements/common/.common/.codex/skills/sub2api-account-json-fix/SKILL.md +0 -126
  22. package/enhancements/common/.common/.codex/skills/sub2api-account-json-fix/agents/openai.yaml +0 -4
  23. package/enhancements/common/.common/.codex/skills/sub2api-account-json-fix/env/push.env.example +0 -8
  24. package/enhancements/common/.common/.codex/skills/sub2api-account-json-fix/scripts/fix_exported_account_json.py +0 -890
  25. package/enhancements/common/.common/.codex/skills/sub2api-account-json-fix/scripts/run.sh +0 -26
@@ -2,8 +2,8 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """从 `.trellis/spec/` 发现相关项目 SOP/spec 文件。
4
4
 
5
- 这个 helper 刻意保持轻量:只返回候选路径和命中原因,让 AI 在执行流程性或
6
- 高影响动作前读取匹配文件;它不把完整文档注入上下文。
5
+ 这个 helper 刻意保持轻量:只返回候选路径、置信度和命中原因,让 AI 在项目
6
+ 局部知识可能影响做法的决策边界前读取匹配文件;它不把完整文档注入上下文。
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -21,40 +21,72 @@ MAX_BODY_CHARS = 8000
21
21
  DEFAULT_LIMIT = 3
22
22
  MIN_SCORE = 3
23
23
  MIN_BODY_ONLY_HITS = 5
24
- MIN_HEADING_BODY_HITS = 3
25
- BODY_WEAK_TOKENS = {
24
+ MIN_ANCHORED_BODY_HITS = 2
25
+ WEAK_TOKENS = {
26
26
  "action",
27
27
  "actions",
28
28
  "after",
29
+ "and",
29
30
  "before",
31
+ "change",
32
+ "changes",
33
+ "cli",
30
34
  "command",
31
35
  "commands",
36
+ "commit",
32
37
  "context",
33
38
  "current",
39
+ "data",
40
+ "documentation",
41
+ "edit",
34
42
  "file",
35
43
  "files",
44
+ "flow",
45
+ "flower",
46
+ "for",
47
+ "from",
48
+ "guide",
49
+ "guides",
50
+ "in",
51
+ "index",
36
52
  "match",
37
53
  "matched",
38
54
  "matches",
39
55
  "matching",
56
+ "md",
40
57
  "normal",
58
+ "of",
59
+ "or",
41
60
  "path",
42
61
  "paths",
43
62
  "project",
63
+ "py",
44
64
  "read",
65
+ "readme",
45
66
  "reason",
46
67
  "reasons",
47
68
  "relevant",
69
+ "run",
70
+ "simple",
71
+ "small",
48
72
  "sop",
49
73
  "spec",
50
74
  "status",
51
75
  "task",
52
76
  "tasks",
77
+ "the",
78
+ "to",
79
+ "trellis",
80
+ "typo",
81
+ "update",
82
+ "with",
53
83
  "workflow",
54
84
  }
55
- TOKEN_RE = re.compile(r"[A-Za-z0-9_.@/-]+|[\u4e00-\u9fff]{2,}")
85
+ TOKEN_RE = re.compile(r"[A-Za-z0-9@]+|[\u4e00-\u9fff]+")
86
+ CJK_RE = re.compile(r"^[\u4e00-\u9fff]+$")
56
87
  HEADER_RE = re.compile(r"^\s{0,3}#{1,3}\s+(.+?)\s*$", re.MULTILINE)
57
88
  FRONTMATTER_BOUNDARY_RE = re.compile(r"^---\s*$")
89
+ MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+\.md(?:#[^)]+)?)\)")
58
90
 
59
91
 
60
92
  @dataclass
@@ -66,7 +98,9 @@ class Candidate:
66
98
  kind: str
67
99
  load: str
68
100
  priority: str
101
+ confidence: str
69
102
  reasons: list[str]
103
+ action: str
70
104
 
71
105
 
72
106
  def find_trellis_root(start: Path) -> Path | None:
@@ -183,8 +217,45 @@ def as_list(value: Any) -> list[str]:
183
217
  return [text] if text else []
184
218
 
185
219
 
220
+ def add_token(tokens: list[str], seen: set[str], token: str) -> None:
221
+ """按原始顺序追加去重 token。
222
+
223
+ Args:
224
+ tokens: 正在构造的 token 列表。
225
+ seen: 已追加 token 集合。
226
+ token: 待追加 token。
227
+
228
+ Returns:
229
+ None。
230
+ """
231
+ if len(token) < 2 or token in seen:
232
+ return
233
+ seen.add(token)
234
+ tokens.append(token)
235
+
236
+
237
+ def add_cjk_tokens(tokens: list[str], seen: set[str], text: str) -> None:
238
+ """为连续中文文本追加有限 n-gram token。
239
+
240
+ 中文没有空格分词。这里生成 2 到 6 字的 n-gram,用 token 集合匹配替代旧
241
+ 版任意子串匹配,同时保留 `发版` 命中 `发版流程` 这类常见能力。
242
+
243
+ Args:
244
+ tokens: 正在构造的 token 列表。
245
+ seen: 已追加 token 集合。
246
+ text: 连续中文文本。
247
+
248
+ Returns:
249
+ None。
250
+ """
251
+ max_size = min(6, len(text))
252
+ for size in range(2, max_size + 1):
253
+ for start in range(0, len(text) - size + 1):
254
+ add_token(tokens, seen, text[start : start + size])
255
+
256
+
186
257
  def normalize_tokens(text: str) -> list[str]:
187
- """提取查询 token,用于确定性的轻量匹配。
258
+ """提取查询或文档 token,用于确定性的轻量匹配。
188
259
 
189
260
  Args:
190
261
  text: 查询或可搜索文本。
@@ -195,14 +266,129 @@ def normalize_tokens(text: str) -> list[str]:
195
266
  seen: set[str] = set()
196
267
  tokens: list[str] = []
197
268
  for match in TOKEN_RE.finditer(text.lower()):
198
- token = match.group(0).strip("._-/")
199
- if len(token) < 2 or token in seen:
200
- continue
201
- seen.add(token)
202
- tokens.append(token)
269
+ token = match.group(0)
270
+ if CJK_RE.match(token):
271
+ add_cjk_tokens(tokens, seen, token)
272
+ else:
273
+ add_token(tokens, seen, token)
203
274
  return tokens
204
275
 
205
276
 
277
+ def significant_hits(query_tokens: list[str], target_tokens: list[str]) -> list[str]:
278
+ """计算非弱词 token 命中,保留查询 token 顺序。
279
+
280
+ Args:
281
+ query_tokens: 查询 token。
282
+ target_tokens: 待匹配文本 token。
283
+
284
+ Returns:
285
+ 非弱词命中列表。
286
+ """
287
+ target_set = set(target_tokens)
288
+ return [
289
+ token
290
+ for token in query_tokens
291
+ if token not in WEAK_TOKENS and token in target_set
292
+ ]
293
+
294
+
295
+ def collect_index_descriptions(spec_dir: Path) -> dict[str, list[str]]:
296
+ """从 `index.md` 链接行收集目标文档的路由描述。
297
+
298
+ Args:
299
+ spec_dir: `.trellis/spec` 目录。
300
+
301
+ Returns:
302
+ 以 spec 相对路径为 key 的描述文本列表。
303
+ """
304
+ descriptions: dict[str, list[str]] = {}
305
+ spec_root = spec_dir.resolve()
306
+ for index_path in iter_spec_files(spec_dir):
307
+ if index_path.name != "index.md":
308
+ continue
309
+
310
+ text = read_markdown(index_path)
311
+ if text is None:
312
+ continue
313
+ _, body = parse_frontmatter(text)
314
+
315
+ for line in body.splitlines():
316
+ matches = list(MARKDOWN_LINK_RE.finditer(line))
317
+ if not matches:
318
+ continue
319
+
320
+ clean_line = MARKDOWN_LINK_RE.sub(lambda item: item.group(1), line).strip()
321
+ for match in matches:
322
+ link_target = match.group(2).split("#", 1)[0].strip()
323
+ if "://" in link_target or link_target.startswith("#"):
324
+ continue
325
+
326
+ target_path = (index_path.parent / link_target).resolve()
327
+ try:
328
+ target_rel_path = target_path.relative_to(spec_root).as_posix()
329
+ except ValueError:
330
+ continue
331
+
332
+ if not target_path.is_file() or target_path == index_path.resolve():
333
+ continue
334
+
335
+ description = f"{match.group(1)} {clean_line}".strip()
336
+ if description:
337
+ descriptions.setdefault(target_rel_path, []).append(description)
338
+ return descriptions
339
+
340
+
341
+ def classify_confidence(
342
+ matched_triggers: list[str],
343
+ path_hits: list[str],
344
+ header_hits: list[str],
345
+ index_hits: list[str],
346
+ body_hits: list[str],
347
+ ) -> str | None:
348
+ """根据强锚点和弱证据判断候选置信度。
349
+
350
+ Args:
351
+ matched_triggers: frontmatter trigger 命中。
352
+ path_hits: 路径命中。
353
+ header_hits: 标题命中。
354
+ index_hits: index 描述命中。
355
+ body_hits: 正文样本命中。
356
+
357
+ Returns:
358
+ `high` / `medium`;证据不足时返回 None。
359
+ """
360
+ if matched_triggers:
361
+ return "high"
362
+
363
+ anchor_groups = [path_hits, header_hits, index_hits]
364
+ anchor_count = sum(1 for group in anchor_groups if group)
365
+ if len(path_hits) >= 2 or len(header_hits) >= 2 or len(index_hits) >= 2:
366
+ return "high"
367
+ if anchor_count >= 2:
368
+ return "high"
369
+ if anchor_count == 1 and len(body_hits) >= MIN_ANCHORED_BODY_HITS:
370
+ return "high"
371
+ if anchor_count == 1:
372
+ return "medium"
373
+ if len(body_hits) >= MIN_BODY_ONLY_HITS:
374
+ return "medium"
375
+ return None
376
+
377
+
378
+ def action_for_confidence(confidence: str) -> str:
379
+ """按置信度返回读取建议。
380
+
381
+ Args:
382
+ confidence: 候选置信度。
383
+
384
+ Returns:
385
+ 面向 AI 的行动建议。
386
+ """
387
+ if confidence == "high":
388
+ return "read before acting"
389
+ return "read if clearly relevant"
390
+
391
+
206
392
  def read_markdown(path: Path) -> str | None:
207
393
  """以容错 UTF-8 方式读取 Markdown 文件。
208
394
 
@@ -239,7 +425,13 @@ def iter_spec_files(spec_dir: Path) -> list[Path]:
239
425
  return sorted(result)
240
426
 
241
427
 
242
- def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> Candidate | None:
428
+ def score_file(
429
+ root: Path,
430
+ path: Path,
431
+ query: str,
432
+ query_tokens: list[str],
433
+ index_descriptions: dict[str, list[str]],
434
+ ) -> Candidate | None:
243
435
  """按查询为一个 Markdown spec 文件打分。
244
436
 
245
437
  Args:
@@ -247,6 +439,7 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
247
439
  path: Markdown 文件路径。
248
440
  query: 原始查询文本。
249
441
  query_tokens: 标准化后的查询 token。
442
+ index_descriptions: 从 index.md 收集的目标文档描述。
250
443
 
251
444
  Returns:
252
445
  分数达到阈值时返回候选项,否则返回 None。
@@ -258,11 +451,13 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
258
451
  metadata, body = parse_frontmatter(text)
259
452
  rel_path = path.relative_to(root).as_posix()
260
453
  spec_rel_path = path.relative_to(root / ".trellis" / "spec").as_posix()
261
- spec_rel_lower = spec_rel_path.lower()
454
+ spec_rel_tokens = normalize_tokens(spec_rel_path)
262
455
  body_sample = body[:MAX_BODY_CHARS]
263
- body_lower = body_sample.lower()
456
+ body_tokens = normalize_tokens(body_sample)
264
457
  headers = HEADER_RE.findall(body)
265
- header_text = " ".join(headers).lower()
458
+ header_tokens = normalize_tokens(" ".join(headers))
459
+ index_text = " ".join(index_descriptions.get(spec_rel_path, []))
460
+ index_tokens = normalize_tokens(index_text)
266
461
 
267
462
  kind = str(metadata.get("kind") or "").strip()
268
463
  load = str(metadata.get("load") or "").strip()
@@ -282,31 +477,35 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
282
477
  score += 8 * len(matched_triggers)
283
478
  reasons.append(f"matched triggers: {', '.join(matched_triggers[:5])}")
284
479
 
285
- path_hits = [token for token in query_tokens if token in spec_rel_lower]
480
+ path_hits = significant_hits(query_tokens, spec_rel_tokens)
286
481
  if path_hits:
287
- score += 4 * len(path_hits)
482
+ score += 5 * len(path_hits)
288
483
  reasons.append(f"matched path tokens: {', '.join(path_hits[:5])}")
289
484
 
290
- header_hits = [token for token in query_tokens if token in header_text]
485
+ header_hits = significant_hits(query_tokens, header_tokens)
291
486
  if header_hits:
292
487
  score += 3 * len(header_hits)
293
488
  reasons.append(f"matched headings: {', '.join(header_hits[:5])}")
294
489
 
295
- raw_body_hits = [token for token in query_tokens if token in body_lower]
296
- body_hits = [token for token in raw_body_hits if token not in BODY_WEAK_TOKENS]
490
+ index_hits = significant_hits(query_tokens, index_tokens)
491
+ if index_hits:
492
+ score += 4 * len(index_hits)
493
+ reasons.append(f"matched index descriptions: {', '.join(index_hits[:5])}")
494
+
495
+ body_hits = significant_hits(query_tokens, body_tokens)
297
496
  if body_hits:
298
497
  score += len(body_hits)
299
498
  reasons.append(f"matched body tokens: {', '.join(body_hits[:5])}")
300
499
 
301
- # 避免 `json` / `output` / `spec` 这类泛词把只有正文弱命中的文件全部拉进上下文。
302
- strong_match = (
303
- bool(matched_triggers)
304
- or bool(path_hits)
305
- or len(header_hits) >= 2
306
- or (bool(header_hits) and len(body_hits) >= MIN_HEADING_BODY_HITS)
307
- or len(body_hits) >= MIN_BODY_ONLY_HITS
500
+ # 避免 `to` / `flow` / `commit` 这类泛词或少量正文词把无关文件拉进上下文。
501
+ confidence = classify_confidence(
502
+ matched_triggers,
503
+ path_hits,
504
+ header_hits,
505
+ index_hits,
506
+ body_hits,
308
507
  )
309
- if not strong_match:
508
+ if confidence is None:
310
509
  return None
311
510
 
312
511
  if kind.lower() in {"sop", "procedure", "guide", "thinking-guide"} and score > 0:
@@ -325,7 +524,9 @@ def score_file(root: Path, path: Path, query: str, query_tokens: list[str]) -> C
325
524
  kind=kind or ("thinking-guide" if "/guides/" in f"/{rel_path}" else "spec"),
326
525
  load=load,
327
526
  priority=priority,
527
+ confidence=confidence,
328
528
  reasons=reasons,
529
+ action=action_for_confidence(confidence),
329
530
  )
330
531
 
331
532
 
@@ -345,12 +546,20 @@ def find_candidates(root: Path, query: str, limit: int) -> list[Candidate]:
345
546
  return []
346
547
 
347
548
  candidates: list[Candidate] = []
348
- for path in iter_spec_files(root / ".trellis" / "spec"):
349
- candidate = score_file(root, path, query, query_tokens)
549
+ spec_dir = root / ".trellis" / "spec"
550
+ index_descriptions = collect_index_descriptions(spec_dir)
551
+ for path in iter_spec_files(spec_dir):
552
+ candidate = score_file(root, path, query, query_tokens, index_descriptions)
350
553
  if candidate:
351
554
  candidates.append(candidate)
352
555
 
353
- candidates.sort(key=lambda item: (-item.score, item.path))
556
+ candidates.sort(
557
+ key=lambda item: (
558
+ 0 if item.confidence == "high" else 1,
559
+ -item.score,
560
+ item.path,
561
+ )
562
+ )
354
563
  return candidates[:limit]
355
564
 
356
565
 
@@ -372,13 +581,14 @@ def format_markdown(candidates: list[Candidate]) -> str:
372
581
  lines.append(f"- {candidate.path}")
373
582
  lines.append(f" kind: {candidate.kind}")
374
583
  lines.append(f" score: {candidate.score}")
584
+ lines.append(f" confidence: {candidate.confidence}")
375
585
  if candidate.load:
376
586
  lines.append(f" load: {candidate.load}")
377
587
  if candidate.priority:
378
588
  lines.append(f" priority: {candidate.priority}")
379
589
  if candidate.reasons:
380
590
  lines.append(f" reason: {'; '.join(candidate.reasons)}")
381
- lines.append(" action: read before acting")
591
+ lines.append(f" action: {candidate.action}")
382
592
  return "\n".join(lines)
383
593
 
384
594
 
@@ -396,10 +606,11 @@ def format_json(candidates: list[Candidate]) -> str:
396
606
  "file": candidate.path,
397
607
  "kind": candidate.kind,
398
608
  "score": candidate.score,
609
+ "confidence": candidate.confidence,
399
610
  "load": candidate.load,
400
611
  "priority": candidate.priority,
401
612
  "reason": candidate.reasons,
402
- "action": "read before acting",
613
+ "action": candidate.action,
403
614
  }
404
615
  for candidate in candidates
405
616
  ]
@@ -1,14 +1,13 @@
1
1
  {
2
- "syncedAt": "2026-07-01T04:27:03.524Z",
2
+ "syncedAt": "2026-07-02T11:06:42.782Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "7a9b882db76500b49b38cf423be682fb06effe1a",
4
+ "sourceCommit": "a70185e3b372058363296e81e2138f4f239d6e3b",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "craft-rpa",
8
8
  "craft-slides",
9
9
  "humanize-writing",
10
10
  "open-idea",
11
- "sub2api-account-json-fix",
12
11
  "torrent-analyze"
13
12
  ],
14
13
  "claudeSkills": [
@@ -16,7 +15,6 @@
16
15
  "craft-slides",
17
16
  "humanize-writing",
18
17
  "open-idea",
19
- "sub2api-account-json-fix",
20
18
  "torrent-analyze"
21
19
  ]
22
20
  },
@@ -140,6 +138,7 @@
140
138
  ],
141
139
  "scripts": [
142
140
  "auto_loop.py",
141
+ "push_snapshot.py",
143
142
  "spec_router.py"
144
143
  ]
145
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.0-beta.4",
3
+ "version": "0.4.0",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,8 @@ export function copyScriptAssets(target, variantDir, skills = []) {
24
24
  let aliases = [];
25
25
  if (name === "auto_loop") {
26
26
  aliases = ["auto-loop", "auto-loop-runner", "trellis-auto-loop"];
27
+ } else if (name === "push_snapshot") {
28
+ aliases = ["push-snapshot", "trellis-push", "push", "snapshot"];
27
29
  } else if (name === "spec_router") {
28
30
  aliases = [
29
31
  "spec-router",
@@ -40,7 +40,6 @@ const SKILL_DESCRIPTION_OVERRIDES = {
40
40
  "plan-version": "规划版本任务、波次和分工",
41
41
  push: "提交、推送并同步任务进度",
42
42
  "re-implement": "需求变更后重新实现",
43
- "sub2api-account-json-fix": "批量补全并推送 sub2api 账号 JSON",
44
43
  "sync-prd": "根据代码或需求变化同步 PRD",
45
44
  "torrent-analyze": "解析磁链或种子 hash 并整理信息",
46
45
  "trellis-analyze-task": "深度分析并细化任务",
@@ -1,47 +0,0 @@
1
- ---
2
- name: sub2api-account-json-fix
3
- description: 修正当前项目里的 sub2api 账号导出 JSON,并把这些账号直接推送到 sub2api admin 接口。用于批量推送 `sub2api-account-*.json` 到远端 sub2api,尤其适合先同步模板里的 `model_mapping`、`allow_overages`,再按指定 group_ids 创建账号。
4
- argument-hint: <template-json> [target-json...]
5
- disable-model-invocation: true
6
- ---
7
-
8
- # Sub2API 账号推送
9
-
10
- 先确认模板账号,再运行项目内的推送脚本。
11
-
12
- ## 规则
13
-
14
- - 先用模板账号补齐目标账号的 `credentials.model_mapping`。
15
- - 确保目标账号包含 `extra.allow_overages`。
16
- - 把 `name` 改成 `credentials.email`。
17
- - 然后把代理推送到 sub2api,再逐个创建账号。
18
- - 创建账号后默认立刻关闭调度。
19
- - 如果账号带有 `refresh_token`,创建后默认立刻调用一次 refresh,确保远端拿到最新 access token。
20
- - 如果 `.claude/skills/sub2api-account-json-fix/env/push.env` 不存在,继续检查系统环境变量。
21
- - 如果地址或 token 仍未配置,或者 token 还是 `admin-xxxx` 这种示例值,先向用户索要真实值,不要直接推送。
22
-
23
- ## 技能目录配置
24
-
25
- 优先读取:
26
-
27
- - `.claude/skills/sub2api-account-json-fix/env/push.env`
28
-
29
- 支持这些键:
30
-
31
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL`
32
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN`
33
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS`
34
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE`
35
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE`
36
-
37
- ## 执行方式
38
-
39
- 如果用户给了参数,按“第一个参数是模板文件,后续参数是目标文件”的规则执行;否则先向用户确认模板文件。
40
-
41
- 运行:
42
-
43
- ```bash
44
- bash .claude/skills/sub2api-account-json-fix/scripts/run.sh $ARGUMENTS
45
- ```
46
-
47
- 如果用户没有显式要求写回本地文件,就不要自动加 `--write`;先预览或直接推送内存中的规范化结果即可。
@@ -1,8 +0,0 @@
1
- # Claude Code 项目技能的本地配置示例。
2
- # 把本文件复制为同目录下的 push.env 后再填写真实值。
3
-
4
- SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL=http://127.0.0.1:8080/api/v1/admin
5
- SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN=admin-xxxx
6
- SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS=3,7
7
- SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE=false
8
- SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE=true
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
- CLAUDE_SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6
- SKILL_GARDEN_ROOT="$(cd "$CLAUDE_SKILL_DIR/../../.." && pwd -P)"
7
- MAIN_SCRIPT="$SKILL_GARDEN_ROOT/.codex/skills/sub2api-account-json-fix/scripts/fix_exported_account_json.py"
8
- LOCAL_ENV_FILE="$CLAUDE_SKILL_DIR/env/push.env"
9
-
10
- export SUB2API_ACCOUNT_JSON_FIX_ENV_FILE="$LOCAL_ENV_FILE"
11
-
12
- if [[ -f "$LOCAL_ENV_FILE" ]]; then
13
- set -a
14
- # shellcheck disable=SC1090
15
- source "$LOCAL_ENV_FILE"
16
- set +a
17
- fi
18
-
19
- if [[ $# -lt 1 ]]; then
20
- echo "用法: run.sh <template-json> [target-json...]" >&2
21
- exit 2
22
- fi
23
-
24
- TEMPLATE_FILE="$1"
25
- shift
26
-
27
- python "$MAIN_SCRIPT" --template "$TEMPLATE_FILE" "$@" --push