claudeos-core 2.1.0 → 2.2.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 +456 -0
- package/README.de.md +33 -39
- package/README.es.md +33 -39
- package/README.fr.md +33 -39
- package/README.hi.md +33 -39
- package/README.ja.md +33 -39
- package/README.ko.md +37 -43
- package/README.md +37 -43
- package/README.ru.md +35 -39
- package/README.vi.md +33 -39
- package/README.zh-CN.md +33 -39
- package/bin/commands/init.js +81 -45
- package/content-validator/index.js +6 -1
- package/lib/env-parser.js +317 -0
- package/lib/memory-scaffold.js +7 -5
- package/package.json +1 -1
- package/pass-prompts/templates/angular/pass3.md +28 -13
- package/pass-prompts/templates/common/claude-md-scaffold.md +644 -0
- package/pass-prompts/templates/common/pass3-footer.md +185 -0
- package/pass-prompts/templates/common/pass4.md +6 -3
- package/pass-prompts/templates/common/staging-override.md +1 -1
- package/pass-prompts/templates/java-spring/pass3.md +31 -21
- package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
- package/pass-prompts/templates/node-express/pass3.md +30 -21
- package/pass-prompts/templates/node-fastify/pass3.md +28 -14
- package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
- package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
- package/pass-prompts/templates/node-vite/pass3.md +30 -13
- package/pass-prompts/templates/python-django/pass3.md +32 -21
- package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
- package/pass-prompts/templates/python-flask/pass3.md +31 -13
- package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
- package/plan-installer/index.js +8 -0
- package/plan-installer/prompt-generator.js +18 -1
- package/plan-installer/stack-detector.js +16 -0
package/bin/commands/init.js
CHANGED
|
@@ -495,14 +495,14 @@ async function runPass3Split(ctx) {
|
|
|
495
495
|
|
|
496
496
|
// ═══ Stage 3b: CLAUDE.md + standard/ + .claude/rules/ ═══════════
|
|
497
497
|
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
498
|
+
// Single batch (domains ≤ 15): keep legacy "3b" marker (backward-compatible).
|
|
499
|
+
// Multi-batch (domains > 15): run "3b-core" first, then "3b-1", "3b-2", ...
|
|
500
500
|
//
|
|
501
|
-
// 3b-core
|
|
502
|
-
// +
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
501
|
+
// Rationale for splitting 3b-core: if the first multi-batch stage handled
|
|
502
|
+
// "CLAUDE.md + common standards + 15 domains" in a single session, that
|
|
503
|
+
// single stage would hit ~70-80 files — close to 2x the observed overflow
|
|
504
|
+
// threshold (~40 files). Splitting the common files into their own stage
|
|
505
|
+
// keeps every stage under ~50 files.
|
|
506
506
|
if (isBatched) {
|
|
507
507
|
await runStage("3b-core", "core common files (CLAUDE.md + common standard + common rules)",
|
|
508
508
|
buildStageCorePrompt("3b", batches),
|
|
@@ -522,16 +522,16 @@ async function runPass3Split(ctx) {
|
|
|
522
522
|
problems.push("claudeos-core/standard/00.core/01.project-overview.md is empty");
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
|
-
// 3b-core
|
|
525
|
+
// 3b-core generates only common rules — rules-count validation happens in 3b-N.
|
|
526
526
|
return problems;
|
|
527
527
|
},
|
|
528
528
|
}
|
|
529
529
|
);
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
532
|
+
// Per-domain batch loop.
|
|
533
|
+
// Single batch: stageId "3b", common files included (legacy behavior).
|
|
534
|
+
// Multi-batch: stageId "3b-1", "3b-2", ..., common files already handled in 3b-core.
|
|
535
535
|
for (let bi = 0; bi < batches.length; bi++) {
|
|
536
536
|
const batchDomains = batches[bi];
|
|
537
537
|
const stageId = isBatched ? `3b-${bi + 1}` : "3b";
|
|
@@ -539,8 +539,8 @@ async function runPass3Split(ctx) {
|
|
|
539
539
|
? `domain batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
|
|
540
540
|
: "core files (CLAUDE.md + standard + rules)";
|
|
541
541
|
|
|
542
|
-
//
|
|
543
|
-
//
|
|
542
|
+
// Per-batch prompt: inject into the original 3b header the list of domains scoped to this batch.
|
|
543
|
+
// In multi-batch mode every batch generates "domain-specific files only" (common files handled in 3b-core).
|
|
544
544
|
const batchScopeNote = isBatched
|
|
545
545
|
? buildBatchScopeNote("3b", bi, batches.length, batchDomains)
|
|
546
546
|
: "";
|
|
@@ -553,7 +553,7 @@ async function runPass3Split(ctx) {
|
|
|
553
553
|
expectsStagedRules: true,
|
|
554
554
|
validate: () => {
|
|
555
555
|
const problems = [];
|
|
556
|
-
//
|
|
556
|
+
// Single batch: validate common files here (no 3b-core exists).
|
|
557
557
|
if (!isBatched) {
|
|
558
558
|
if (!fileExists(claudeMdPath)) {
|
|
559
559
|
problems.push("CLAUDE.md was not created");
|
|
@@ -568,7 +568,7 @@ async function runPass3Split(ctx) {
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
|
-
//
|
|
571
|
+
// For every batch, confirm rules/ was generated (at least one staged-rules move must succeed).
|
|
572
572
|
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
573
573
|
const rulesCount = countFilesRecursive(rulesDir);
|
|
574
574
|
if (rulesCount === 0) {
|
|
@@ -581,11 +581,11 @@ async function runPass3Split(ctx) {
|
|
|
581
581
|
|
|
582
582
|
// ═══ Stage 3c: skills/ + guide/ ═══════════════════════════════
|
|
583
583
|
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
584
|
+
// Single batch (domains ≤ 15): keep legacy "3c" marker (guide + skills together).
|
|
585
|
+
// Multi-batch (domains > 15): run "3c-core" first, then "3c-1", "3c-2", ...
|
|
586
586
|
//
|
|
587
|
-
// 3c-core
|
|
588
|
-
//
|
|
587
|
+
// Rationale for splitting 3c-core: the 9 guide files + common skills are fixed regardless of domain count.
|
|
588
|
+
// Mixing them into domain batches makes the first batch heavier than the others.
|
|
589
589
|
if (isBatched) {
|
|
590
590
|
await runStage("3c-core", "common guides and shared skills",
|
|
591
591
|
buildStageCorePrompt("3c", batches),
|
|
@@ -604,16 +604,16 @@ async function runPass3Split(ctx) {
|
|
|
604
604
|
for (const g of missingGuides) {
|
|
605
605
|
problems.push(`claudeos-core/guide/${g} missing or empty`);
|
|
606
606
|
}
|
|
607
|
-
// skills/
|
|
607
|
+
// Final skills/ validation is performed in the last domain batch.
|
|
608
608
|
return problems;
|
|
609
609
|
},
|
|
610
610
|
}
|
|
611
611
|
);
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
614
|
+
// Per-domain skills batch loop.
|
|
615
|
+
// Single batch: guide + skills together (legacy behavior).
|
|
616
|
+
// Multi-batch: skills only; guide was already handled in 3c-core.
|
|
617
617
|
for (let bi = 0; bi < batches.length; bi++) {
|
|
618
618
|
const batchDomains = batches[bi];
|
|
619
619
|
const stageId = isBatched ? `3c-${bi + 1}` : "3c";
|
|
@@ -633,7 +633,7 @@ async function runPass3Split(ctx) {
|
|
|
633
633
|
expectsStagedRules: true, // skills occasionally include rule files
|
|
634
634
|
validate: () => {
|
|
635
635
|
const problems = [];
|
|
636
|
-
//
|
|
636
|
+
// Single batch: validate guide here as well (no 3c-core).
|
|
637
637
|
if (!isBatched) {
|
|
638
638
|
const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
639
639
|
const missingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
@@ -647,7 +647,7 @@ async function runPass3Split(ctx) {
|
|
|
647
647
|
problems.push(`claudeos-core/guide/${g} missing or empty`);
|
|
648
648
|
}
|
|
649
649
|
}
|
|
650
|
-
//
|
|
650
|
+
// Final skills validation: full check only in the single-batch case or the last multi-batch.
|
|
651
651
|
if (!isBatched || bi === batches.length - 1) {
|
|
652
652
|
const { hasNonEmptyMdRecursive } = require("../../lib/expected-outputs");
|
|
653
653
|
const skillsDir = path.join(PROJECT_ROOT, "claudeos-core/skills");
|
|
@@ -662,16 +662,16 @@ async function runPass3Split(ctx) {
|
|
|
662
662
|
|
|
663
663
|
// ═══ Stage 3d: plan/ + database/ + mcp-guide/ ═════════════════
|
|
664
664
|
//
|
|
665
|
-
// 3d
|
|
666
|
-
// database/mcp-guide
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// master plan
|
|
671
|
-
//
|
|
665
|
+
// 3d used to aggregate standard/rules/skills/guide into a master plan
|
|
666
|
+
// and generate database/mcp-guide stubs. But the master plan itself was
|
|
667
|
+
// an internal backup/management file never loaded by Claude Code at runtime,
|
|
668
|
+
// and at high domain counts the single-session aggregation caused "Prompt is
|
|
669
|
+
// too long" failures (at 18 domains, 3d-standard failed at the 32-file mark).
|
|
670
|
+
// Dropping master plan generation is the correct call for stability, and
|
|
671
|
+
// users can aggregate manually with their own script when needed.
|
|
672
672
|
//
|
|
673
|
-
//
|
|
674
|
-
// 3d-aux → database/ + mcp-guide/ (
|
|
673
|
+
// As a result, 3d only keeps the aux stage:
|
|
674
|
+
// 3d-aux → database/ + mcp-guide/ (project-specific stub descriptions)
|
|
675
675
|
|
|
676
676
|
// 3d-aux: database/ + mcp-guide/ (absence is warning-level)
|
|
677
677
|
await runStage("3d-aux", "aux docs (database + mcp-guide)",
|
|
@@ -690,10 +690,10 @@ async function runPass3Split(ctx) {
|
|
|
690
690
|
` Check disk space / permissions on ${GENERATED_DIR}/.`
|
|
691
691
|
);
|
|
692
692
|
}
|
|
693
|
-
//
|
|
694
|
-
// 3a (1) + 3b
|
|
695
|
-
//
|
|
696
|
-
//
|
|
693
|
+
// Total stage count
|
|
694
|
+
// 3a (1) + 3b (1 single or core+N) + 3c (1 single or core+N) + 3d-aux (1)
|
|
695
|
+
// Single batch: 1 + 1 + 1 + 1 = 4
|
|
696
|
+
// Multi-batch: 1 + (1 + N) + (1 + N) + 1 = 2N + 4
|
|
697
697
|
const three3dStages = 1; // aux only (master plan aggregation removed)
|
|
698
698
|
const totalStages = isBatched
|
|
699
699
|
? (1 + 1 + batches.length + 1 + batches.length + three3dStages)
|
|
@@ -791,6 +791,42 @@ async function cmdInit(parsedArgs) {
|
|
|
791
791
|
wasFreshClean = true;
|
|
792
792
|
log(" 🔄 Previous results deleted (--force)\n");
|
|
793
793
|
} else {
|
|
794
|
+
// v2.2.0 upgrade detection: if project was generated with older claudeos-core
|
|
795
|
+
// (pre-2.2.0), default "resume" mode will skip regeneration of existing files
|
|
796
|
+
// per Rule B idempotency, meaning v2.2.0 structural improvements will NOT be
|
|
797
|
+
// picked up. Detect this case by checking CLAUDE.md for v2.2.0 markers.
|
|
798
|
+
const claudeMd = path.join(PROJECT_ROOT, "CLAUDE.md");
|
|
799
|
+
if (fileExists(claudeMd)) {
|
|
800
|
+
try {
|
|
801
|
+
const content = fs.readFileSync(claudeMd, "utf-8");
|
|
802
|
+
// v2.2.0 scaffold enforces EXACTLY 8 top-level `##` sections.
|
|
803
|
+
// Pre-v2.2.0 CLAUDE.md files typically carry 9+ sections (extra
|
|
804
|
+
// "Rules Summary" / "Common Rules" / "Required to Observe"
|
|
805
|
+
// blocks that v2.2.0 forbids). Counting `^## ` headings is a
|
|
806
|
+
// language-independent heuristic that works across all 10
|
|
807
|
+
// supported output languages. False positive (an existing
|
|
808
|
+
// 8-section pre-v2.2.0 CLAUDE.md) is acceptable — the user
|
|
809
|
+
// simply won't see the upgrade warning and can still run
|
|
810
|
+
// `--force` manually.
|
|
811
|
+
const sectionCount = (content.match(/^## /gm) || []).length;
|
|
812
|
+
const hasV220Section8 = sectionCount === 8;
|
|
813
|
+
if (!hasV220Section8) {
|
|
814
|
+
log("\n ⚠️ v2.2.0 upgrade detected");
|
|
815
|
+
log(" ─────────────────────────");
|
|
816
|
+
log(" Your existing CLAUDE.md was generated with an older claudeos-core version.");
|
|
817
|
+
log(" v2.2.0 introduces structural changes that the default 'resume' mode");
|
|
818
|
+
log(" CANNOT apply because existing files are preserved under Rule B (idempotency).");
|
|
819
|
+
log("");
|
|
820
|
+
log(" To fully adopt v2.2.0, choose one of:");
|
|
821
|
+
log(" 1. Rerun with --force: npx claudeos-core init --force");
|
|
822
|
+
log(" (overwrites generated files; your memory/ content is preserved)");
|
|
823
|
+
log(" 2. Choose 'fresh' below (equivalent to --force)");
|
|
824
|
+
log("");
|
|
825
|
+
log(" See CHANGELOG.md Migration section for full details.\n");
|
|
826
|
+
}
|
|
827
|
+
} catch (_) { /* Read error is non-fatal; proceed to resume prompt */ }
|
|
828
|
+
}
|
|
829
|
+
|
|
794
830
|
const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
|
|
795
831
|
const mode = await selectResumeMode(lang, status);
|
|
796
832
|
if (!mode) throw new InitError("Cancelled.");
|
|
@@ -1143,12 +1179,12 @@ async function cmdInit(parsedArgs) {
|
|
|
1143
1179
|
}
|
|
1144
1180
|
}
|
|
1145
1181
|
if (fileExists(pass3Marker)) {
|
|
1146
|
-
// v2.1.1: split-mode partial
|
|
1147
|
-
// { mode: "split", groupsCompleted: [...], completedAt: undefined }
|
|
1148
|
-
//
|
|
1149
|
-
// guide/skills/plan
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1182
|
+
// v2.1.1: protect split-mode partial markers.
|
|
1183
|
+
// An in-progress marker of the form { mode: "split", groupsCompleted: [...], completedAt: undefined }
|
|
1184
|
+
// must NOT be treated as stale. When the pipeline has normally completed
|
|
1185
|
+
// only through 3b, guide/skills/plan are empty, so the legacy check
|
|
1186
|
+
// falsely flagged the marker as stale. Deleting it made runPass3Split
|
|
1187
|
+
// lose track of completed stages and re-run from 3a (correct but wastes ~2x tokens).
|
|
1152
1188
|
let markerIsSplitPartial = false;
|
|
1153
1189
|
try {
|
|
1154
1190
|
const parsed = JSON.parse(readFile(pass3Marker));
|
|
@@ -1158,7 +1194,7 @@ async function cmdInit(parsedArgs) {
|
|
|
1158
1194
|
Array.isArray(parsed.groupsCompleted) &&
|
|
1159
1195
|
!parsed.completedAt;
|
|
1160
1196
|
} catch (_e) {
|
|
1161
|
-
// malformed marker → stale check
|
|
1197
|
+
// malformed marker → fall through to the stale check (legacy behavior)
|
|
1162
1198
|
}
|
|
1163
1199
|
|
|
1164
1200
|
if (markerIsSplitPartial) {
|
|
@@ -241,6 +241,11 @@ async function main() {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
// ─── 6. claudeos-core/plan/** ──────────────────────────
|
|
244
|
+
// v2.1.0+ removed master plan generation; plan/ is optional and is not created
|
|
245
|
+
// during fresh init. If the directory exists (legacy projects, user-authored
|
|
246
|
+
// plan files), we still validate its contents. If it is absent, that is the
|
|
247
|
+
// expected state post-v2.1.0 — do not push a MISSING error (parallel to
|
|
248
|
+
// plan-validator / manifest-generator which were already updated in v2.1.0).
|
|
244
249
|
console.log(" [6/9] claudeos-core/plan/...");
|
|
245
250
|
if (fs.existsSync(PLAN_DIR)) {
|
|
246
251
|
const planFiles = await glob("*.md", { cwd: PLAN_DIR, absolute: true });
|
|
@@ -261,7 +266,7 @@ async function main() {
|
|
|
261
266
|
}
|
|
262
267
|
console.log(` ${planFiles.length} files checked`);
|
|
263
268
|
} else {
|
|
264
|
-
|
|
269
|
+
console.log(" ⏭️ plan/ not present (expected post-v2.1.0)");
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
// ─── 7. claudeos-core/database/** ──────────────────────
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env-parser.js — Parse .env* files for factual project configuration.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS:
|
|
5
|
+
* claudeos-core's "LLMs guess, code confirms" principle requires that
|
|
6
|
+
* factual project data (ports, hosts, API targets) be extracted from
|
|
7
|
+
* declarative sources the project itself maintains, not guessed from
|
|
8
|
+
* framework defaults. `.env.example` is such a source — it's the
|
|
9
|
+
* canonical declaration of a project's runtime configuration surface.
|
|
10
|
+
*
|
|
11
|
+
* Historically, stack-detector only parsed .env for DATABASE_URL to
|
|
12
|
+
* identify the DB. Everything else (ports, hosts, API endpoints) fell
|
|
13
|
+
* back to hardcoded framework defaults (e.g., Vite → 5173), which
|
|
14
|
+
* silently produced wrong values whenever a project customized its
|
|
15
|
+
* configuration via .env. This utility closes that gap.
|
|
16
|
+
*
|
|
17
|
+
* SEARCH ORDER:
|
|
18
|
+
* `.env.example` is preferred over actual `.env` files because it is
|
|
19
|
+
* the shape-of-truth committed to VCS: developer-neutral, reflecting
|
|
20
|
+
* the project's intended configuration surface, not one contributor's
|
|
21
|
+
* local overrides.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const { readFileSafe, existsSafe } = require("./safe-fs");
|
|
28
|
+
|
|
29
|
+
// Search order: public-facing → developer-specific → runtime-specific.
|
|
30
|
+
// .env.example is canonical because it's the committed, intended config.
|
|
31
|
+
const ENV_FILE_ORDER = [
|
|
32
|
+
".env.example",
|
|
33
|
+
".env.local.example",
|
|
34
|
+
".env.development.example",
|
|
35
|
+
".env.sample",
|
|
36
|
+
".env.template",
|
|
37
|
+
".env",
|
|
38
|
+
".env.local",
|
|
39
|
+
".env.development",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Port variable name conventions across frameworks.
|
|
43
|
+
// Ordered by specificity — more specific wins when multiple are present.
|
|
44
|
+
const PORT_VAR_KEYS = [
|
|
45
|
+
// Vite-specific common patterns
|
|
46
|
+
"VITE_PORT",
|
|
47
|
+
"VITE_DEV_PORT",
|
|
48
|
+
"VITE_DEV_SERVER_PORT",
|
|
49
|
+
"VITE_DESKTOP_PORT",
|
|
50
|
+
// Next.js
|
|
51
|
+
"NEXT_PUBLIC_PORT",
|
|
52
|
+
"NEXT_PORT",
|
|
53
|
+
// Nuxt
|
|
54
|
+
"NUXT_PORT",
|
|
55
|
+
"NUXT_PUBLIC_PORT",
|
|
56
|
+
// Angular
|
|
57
|
+
"NG_PORT",
|
|
58
|
+
"NG_DEV_PORT",
|
|
59
|
+
// Node / backend frameworks
|
|
60
|
+
"APP_PORT",
|
|
61
|
+
"SERVER_PORT",
|
|
62
|
+
"HTTP_PORT",
|
|
63
|
+
"DEV_PORT",
|
|
64
|
+
// Python
|
|
65
|
+
"FLASK_RUN_PORT",
|
|
66
|
+
"UVICORN_PORT",
|
|
67
|
+
"DJANGO_PORT",
|
|
68
|
+
// Generic last — lowest priority because "PORT" collides with too many things
|
|
69
|
+
"PORT",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Host variable conventions.
|
|
73
|
+
const HOST_VAR_KEYS = [
|
|
74
|
+
"VITE_DEV_HOST",
|
|
75
|
+
"VITE_HOST",
|
|
76
|
+
"NEXT_PUBLIC_HOST",
|
|
77
|
+
"NUXT_HOST",
|
|
78
|
+
"APP_HOST",
|
|
79
|
+
"SERVER_HOST",
|
|
80
|
+
"HTTP_HOST",
|
|
81
|
+
"HOST",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// API target / backend proxy conventions.
|
|
85
|
+
const API_TARGET_VAR_KEYS = [
|
|
86
|
+
"VITE_API_TARGET",
|
|
87
|
+
"VITE_API_URL",
|
|
88
|
+
"VITE_API_BASE_URL",
|
|
89
|
+
"NEXT_PUBLIC_API_URL",
|
|
90
|
+
"NEXT_PUBLIC_API_BASE_URL",
|
|
91
|
+
"NUXT_PUBLIC_API_BASE",
|
|
92
|
+
"API_TARGET",
|
|
93
|
+
"API_URL",
|
|
94
|
+
"API_BASE_URL",
|
|
95
|
+
"BACKEND_URL",
|
|
96
|
+
"PROXY_TARGET",
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse .env-style file content into a flat key-value object.
|
|
101
|
+
* Handles: KEY=VALUE, quoted values, inline comments, blank lines, export prefix.
|
|
102
|
+
* Does NOT expand ${VAR} interpolation — we keep raw declared values.
|
|
103
|
+
*/
|
|
104
|
+
function parseEnvContent(content) {
|
|
105
|
+
if (!content || typeof content !== "string") return {};
|
|
106
|
+
const result = {};
|
|
107
|
+
const lines = content.split(/\r?\n/);
|
|
108
|
+
for (const rawLine of lines) {
|
|
109
|
+
let line = rawLine.trim();
|
|
110
|
+
if (!line) continue;
|
|
111
|
+
if (line.startsWith("#")) continue;
|
|
112
|
+
// Strip `export` prefix (common in shell-sourced env files)
|
|
113
|
+
if (line.startsWith("export ")) line = line.slice(7).trim();
|
|
114
|
+
const eq = line.indexOf("=");
|
|
115
|
+
if (eq === -1) continue;
|
|
116
|
+
const key = line.slice(0, eq).trim();
|
|
117
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
118
|
+
let value = line.slice(eq + 1).trim();
|
|
119
|
+
// Strip surrounding single or double quotes
|
|
120
|
+
if (
|
|
121
|
+
(value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
122
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)
|
|
123
|
+
) {
|
|
124
|
+
value = value.slice(1, -1);
|
|
125
|
+
} else {
|
|
126
|
+
// Strip inline comment (only on unquoted values)
|
|
127
|
+
const hashIdx = value.indexOf(" #");
|
|
128
|
+
if (hashIdx !== -1) value = value.slice(0, hashIdx).trim();
|
|
129
|
+
}
|
|
130
|
+
result[key] = value;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Locate the most authoritative env file in a project root.
|
|
137
|
+
* Returns the absolute path, or null if none found.
|
|
138
|
+
*/
|
|
139
|
+
function findPrimaryEnvFile(root) {
|
|
140
|
+
for (const name of ENV_FILE_ORDER) {
|
|
141
|
+
const p = path.join(root, name);
|
|
142
|
+
if (existsSafe(p)) return p;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read and parse the primary env file. Returns { file, vars } or null.
|
|
149
|
+
*/
|
|
150
|
+
function readPrimaryEnv(root) {
|
|
151
|
+
const file = findPrimaryEnvFile(root);
|
|
152
|
+
if (!file) return null;
|
|
153
|
+
const content = readFileSafe(file);
|
|
154
|
+
if (!content) return null;
|
|
155
|
+
return {
|
|
156
|
+
file: path.basename(file),
|
|
157
|
+
vars: parseEnvContent(content),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract a port value from parsed env vars. Returns integer or null.
|
|
163
|
+
* First match by PORT_VAR_KEYS ordering wins.
|
|
164
|
+
*/
|
|
165
|
+
function extractPort(vars) {
|
|
166
|
+
if (!vars) return null;
|
|
167
|
+
for (const key of PORT_VAR_KEYS) {
|
|
168
|
+
if (key in vars) {
|
|
169
|
+
const n = parseInt(vars[key], 10);
|
|
170
|
+
if (!Number.isNaN(n) && n > 0 && n < 65536) return n;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract a host value from parsed env vars. Returns string or null.
|
|
178
|
+
*/
|
|
179
|
+
function extractHost(vars) {
|
|
180
|
+
if (!vars) return null;
|
|
181
|
+
for (const key of HOST_VAR_KEYS) {
|
|
182
|
+
if (key in vars && vars[key]) return vars[key];
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract an API target URL from parsed env vars. Returns string or null.
|
|
189
|
+
*/
|
|
190
|
+
function extractApiTarget(vars) {
|
|
191
|
+
if (!vars) return null;
|
|
192
|
+
for (const key of API_TARGET_VAR_KEYS) {
|
|
193
|
+
if (key in vars && vars[key]) return vars[key];
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sensitive variable name patterns. env vars matching any of these patterns
|
|
200
|
+
* are redacted from the `vars` map returned to downstream consumers
|
|
201
|
+
* (stack-detector, prompt-generator, CLAUDE.md scaffold).
|
|
202
|
+
*
|
|
203
|
+
* Even though `.env.example` is conventionally a placeholder file committed
|
|
204
|
+
* to VCS (and should not contain real secrets), projects occasionally check
|
|
205
|
+
* in real values by mistake. claudeos-core piping those values into
|
|
206
|
+
* CLAUDE.md would amplify the leak — CLAUDE.md is committed, shared, and
|
|
207
|
+
* potentially published as part of open-source documentation.
|
|
208
|
+
*
|
|
209
|
+
* Redaction strategy:
|
|
210
|
+
* - Matching keys are kept in the map so consumers can still detect
|
|
211
|
+
* "this variable exists" (e.g., "project declares an API_KEY env var").
|
|
212
|
+
* - Values are replaced with the sentinel string "***REDACTED***".
|
|
213
|
+
* - extractPort / extractHost / extractApiTarget already scan only a
|
|
214
|
+
* whitelist of config-relevant keys (PORT, HOST, API_TARGET, etc.)
|
|
215
|
+
* so sensitive keys cannot leak through those paths regardless of
|
|
216
|
+
* this filter.
|
|
217
|
+
*
|
|
218
|
+
* Patterns are case-insensitive substring matches against the variable name.
|
|
219
|
+
*/
|
|
220
|
+
const SENSITIVE_VAR_PATTERNS = [
|
|
221
|
+
/password/i,
|
|
222
|
+
/passwd/i,
|
|
223
|
+
/secret/i,
|
|
224
|
+
/api[_-]?key/i,
|
|
225
|
+
/access[_-]?key/i,
|
|
226
|
+
/private[_-]?key/i,
|
|
227
|
+
/auth[_-]?token/i,
|
|
228
|
+
/token/i, // matches TOKEN, AUTH_TOKEN, GIT_TOKEN, NPM_TOKEN
|
|
229
|
+
// (underscore is a word character in regex \b,
|
|
230
|
+
// so \btoken\b fails to match "_TOKEN" suffix)
|
|
231
|
+
/credential/i,
|
|
232
|
+
/bearer/i,
|
|
233
|
+
/\bsalt\b/i,
|
|
234
|
+
/encryption[_-]?key/i,
|
|
235
|
+
/cert(ificate)?[_-]?key/i,
|
|
236
|
+
/secret[_-]?key/i,
|
|
237
|
+
/client[_-]?secret/i,
|
|
238
|
+
/session[_-]?secret/i,
|
|
239
|
+
/jwt[_-]?secret/i,
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns true if the given env var name matches any sensitive pattern.
|
|
244
|
+
*/
|
|
245
|
+
function isSensitiveVarName(name) {
|
|
246
|
+
if (!name || typeof name !== "string") return false;
|
|
247
|
+
return SENSITIVE_VAR_PATTERNS.some(re => re.test(name));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Redacts sensitive values in an env vars map. Returns a new object;
|
|
252
|
+
* original is not mutated. Preserves keys so "variable exists" signal
|
|
253
|
+
* is kept, but replaces values with a sentinel string.
|
|
254
|
+
*
|
|
255
|
+
* Whitelist exception: DATABASE_URL is kept as-is because stack-detector's
|
|
256
|
+
* db-identification path has always used it and existing project-analysis
|
|
257
|
+
* consumers depend on reading it. (The DB URL contains credentials, but
|
|
258
|
+
* this has been the established behavior since v1.x and changing it would
|
|
259
|
+
* be a breaking change. Downstream consumers that write CLAUDE.md content
|
|
260
|
+
* from vars should still redact it at their layer.)
|
|
261
|
+
*/
|
|
262
|
+
function redactSensitiveVars(vars) {
|
|
263
|
+
if (!vars || typeof vars !== "object") return vars;
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
266
|
+
if (k === "DATABASE_URL") {
|
|
267
|
+
out[k] = v; // documented whitelist for stack-detector back-compat
|
|
268
|
+
} else if (isSensitiveVarName(k)) {
|
|
269
|
+
out[k] = "***REDACTED***";
|
|
270
|
+
} else {
|
|
271
|
+
out[k] = v;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Top-level convenience: read the project's env file and produce the
|
|
279
|
+
* stack.envInfo object consumed by project-analysis.json.
|
|
280
|
+
*
|
|
281
|
+
* Returns null when no env file exists (caller falls back to framework defaults).
|
|
282
|
+
*
|
|
283
|
+
* Sensitive variable values (passwords, secrets, tokens, API keys) are
|
|
284
|
+
* redacted in `vars` via redactSensitiveVars before being returned.
|
|
285
|
+
* extractPort/Host/ApiTarget use a whitelist of config-relevant keys so
|
|
286
|
+
* they are unaffected by redaction.
|
|
287
|
+
*/
|
|
288
|
+
function readStackEnvInfo(root) {
|
|
289
|
+
const primary = readPrimaryEnv(root);
|
|
290
|
+
if (!primary) return null;
|
|
291
|
+
const { file, vars } = primary;
|
|
292
|
+
return {
|
|
293
|
+
source: file,
|
|
294
|
+
vars: redactSensitiveVars(vars),
|
|
295
|
+
port: extractPort(vars),
|
|
296
|
+
host: extractHost(vars),
|
|
297
|
+
apiTarget: extractApiTarget(vars),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
parseEnvContent,
|
|
303
|
+
findPrimaryEnvFile,
|
|
304
|
+
readPrimaryEnv,
|
|
305
|
+
extractPort,
|
|
306
|
+
extractHost,
|
|
307
|
+
extractApiTarget,
|
|
308
|
+
readStackEnvInfo,
|
|
309
|
+
isSensitiveVarName,
|
|
310
|
+
redactSensitiveVars,
|
|
311
|
+
// Exported for test visibility:
|
|
312
|
+
ENV_FILE_ORDER,
|
|
313
|
+
PORT_VAR_KEYS,
|
|
314
|
+
HOST_VAR_KEYS,
|
|
315
|
+
API_TARGET_VAR_KEYS,
|
|
316
|
+
SENSITIVE_VAR_PATTERNS,
|
|
317
|
+
};
|
package/lib/memory-scaffold.js
CHANGED
|
@@ -720,8 +720,10 @@ function appendClaudeMdL4Memory(claudeMdPath, { lang = "en" } = {}) {
|
|
|
720
720
|
// Heading pattern: lines starting with `##` (or more) that contain `(L4)`.
|
|
721
721
|
// Using a heading-scoped regex avoids false positives when the user has
|
|
722
722
|
// written `(L4)` elsewhere in body text (e.g. "Layer 4 load balancer (L4)").
|
|
723
|
-
// The language-independent `(L4)` token still allows translated
|
|
724
|
-
//
|
|
723
|
+
// The language-independent `(L4)` token still allows translated
|
|
724
|
+
// headings (in any of the 10 supported output languages) to be
|
|
725
|
+
// recognised — only the `(L4)` suffix is stable; the preceding
|
|
726
|
+
// "Memory" word varies per language.
|
|
725
727
|
const MARKER_REGEX = /^#{2,}\s+.*\(L4\)/m;
|
|
726
728
|
if (!existsSafe(claudeMdPath)) return false;
|
|
727
729
|
const existing = readFileSafe(claudeMdPath);
|
|
@@ -980,7 +982,7 @@ function scaffoldDocWritingGuide(standardCoreDir, { overwrite = false, lang = "e
|
|
|
980
982
|
// Pass 3c is expected to generate claudeos-core/skills/00.shared/MANIFEST.md,
|
|
981
983
|
// but the stack pass3.md templates list it among generation targets without
|
|
982
984
|
// marking it REQUIRED. On frontend-only or skill-sparse projects Claude may
|
|
983
|
-
// omit it, leaving .claude/rules/50.sync/
|
|
985
|
+
// omit it, leaving .claude/rules/50.sync/02.skills-sync.md (which names
|
|
984
986
|
// MANIFEST.md as the single source of truth for skill registration) pointing
|
|
985
987
|
// at a non-existent file. This gap-fill creates a minimal stub in Pass 4 if
|
|
986
988
|
// the file is missing after Pass 3 completes — same contract as
|
|
@@ -988,7 +990,7 @@ function scaffoldDocWritingGuide(standardCoreDir, { overwrite = false, lang = "e
|
|
|
988
990
|
const SKILLS_MANIFEST_STUB = `# Skill Registry
|
|
989
991
|
|
|
990
992
|
_Single source of truth for registered skills in this project._
|
|
991
|
-
_Referenced by: \`.claude/rules/50.sync/
|
|
993
|
+
_Referenced by: \`.claude/rules/50.sync/02.skills-sync.md\`_
|
|
992
994
|
|
|
993
995
|
## How to register a skill
|
|
994
996
|
|
|
@@ -1005,7 +1007,7 @@ with its path, purpose, and the orchestrator file that invokes it.
|
|
|
1005
1007
|
|
|
1006
1008
|
- When a skill file is added/renamed/deleted under \`claudeos-core/skills/\`,
|
|
1007
1009
|
update this manifest in the same commit.
|
|
1008
|
-
- When this manifest is modified, \`.claude/rules/50.sync/
|
|
1010
|
+
- When this manifest is modified, \`.claude/rules/50.sync/02.skills-sync.md\`
|
|
1009
1011
|
is NOT modified — it references this file by path, not by content.
|
|
1010
1012
|
`;
|
|
1011
1013
|
|
package/package.json
CHANGED