@nfinitmonkeys/clan-engine 0.1.1 → 0.3.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/README.md +62 -10
- package/dist/apply.js +7 -1
- package/dist/brain-build.d.ts +49 -0
- package/dist/brain-build.js +278 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +75 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/inspect.js +102 -5
- package/dist/inventory.d.ts +3 -1
- package/dist/inventory.js +4 -2
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.js +11 -0
- package/dist/reconcile.js +24 -4
- package/dist/sanitize.d.ts +23 -0
- package/dist/sanitize.js +71 -0
- package/dist/templates.d.ts +4 -1
- package/dist/templates.js +244 -20
- package/dist/types.d.ts +24 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/inspect.js
CHANGED
|
@@ -51,6 +51,70 @@ function brainRootFor(tier, alphaName) {
|
|
|
51
51
|
return `.brains/${(alphaName ?? '').toLowerCase()}`;
|
|
52
52
|
return '.brain';
|
|
53
53
|
}
|
|
54
|
+
/** Vault presence at a root: a wiki/ dir or a .brain-meta.json. */
|
|
55
|
+
function vaultExists(repo, r) {
|
|
56
|
+
return existsSync(join(repo, r, 'wiki')) || existsSync(join(repo, r, '.brain-meta.json'));
|
|
57
|
+
}
|
|
58
|
+
/** The vault an EXISTING load-brain SessionStart hook already points at
|
|
59
|
+
* (repo-relative), when that directory still exists. The live hook is ground
|
|
60
|
+
* truth: it names the vault sessions ACTUALLY load, so setup must never
|
|
61
|
+
* repoint away from it (e.g. the jungle repo's debris-only .brain/ vs the
|
|
62
|
+
* real .brains/obsidian the hook hydrates). */
|
|
63
|
+
function hookedBrainRoot(repo) {
|
|
64
|
+
const raw = read(join(repo, '.claude', 'settings.json'));
|
|
65
|
+
if (raw === null)
|
|
66
|
+
return undefined;
|
|
67
|
+
let doc;
|
|
68
|
+
try {
|
|
69
|
+
doc = JSON.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const entries = Array.isArray(doc?.hooks?.SessionStart) ? doc.hooks.SessionStart : [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
for (const h of entry?.hooks ?? []) {
|
|
77
|
+
const m = /(?:^|[\s"'=])((?:\$\{?CLAUDE_PROJECT_DIR\}?\/)?[^\s"']+)\/scripts\/load-brain\.(?:mjs|sh)/
|
|
78
|
+
.exec(h?.command ?? '');
|
|
79
|
+
if (!m)
|
|
80
|
+
continue;
|
|
81
|
+
const rel = m[1].replace(/^\$\{?CLAUDE_PROJECT_DIR\}?\//, '').replace(/^\.\//, '');
|
|
82
|
+
if (rel && !rel.startsWith('/') && !rel.includes('..') && existsSync(join(repo, rel)))
|
|
83
|
+
return rel;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/** An EXISTING vault at ANY known root. Precedence:
|
|
89
|
+
* 1. the vault a load-brain SessionStart hook already loads — wins outright;
|
|
90
|
+
* 2. vaults containing wiki/identity.md, in order .district/brain >
|
|
91
|
+
* tier-default root > .brain > .brains/<alpha> > other .brains/*
|
|
92
|
+
* (preferring 'obsidian') — a real identity beats debris (e.g. a
|
|
93
|
+
* sources-only .brain/ left by a memory bridge);
|
|
94
|
+
* 3. bare-existing vaults in that same order;
|
|
95
|
+
* 4. none → undefined (tier default applies downstream).
|
|
96
|
+
* Data safety: reconcile scaffolds at the detected root, never a tier-default
|
|
97
|
+
* duplicate — and never away from the vault the live hook hydrates. */
|
|
98
|
+
function detectBrainRoot(repo, tier, alphaName) {
|
|
99
|
+
const hooked = hookedBrainRoot(repo);
|
|
100
|
+
if (hooked)
|
|
101
|
+
return hooked;
|
|
102
|
+
const candidates = ['.district/brain', brainRootFor(tier, alphaName), '.brain'];
|
|
103
|
+
if (alphaName)
|
|
104
|
+
candidates.push(`.brains/${alphaName.toLowerCase()}`);
|
|
105
|
+
try {
|
|
106
|
+
const dirs = readdirSync(join(repo, '.brains')).filter(d => vaultExists(repo, `.brains/${d}`));
|
|
107
|
+
for (const d of [...dirs.filter(x => x === 'obsidian'), ...dirs.filter(x => x !== 'obsidian').sort()]) {
|
|
108
|
+
candidates.push(`.brains/${d}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* no .brains */ }
|
|
112
|
+
const uniq = [...new Set(candidates)].filter(c => c && !c.endsWith('/'));
|
|
113
|
+
const withIdentity = uniq.find(r => existsSync(join(repo, r, 'wiki', 'identity.md')));
|
|
114
|
+
if (withIdentity)
|
|
115
|
+
return withIdentity;
|
|
116
|
+
return uniq.find(r => vaultExists(repo, r));
|
|
117
|
+
}
|
|
54
118
|
export function inspect(repo, opts = {}) {
|
|
55
119
|
const notes = [];
|
|
56
120
|
const { tier, source: tierSource } = detectTier(repo, opts.tierFlag);
|
|
@@ -69,15 +133,48 @@ export function inspect(repo, opts = {}) {
|
|
|
69
133
|
notes.push('.jungle/config.yaml is unparseable');
|
|
70
134
|
}
|
|
71
135
|
}
|
|
72
|
-
|
|
136
|
+
// 'registered' / 'onboarded' are older attach-era statuses pointing at a real
|
|
137
|
+
// Jungle (fleet: Funnel, Beacon) — flipping them to standalone would rewrite
|
|
138
|
+
// their packs with "the user is the approver" fiction. apiUrl is the
|
|
139
|
+
// connection intent (Funnel's apiKey is empty but its Jungle is real — the
|
|
140
|
+
// missing-key note below stays honest). Full creds with no status at all =
|
|
141
|
+
// legacy attach (old clan-ready treated apiUrl alone as connected).
|
|
142
|
+
const status = cfg?.jungle?.status;
|
|
143
|
+
const hasCreds = !!(cfg?.jungle?.apiUrl && cfg?.jungle?.alphaId && cfg?.jungle?.apiKey);
|
|
144
|
+
const connected = status === 'connected' || status === 'approved'
|
|
145
|
+
|| ((status === 'registered' || status === 'onboarded') && !!cfg?.jungle?.apiUrl)
|
|
146
|
+
|| (status === undefined && hasCreds);
|
|
73
147
|
const mode = connected ? 'connected' : 'standalone';
|
|
74
|
-
|
|
148
|
+
// ── .district/district.config.yaml (the district box's identity) ──
|
|
149
|
+
let dcfg = null;
|
|
150
|
+
const dcfgRaw = read(join(repo, '.district', 'district.config.yaml'));
|
|
151
|
+
if (dcfgRaw !== null) {
|
|
152
|
+
try {
|
|
153
|
+
dcfg = YAML.parse(dcfgRaw);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
notes.push('.district/district.config.yaml is unparseable');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ── existing vault (any known root) — wins over the tier default ──
|
|
160
|
+
const existingBrainRoot = detectBrainRoot(repo, tier, dcfg?.district?.alphaName ?? cfg?.project?.alphaName);
|
|
161
|
+
const identity = dcfg?.district?.name && dcfg?.district?.alphaName ? {
|
|
162
|
+
name: dcfg.district.name,
|
|
163
|
+
alphaName: dcfg.district.alphaName,
|
|
164
|
+
description: dcfg.district.description ?? '',
|
|
165
|
+
tier,
|
|
166
|
+
mode,
|
|
167
|
+
alphaId: dcfg.district.alphaId,
|
|
168
|
+
boxPort: String(dcfg.http?.port ?? '7787'),
|
|
169
|
+
brainRoot: existingBrainRoot,
|
|
170
|
+
} : cfg?.project?.name && cfg?.project?.alphaName ? {
|
|
75
171
|
name: cfg.project.name,
|
|
76
172
|
alphaName: cfg.project.alphaName,
|
|
77
173
|
description: cfg.project.description ?? '',
|
|
78
174
|
tier,
|
|
79
175
|
mode,
|
|
80
176
|
alphaId: cfg.jungle?.alphaId,
|
|
177
|
+
brainRoot: existingBrainRoot,
|
|
81
178
|
jungle: connected && cfg.jungle?.apiUrl && cfg.jungle?.alphaId && cfg.jungle?.apiKey
|
|
82
179
|
? { apiUrl: cfg.jungle.apiUrl, alphaId: cfg.jungle.alphaId, apiKey: cfg.jungle.apiKey }
|
|
83
180
|
: undefined,
|
|
@@ -104,8 +201,8 @@ export function inspect(repo, opts = {}) {
|
|
|
104
201
|
notes.push('.mcp.json is unparseable');
|
|
105
202
|
}
|
|
106
203
|
}
|
|
107
|
-
// ── brain (at the tier-aware
|
|
108
|
-
const bRoot = brainRootFor(tier, cfg?.project?.alphaName);
|
|
204
|
+
// ── brain (existing vault at any root > the tier-aware default) ──
|
|
205
|
+
const bRoot = existingBrainRoot ?? brainRootFor(tier, dcfg?.district?.alphaName ?? cfg?.project?.alphaName);
|
|
109
206
|
let hasBrain = false;
|
|
110
207
|
let brainLayoutVersion;
|
|
111
208
|
const metaRaw = read(join(repo, bRoot, '.brain-meta.json'));
|
|
@@ -166,6 +263,6 @@ export function inspect(repo, opts = {}) {
|
|
|
166
263
|
repo, os, isGit, tier, tierSource, mode, identity,
|
|
167
264
|
hasConfig, configError, hasMcp, mcpServers,
|
|
168
265
|
inventory: scanInventory(repo, { home: opts.home }),
|
|
169
|
-
hasBrain, brainLayoutVersion, state, notes,
|
|
266
|
+
hasBrain, brainLayoutVersion, brainRoot: existingBrainRoot, state, notes,
|
|
170
267
|
};
|
|
171
268
|
}
|
package/dist/inventory.d.ts
CHANGED
|
@@ -14,5 +14,7 @@ import type { InventoryItem } from './types.js';
|
|
|
14
14
|
export declare function scanInventory(repo: string, opts?: {
|
|
15
15
|
home?: string;
|
|
16
16
|
}): InventoryItem[];
|
|
17
|
-
/** Write the manifest the awareness skill references. Returns the repo-relative
|
|
17
|
+
/** Write the manifest the awareness skill references. Returns the repo-relative
|
|
18
|
+
* path. PROJECT scope only — global (~/.claude) assets are the operator's
|
|
19
|
+
* private tooling and must not be persisted into a committable repo file. */
|
|
18
20
|
export declare function writeInventoryManifest(repo: string, items: InventoryItem[]): string;
|
package/dist/inventory.js
CHANGED
|
@@ -126,11 +126,13 @@ export function scanInventory(repo, opts = {}) {
|
|
|
126
126
|
return true;
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
/** Write the manifest the awareness skill references. Returns the repo-relative
|
|
129
|
+
/** Write the manifest the awareness skill references. Returns the repo-relative
|
|
130
|
+
* path. PROJECT scope only — global (~/.claude) assets are the operator's
|
|
131
|
+
* private tooling and must not be persisted into a committable repo file. */
|
|
130
132
|
export function writeInventoryManifest(repo, items) {
|
|
131
133
|
const dir = join(repo, '.clan');
|
|
132
134
|
mkdirSync(dir, { recursive: true });
|
|
133
|
-
const rel = items.map(i => ({ kind: i.kind, name: i.name, scope: i.scope, description: i.description ?? null }));
|
|
135
|
+
const rel = items.filter(i => i.scope === 'project').map(i => ({ kind: i.kind, name: i.name, scope: i.scope, description: i.description ?? null }));
|
|
134
136
|
writeFileSync(join(dir, 'inventory.json'), JSON.stringify({ generatedBy: 'clan-engine', items: rel }, null, 2) + '\n', 'utf-8');
|
|
135
137
|
return '.clan/inventory.json';
|
|
136
138
|
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export declare function jungleLocalServer(c: JungleCreds): {
|
|
|
17
17
|
args: string[];
|
|
18
18
|
env: Record<string, string>;
|
|
19
19
|
};
|
|
20
|
+
/** District-tier creds: the jungle-local server speaks to the LOCAL district
|
|
21
|
+
* box (localhost:<boxPort>), not the cloud Jungle. Same server shape — only
|
|
22
|
+
* the URL differs. The self-row key is only capturable at district init (the
|
|
23
|
+
* box stores its hash); callers pass it via env, never argv. */
|
|
24
|
+
export declare function districtBoxCreds(boxPort: string, alphaId: string, apiKey: string): JungleCreds;
|
|
25
|
+
/** .mcp.json plan for a district — jungle-local → the local box. */
|
|
26
|
+
export declare function planDistrictMcpJson(repo: string, boxPort: string, creds: {
|
|
27
|
+
alphaId: string;
|
|
28
|
+
apiKey: string;
|
|
29
|
+
}, isGit: boolean): PlanAction[];
|
|
20
30
|
/**
|
|
21
31
|
* Produce the reconciliation action(s) for .mcp.json given the target creds.
|
|
22
32
|
* Returns a 'merge' action (new file content) plus, when a fresh file is
|
package/dist/mcp.js
CHANGED
|
@@ -17,6 +17,17 @@ export function jungleLocalServer(c) {
|
|
|
17
17
|
env.JUNGLE_HUMAN_EMAIL = c.humanEmail;
|
|
18
18
|
return { command: 'npx', args: ['-y', '@nfinitmonkeys/jungle-mcp'], env };
|
|
19
19
|
}
|
|
20
|
+
/** District-tier creds: the jungle-local server speaks to the LOCAL district
|
|
21
|
+
* box (localhost:<boxPort>), not the cloud Jungle. Same server shape — only
|
|
22
|
+
* the URL differs. The self-row key is only capturable at district init (the
|
|
23
|
+
* box stores its hash); callers pass it via env, never argv. */
|
|
24
|
+
export function districtBoxCreds(boxPort, alphaId, apiKey) {
|
|
25
|
+
return { apiUrl: `http://localhost:${boxPort}`, alphaId, apiKey };
|
|
26
|
+
}
|
|
27
|
+
/** .mcp.json plan for a district — jungle-local → the local box. */
|
|
28
|
+
export function planDistrictMcpJson(repo, boxPort, creds, isGit) {
|
|
29
|
+
return planMcpJson(repo, districtBoxCreds(boxPort, creds.alphaId, creds.apiKey), isGit);
|
|
30
|
+
}
|
|
20
31
|
/**
|
|
21
32
|
* Produce the reconciliation action(s) for .mcp.json given the target creds.
|
|
22
33
|
* Returns a 'merge' action (new file content) plus, when a fresh file is
|
package/dist/reconcile.js
CHANGED
|
@@ -63,6 +63,11 @@ function reconcileClaudeMd(repo, i) {
|
|
|
63
63
|
}
|
|
64
64
|
export function reconcile(identity, ins, opts = {}) {
|
|
65
65
|
const repo = ins.repo;
|
|
66
|
+
// Data safety: an EXISTING vault at any known root wins over the tier
|
|
67
|
+
// default — re-tiering (or the jungle repo's own .brain/) must never mint a
|
|
68
|
+
// duplicate vault. inspect detects it; every template downstream honors it.
|
|
69
|
+
if (ins.brainRoot && !identity.brainRoot)
|
|
70
|
+
identity = { ...identity, brainRoot: ins.brainRoot };
|
|
66
71
|
const actions = [];
|
|
67
72
|
// 1. CLAUDE.md declaration
|
|
68
73
|
actions.push(reconcileClaudeMd(repo, identity));
|
|
@@ -83,19 +88,34 @@ export function reconcile(identity, ins, opts = {}) {
|
|
|
83
88
|
// 4. managed .claude surface (awareness SKILL.md + tier command pack + hook script)
|
|
84
89
|
for (const f of claudeSurface(identity))
|
|
85
90
|
actions.push(reconcileFile(repo, f));
|
|
86
|
-
// 5. inventory manifest (refresh when changed — idempotent otherwise)
|
|
87
|
-
|
|
91
|
+
// 5. inventory manifest (refresh when changed — idempotent otherwise).
|
|
92
|
+
// PROJECT scope only: persisting the operator's global ~/.claude asset
|
|
93
|
+
// list would leak private tooling metadata into every (committable) repo
|
|
94
|
+
// and churn on every machine change. Global items stay in the in-memory
|
|
95
|
+
// InspectResult for the wizard.
|
|
96
|
+
const inv = scanInventory(repo).filter(i => i.scope === 'project');
|
|
88
97
|
const invContent = JSON.stringify({ generatedBy: 'clan-engine', items: inv.map(i => ({ kind: i.kind, name: i.name, scope: i.scope, description: i.description ?? null })) }, null, 2) + '\n';
|
|
89
98
|
const invExisting = read(repo, '.clan/inventory.json');
|
|
90
99
|
if (invExisting === null)
|
|
91
|
-
actions.push({ kind: 'create', path: '.clan/inventory.json', content: invContent, note: `inventory ${inv.length} existing asset(s) the Alpha can use` });
|
|
100
|
+
actions.push({ kind: 'create', path: '.clan/inventory.json', content: invContent, note: `inventory ${inv.length} existing project asset(s) the Alpha can use` });
|
|
92
101
|
else if (invExisting !== invContent)
|
|
93
|
-
actions.push({ kind: 'update', path: '.clan/inventory.json', content: invContent, note: `refresh inventory (${inv.length} asset(s))` });
|
|
102
|
+
actions.push({ kind: 'update', path: '.clan/inventory.json', content: invContent, note: `refresh inventory (${inv.length} project asset(s))` });
|
|
94
103
|
else
|
|
95
104
|
actions.push({ kind: 'skip', path: '.clan/inventory.json', note: 'inventory unchanged', reason: 'unchanged' });
|
|
96
105
|
// 6. .mcp.json (connected mode)
|
|
97
106
|
if (opts.jungle)
|
|
98
107
|
actions.push(...planMcpJson(repo, opts.jungle, ins.isGit));
|
|
108
|
+
// 6b. journals + backups under .claude/.clan-engine/ can embed live-key
|
|
109
|
+
// artifacts (a backed-up .mcp.json on merge) — never committable. Ignore
|
|
110
|
+
// the dir in the same run that gitignores .mcp.json itself.
|
|
111
|
+
if (ins.isGit) {
|
|
112
|
+
const gi = read(repo, '.gitignore') ?? '';
|
|
113
|
+
if (!gi.split('\n').some(l => l.trim() === '.claude/.clan-engine/')) {
|
|
114
|
+
const priorAppend = actions.some(a => a.kind === 'append' && a.path === '.gitignore');
|
|
115
|
+
const prefix = priorAppend || gi.length === 0 || gi.endsWith('\n') ? '' : '\n';
|
|
116
|
+
actions.push({ kind: 'append', path: '.gitignore', content: prefix + '.claude/.clan-engine/\n', note: 'gitignore .claude/.clan-engine/ (journals + backups may hold backed-up keys)' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
99
119
|
// 7. SessionStart brain hook (tier-aware command)
|
|
100
120
|
actions.push(...planBrainHook(repo, identity));
|
|
101
121
|
// 8. brain scaffold — create-only (never overwrite accumulated brain pages)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret / infra / PHI filter — the non-negotiable gate on anything that goes
|
|
3
|
+
* into a brain. A clan's brain_index is cross-clan-readable by council/master
|
|
4
|
+
* keys, and repo docs + session memory routinely hold ssh keys, prod creds,
|
|
5
|
+
* connection strings, and (for HIPAA clans) PHI. Every candidate body is scanned
|
|
6
|
+
* BEFORE it becomes a brain page; anything that trips is HELD BACK, never
|
|
7
|
+
* written. Reasons are LABELS ONLY — the matched secret text is never returned,
|
|
8
|
+
* so a scan result is safe to log.
|
|
9
|
+
*
|
|
10
|
+
* PHI scanning is ON BY DEFAULT (opt out with { hipaa: false }): a false-positive
|
|
11
|
+
* hold-back is cheap; a PHI leak is catastrophic.
|
|
12
|
+
*/
|
|
13
|
+
export interface ScanResult {
|
|
14
|
+
flagged: boolean;
|
|
15
|
+
reasons: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Scan text for secrets + infra identifiers (always) and PHI (unless
|
|
19
|
+
* hipaa:false is explicitly passed — PHI is scanned by default).
|
|
20
|
+
*/
|
|
21
|
+
export declare function scanSensitive(text: string, opts?: {
|
|
22
|
+
hipaa?: boolean;
|
|
23
|
+
}): ScanResult;
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret / infra / PHI filter — the non-negotiable gate on anything that goes
|
|
3
|
+
* into a brain. A clan's brain_index is cross-clan-readable by council/master
|
|
4
|
+
* keys, and repo docs + session memory routinely hold ssh keys, prod creds,
|
|
5
|
+
* connection strings, and (for HIPAA clans) PHI. Every candidate body is scanned
|
|
6
|
+
* BEFORE it becomes a brain page; anything that trips is HELD BACK, never
|
|
7
|
+
* written. Reasons are LABELS ONLY — the matched secret text is never returned,
|
|
8
|
+
* so a scan result is safe to log.
|
|
9
|
+
*
|
|
10
|
+
* PHI scanning is ON BY DEFAULT (opt out with { hipaa: false }): a false-positive
|
|
11
|
+
* hold-back is cheap; a PHI leak is catastrophic.
|
|
12
|
+
*/
|
|
13
|
+
const SECRET_PATTERNS = [
|
|
14
|
+
['private-key-block', /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP |ENCRYPTED )?PRIVATE KEY-----/],
|
|
15
|
+
['ssh-public-key', /\bssh-(?:rsa|ed25519|dss)\s+AAAA[0-9A-Za-z+/]{20,}/],
|
|
16
|
+
['aws-access-key-id', /\b(?:AKIA|ASIA|AGPA|AIDA)[0-9A-Z]{16}\b/],
|
|
17
|
+
['aws-secret-assignment', /\b(?:aws.?)?secret.?access.?key\b\s*[:=]\s*\S{16,}/i],
|
|
18
|
+
['gcp-api-key', /\bAIza[0-9A-Za-z_\-]{35,}/],
|
|
19
|
+
['gcp-service-account', /\b[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com\b|"private_key_id"\s*:/],
|
|
20
|
+
['azure-storage-key', /\bAccountKey\s*=\s*[A-Za-z0-9+/=]{20,}/i],
|
|
21
|
+
['azure-sas', /\bSharedAccessSignature\b|[?&]sig=[A-Za-z0-9%]{20,}/],
|
|
22
|
+
['stripe-key', /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b|\bwhsec_[A-Za-z0-9]{16,}\b/],
|
|
23
|
+
['twilio-key', /\b(?:SK|AC)[0-9a-f]{32}\b/],
|
|
24
|
+
['sendgrid-key', /\bSG\.[A-Za-z0-9_\-]{16,}\.[A-Za-z0-9_\-]{16,}\b/],
|
|
25
|
+
['github-token', /\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b/],
|
|
26
|
+
['slack-token', /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/],
|
|
27
|
+
['openai-anthropic-cortex-key', /\bsk-(?:ant-|cortex-|proj-|live-)?[A-Za-z0-9_-]{16,}\b/],
|
|
28
|
+
['jwt', /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/],
|
|
29
|
+
['bearer-or-authorization', /\b(?:authorization|bearer)\b\s*:?\s*(?:bearer\s+)?[A-Za-z0-9._\-]{16,}/i],
|
|
30
|
+
['uri-with-embedded-creds', /\b[a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+:[^\s@/]+@/i],
|
|
31
|
+
['dotnet-connection-string', /\b(?:Password|Pwd)\s*=\s*[^;\s]{6,};?/i],
|
|
32
|
+
['generic-secret-assignment', /\b(?:password|passwd|secret|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer|token)\b\s*[:=]\s*['"]?[A-Za-z0-9/_+=.\-]{10,}/i],
|
|
33
|
+
['env-assignment-high-entropy', /^[A-Z][A-Z0-9_]{2,}\s*=\s*\S{16,}$/m],
|
|
34
|
+
];
|
|
35
|
+
const INFRA_PATTERNS = [
|
|
36
|
+
['aws-instance-id', /\bi-[0-9a-f]{8,17}\b/],
|
|
37
|
+
['aws-sg-id', /\bsg-[0-9a-f]{8,17}\b/],
|
|
38
|
+
['aws-subnet-or-vpc-id', /\b(?:subnet|vpc|eni|vol|ami)-[0-9a-f]{8,17}\b/],
|
|
39
|
+
['aws-account-arn', /\b\d{12}\.dkr\.ecr\b|arn:aws:[^\s]*:\d{12}:/i],
|
|
40
|
+
['rds-or-cloud-endpoint', /\b[a-z0-9-]+\.[a-z0-9]+\.[a-z0-9-]+\.(?:rds|elasticache|redshift)\.amazonaws\.com\b/i],
|
|
41
|
+
['private-ipv4', /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3})\b/],
|
|
42
|
+
['private-ipv6', /\b(?:fd[0-9a-f]{2}:[0-9a-f:]{2,}|fe80:[0-9a-f:]{2,})/i],
|
|
43
|
+
['ssh-to-ip', /\bssh\b[^\n]*@\d{1,3}(?:\.\d{1,3}){3}\b/i],
|
|
44
|
+
['key-file-ref', /\b[\w-]{3,}\.(?:pem|ppk)\b|(?:\/|~\/)?\.ssh\/[\w.-]+/],
|
|
45
|
+
];
|
|
46
|
+
const PHI_PATTERNS = [
|
|
47
|
+
['ssn', /\b\d{3}[-. ]\d{2}[-. ]\d{4}\b|\bssn\b[^\n]{0,20}\b\d{9}\b/i],
|
|
48
|
+
['mrn', /\bMRN\b\s*[:#]?\s*\d{5,}/i],
|
|
49
|
+
['dob', /\b(?:DOB|date of birth)\b\s*[:#]?\s*\d/i],
|
|
50
|
+
['patient-identifier', /\bpatient\b[^.\n]{0,40}\b(?:name|ssn|mrn|dob|record\s*number)\b/i],
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Scan text for secrets + infra identifiers (always) and PHI (unless
|
|
54
|
+
* hipaa:false is explicitly passed — PHI is scanned by default).
|
|
55
|
+
*/
|
|
56
|
+
export function scanSensitive(text, opts = {}) {
|
|
57
|
+
const reasons = [];
|
|
58
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
59
|
+
return { flagged: false, reasons };
|
|
60
|
+
for (const [label, re] of SECRET_PATTERNS)
|
|
61
|
+
if (re.test(text))
|
|
62
|
+
reasons.push(label);
|
|
63
|
+
for (const [label, re] of INFRA_PATTERNS)
|
|
64
|
+
if (re.test(text))
|
|
65
|
+
reasons.push('infra:' + label);
|
|
66
|
+
if (opts.hipaa !== false)
|
|
67
|
+
for (const [label, re] of PHI_PATTERNS)
|
|
68
|
+
if (re.test(text))
|
|
69
|
+
reasons.push('phi:' + label);
|
|
70
|
+
return { flagged: reasons.length > 0, reasons };
|
|
71
|
+
}
|
package/dist/templates.d.ts
CHANGED
|
@@ -36,7 +36,10 @@ export declare function findGeoTierMarker(content: string): {
|
|
|
36
36
|
* (reconcile handles that) — we never rewrite a user's prose. */
|
|
37
37
|
export declare function claudeMdIdentity(i: ClanIdentity): string;
|
|
38
38
|
/** Where the brain lives, per tier (matches the fleet's clan-ready layout so
|
|
39
|
-
* the engine never puts .brain/ where a district/jungle expects its vault).
|
|
39
|
+
* the engine never puts .brain/ where a district/jungle expects its vault).
|
|
40
|
+
* An EXISTING vault (i.brainRoot, set from inspect) wins over the tier
|
|
41
|
+
* default — applying to a repo with a vault elsewhere must never mint a
|
|
42
|
+
* duplicate. */
|
|
40
43
|
export declare function brainRoot(i: ClanIdentity): string;
|
|
41
44
|
export declare function brainScaffold(i: ClanIdentity, today: string): GeneratedFile[];
|
|
42
45
|
/** The command wired into .claude/settings.json SessionStart. Relative (hooks
|