@nfinitmonkeys/clan-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/apply.d.ts +14 -0
- package/dist/apply.js +88 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +175 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +85 -0
- package/dist/hooks.d.ts +18 -0
- package/dist/hooks.js +76 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +32 -0
- package/dist/inspect.d.ts +10 -0
- package/dist/inspect.js +171 -0
- package/dist/inventory.d.ts +18 -0
- package/dist/inventory.js +136 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +74 -0
- package/dist/reconcile.d.ts +27 -0
- package/dist/reconcile.js +109 -0
- package/dist/templates.d.ts +58 -0
- package/dist/templates.js +303 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +12 -0
- package/dist/undo.d.ts +14 -0
- package/dist/undo.js +43 -0
- package/package.json +41 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE authoritative template set. This is the single source of truth for every
|
|
3
|
+
* file the engine writes into a node — the whole point of the engine is that
|
|
4
|
+
* the published CLIs AND the internal fleet tooling both render from here, so
|
|
5
|
+
* they can never drift again.
|
|
6
|
+
*
|
|
7
|
+
* Every generated file carries a marker (`generated-by: clan-engine@<v>` +
|
|
8
|
+
* `layout: <n>`) so reconciliation can distinguish our output (safe to update)
|
|
9
|
+
* from a user's hand-written file at the same path (must be backed up).
|
|
10
|
+
*/
|
|
11
|
+
import { ENGINE_VERSION, LAYOUT_VERSION, GENERATED_BY } from './types.js';
|
|
12
|
+
/** Frontmatter marker lines (inside a YAML block). */
|
|
13
|
+
export function markerLines() {
|
|
14
|
+
return `${GENERATED_BY}: ${ENGINE_VERSION}\nlayout: ${LAYOUT_VERSION}`;
|
|
15
|
+
}
|
|
16
|
+
/** Frontmatter keys that mean "this file is engine-managed" — our own marker
|
|
17
|
+
* plus predecessor markers so a first engine pass over a clan-ready-managed
|
|
18
|
+
* repo ADOPTS (updates) the files instead of backup-replacing all of them. */
|
|
19
|
+
const MANAGED_KEYS = [GENERATED_BY, 'generated-by'];
|
|
20
|
+
/** Extract the leading YAML frontmatter block (between the first two `---`). */
|
|
21
|
+
function frontmatterBlock(content) {
|
|
22
|
+
if (!content.startsWith('---'))
|
|
23
|
+
return null;
|
|
24
|
+
const end = content.indexOf('\n---', 3);
|
|
25
|
+
return end === -1 ? null : content.slice(3, end);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* True only when the file carries an engine (or predecessor) marker as a real
|
|
29
|
+
* frontmatter KEY — not merely a substring somewhere in the body. This stops a
|
|
30
|
+
* user file that happens to mention "clan-engine:" in prose from being
|
|
31
|
+
* misclassified as ours and silently overwritten.
|
|
32
|
+
*/
|
|
33
|
+
export function hasManagedMarker(content) {
|
|
34
|
+
const fm = frontmatterBlock(content);
|
|
35
|
+
if (fm === null)
|
|
36
|
+
return false;
|
|
37
|
+
return fm.split('\n').some(line => {
|
|
38
|
+
const m = /^([A-Za-z0-9_-]+):/.exec(line.trim());
|
|
39
|
+
return !!m && MANAGED_KEYS.includes(m[1]);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Geo-tier HTML-comment marker for CLAUDE.md (declared identity, never inferred). */
|
|
43
|
+
export function geoTierMarker(tier) {
|
|
44
|
+
return `<!-- geo-tier: ${tier} -->`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Find a REAL top-level geo-tier declaration: a standalone line (its trimmed
|
|
48
|
+
* content is exactly the marker) that is NOT inside a fenced code block. A
|
|
49
|
+
* marker mentioned inside ``` fences or in prose is documentation, not a
|
|
50
|
+
* declaration, and must be ignored (else tier is mis-detected and retier
|
|
51
|
+
* corrupts the user's docs). Returns the declared tier + the exact line, or null.
|
|
52
|
+
*/
|
|
53
|
+
export function findGeoTierMarker(content) {
|
|
54
|
+
const lineRe = /^<!--\s*geo-tier:\s*(clan|district|jungle|biome)\s*-->$/;
|
|
55
|
+
let inFence = false;
|
|
56
|
+
for (const raw of content.split('\n')) {
|
|
57
|
+
const trimmed = raw.trim();
|
|
58
|
+
if (/^(```|~~~)/.test(trimmed)) {
|
|
59
|
+
inFence = !inFence;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inFence)
|
|
63
|
+
continue;
|
|
64
|
+
const m = lineRe.exec(trimmed);
|
|
65
|
+
if (m)
|
|
66
|
+
return { tier: m[1], line: raw };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// ─── CLAUDE.md identity block ───────────────────────────────────────────────
|
|
71
|
+
/** The identity block prepended to CLAUDE.md when the file is created fresh.
|
|
72
|
+
* When CLAUDE.md already exists we only ensure the geo-tier marker line
|
|
73
|
+
* (reconcile handles that) — we never rewrite a user's prose. */
|
|
74
|
+
export function claudeMdIdentity(i) {
|
|
75
|
+
const noun = i.tier;
|
|
76
|
+
const idLine = i.alphaId ? ` (\`${i.alphaId}\`)` : '';
|
|
77
|
+
return `${geoTierMarker(i.tier)}
|
|
78
|
+
# Identity
|
|
79
|
+
|
|
80
|
+
You are operating as **${i.alphaName}** — the Alpha of ${noun} **${i.name}**${idLine}.
|
|
81
|
+
|
|
82
|
+
This repo is a **${noun.toUpperCase()}** node in the Nfinit Monkeys geography
|
|
83
|
+
(Biome ⊃ Jungle ⊃ District ⊃ Clan). Identity is DECLARED here, never inferred.
|
|
84
|
+
|
|
85
|
+
## Tool defaults
|
|
86
|
+
|
|
87
|
+
- Default MCP prefix: \`mcp__jungle-local__*\` (this node's voice).
|
|
88
|
+
- The Alpha's brain (persistent memory) lives under \`.brain/\` — hydrated at
|
|
89
|
+
session start by the SessionStart hook.
|
|
90
|
+
|
|
91
|
+
## Scope
|
|
92
|
+
|
|
93
|
+
Routine work here is node-local. Slash commands are tier-named:
|
|
94
|
+
\`/${noun}:status\` · \`/${noun}:inbox\` · \`/${noun}:log\` · \`/${noun}:policies\` · \`/${noun}:request\`.
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
// ─── awareness skill (SKILL.md directory format — loadable) ─────────────────
|
|
98
|
+
function awarenessSkill(i) {
|
|
99
|
+
const noun = i.tier;
|
|
100
|
+
const modeLine = i.mode === 'connected'
|
|
101
|
+
? `- **Mode:** connected${i.jungle?.apiUrl ? ` to ${i.jungle.apiUrl}` : ''}${i.alphaId ? `\n- **Alpha ID:** \`${i.alphaId}\`` : ''}`
|
|
102
|
+
: `- **Mode:** standalone — no Jungle, no approval queue. The developer IS the Human Council.`;
|
|
103
|
+
const governance = i.mode === 'connected'
|
|
104
|
+
? `## Governance (connected)
|
|
105
|
+
|
|
106
|
+
Alphas **propose**, humans **approve**. Sensitive actions go through a gated
|
|
107
|
+
approval flow (\`request_approval\` → Human Council reviews in BOSS → execute →
|
|
108
|
+
logged). To change this node's permissions, tell the user to message the Alpha
|
|
109
|
+
in natural language — it will propose the change for approval.`
|
|
110
|
+
: `## Governance (standalone)
|
|
111
|
+
|
|
112
|
+
There is no remote council. The developer at this terminal IS the council. When
|
|
113
|
+
they want to change a policy: confirm it, edit \`.jungle/config.yaml\`, restart
|
|
114
|
+
the Alpha. Explicit confirmation IS the approval — never mutate config silently.`;
|
|
115
|
+
const usingExisting = `## Use what this project already has
|
|
116
|
+
|
|
117
|
+
If the user already has agents, skills, or slash commands (e.g. from other
|
|
118
|
+
tools), PREFER them where they overlap instead of duplicating. The clan is a
|
|
119
|
+
teammate to the existing setup, not a replacement — check \`.clan/inventory.json\`
|
|
120
|
+
(written by the engine) for what's available, and route to it by name.`;
|
|
121
|
+
return {
|
|
122
|
+
path: '.claude/skills/clan-awareness/SKILL.md',
|
|
123
|
+
managed: true,
|
|
124
|
+
content: `---
|
|
125
|
+
name: clan-awareness
|
|
126
|
+
description: This project is a ${noun} node (${i.alphaName} / ${i.name}) run via @nfinitmonkeys/clan — how to answer status, governance, and "talk to the Alpha" questions
|
|
127
|
+
${markerLines()}
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# Clan Awareness — ${i.name}
|
|
131
|
+
|
|
132
|
+
You are inside a **${noun}** node run via \`@nfinitmonkeys/clan\`, with its own
|
|
133
|
+
Alpha (**${i.alphaName}**), policies, and accumulated memory (a "brain").
|
|
134
|
+
|
|
135
|
+
## Working *as* the Alpha
|
|
136
|
+
|
|
137
|
+
In this repo you act **as ${i.alphaName}**. But ${i.alphaName} is bigger than one
|
|
138
|
+
chat: it has a **brain** (\`npx -y @nfinitmonkeys/clan brain "…"\` — persistent
|
|
139
|
+
memory), an **inbox** (\`npx -y @nfinitmonkeys/clan talk "…"\`), and an
|
|
140
|
+
**autonomous loop** (\`npx -y @nfinitmonkeys/clan start\`). If the user says "I
|
|
141
|
+
want to talk to the Alpha," don't say "you already are" — point them to those.
|
|
142
|
+
|
|
143
|
+
## This node
|
|
144
|
+
|
|
145
|
+
- **Name:** ${i.name}
|
|
146
|
+
- **Alpha:** ${i.alphaName}
|
|
147
|
+
- **Tier:** ${noun}
|
|
148
|
+
${modeLine}
|
|
149
|
+
|
|
150
|
+
## Slash commands (tier-named)
|
|
151
|
+
|
|
152
|
+
\`/${noun}:status\` · \`/${noun}:inbox\` · \`/${noun}:log\` · \`/${noun}:policies\` · \`/${noun}:request\`
|
|
153
|
+
|
|
154
|
+
If the user types a different tier's command (e.g. \`/clan:status\` in a
|
|
155
|
+
district), point them to the right one — don't silently alias.
|
|
156
|
+
|
|
157
|
+
${usingExisting}
|
|
158
|
+
|
|
159
|
+
${governance}
|
|
160
|
+
`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// ─── tier-named command pack ────────────────────────────────────────────────
|
|
164
|
+
function cmd(tier, name, description, body) {
|
|
165
|
+
return {
|
|
166
|
+
path: `.claude/commands/${tier}/${name}.md`,
|
|
167
|
+
managed: true,
|
|
168
|
+
content: `---
|
|
169
|
+
name: ${tier}:${name}
|
|
170
|
+
description: ${description}
|
|
171
|
+
${markerLines()}
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
${body}
|
|
175
|
+
`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function commandPack(i) {
|
|
179
|
+
const t = i.tier;
|
|
180
|
+
const files = [
|
|
181
|
+
cmd(t, 'status', `Show this ${t}'s live status — identity, policies, inbox, telemetry`, `Run the clan status command and present it as a clean dashboard. If MCP is wired, you may also call \`mcp__jungle-local__jungle_whoami\` and \`mcp__jungle-local__jungle_get_self\`.
|
|
182
|
+
|
|
183
|
+
\`\`\`bash
|
|
184
|
+
npx -y @nfinitmonkeys/clan status
|
|
185
|
+
\`\`\``),
|
|
186
|
+
cmd(t, 'policies', `Show this ${t}'s autonomous / gated / denied actions`, `\`\`\`bash
|
|
187
|
+
npx -y @nfinitmonkeys/clan policies
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
Explain what each action bucket means for this project. To change one in
|
|
191
|
+
connected mode, tell the user to message the Alpha; standalone, edit
|
|
192
|
+
\`.jungle/config.yaml\` directly.`),
|
|
193
|
+
cmd(t, 'request', `Propose a policy or governance change for this ${t}`, i.mode === 'connected'
|
|
194
|
+
? `Compose the natural-language request the user sends to the Alpha (which files a \`request_approval\` with actionType=policy_change). Confirm the target bucket + rationale, then:
|
|
195
|
+
|
|
196
|
+
\`\`\`bash
|
|
197
|
+
npx -y @nfinitmonkeys/clan talk "your request here"
|
|
198
|
+
\`\`\``
|
|
199
|
+
: `Standalone — the user is the approver. Confirm the change (actionType, bucket, why), edit \`.jungle/config.yaml\`, restart the Alpha. Never fabricate a remote approval flow.`),
|
|
200
|
+
];
|
|
201
|
+
if (i.mode === 'connected') {
|
|
202
|
+
files.push(cmd(t, 'inbox', `View recent messages in ${i.alphaName}'s inbox`, `\`\`\`bash
|
|
203
|
+
npx -y @nfinitmonkeys/clan inbox
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
Summarize pending (●) vs read (○); highlight anything urgent. Requires a Jungle
|
|
207
|
+
connection.`), cmd(t, 'log', `Governance log — every policy change with who/when/why`, `\`\`\`bash
|
|
208
|
+
npx -y @nfinitmonkeys/clan status --json 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{const j=JSON.parse(s);console.log(JSON.stringify((j.self&&j.self.governanceLog||[]).slice(0,10),null,2))}catch{console.log('[]')}})"
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
Format as a timeline (newest first): timestamp, operation, action, requester,
|
|
212
|
+
approver, rationale.`), cmd(t, 'health', `Probe this ${t}'s health and report it to the Jungle`, `\`\`\`bash
|
|
213
|
+
npx -y @nfinitmonkeys/clan health
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
Probes come from \`.jungle/config.yaml\` under \`health:\`. Summarize each check
|
|
217
|
+
(ok / degraded / down) after it reports.`));
|
|
218
|
+
}
|
|
219
|
+
return files;
|
|
220
|
+
}
|
|
221
|
+
// ─── brain scaffold (canonical layout — <brainRoot>/wiki + .brain-meta.json) ─
|
|
222
|
+
/** Where the brain lives, per tier (matches the fleet's clan-ready layout so
|
|
223
|
+
* the engine never puts .brain/ where a district/jungle expects its vault). */
|
|
224
|
+
export function brainRoot(i) {
|
|
225
|
+
if (i.tier === 'district')
|
|
226
|
+
return '.district/brain';
|
|
227
|
+
if (i.tier === 'jungle')
|
|
228
|
+
return `.brains/${i.alphaName.toLowerCase()}`;
|
|
229
|
+
return '.brain';
|
|
230
|
+
}
|
|
231
|
+
export function brainScaffold(i, today) {
|
|
232
|
+
const root = brainRoot(i);
|
|
233
|
+
const files = [];
|
|
234
|
+
const push = (rel, content, managed = true) => files.push({ path: `${root}/${rel}`, content, managed });
|
|
235
|
+
push('.brain-meta.json', JSON.stringify({
|
|
236
|
+
schemaVersion: '1.0',
|
|
237
|
+
layoutVersion: LAYOUT_VERSION,
|
|
238
|
+
generatedBy: `${GENERATED_BY}@${ENGINE_VERSION}`,
|
|
239
|
+
alphaId: i.alphaId ?? null,
|
|
240
|
+
createdAt: today,
|
|
241
|
+
}, null, 2) + '\n');
|
|
242
|
+
push('CLAUDE.md', `# ${i.alphaName}'s Brain — ${i.name} Memory Vault\n\nPersistent, compounding memory. Read \`wiki/hot.md\` first.\n\n| Field | Value |\n|---|---|\n| alphaId | \`${i.alphaId ?? '(unset)'}\` |\n| alphaName | ${i.alphaName} |\n| node | ${i.name} |\n| tier | ${i.tier} |\n`);
|
|
243
|
+
push('wiki/identity.md', `---\nname: Identity — ${i.alphaName}\ndescription: Who I am\ntype: identity\nupdated: ${today}\n---\n\n# ${i.alphaName} — Alpha of ${i.name}\n\n| Field | Value |\n|---|---|\n| alphaId | \`${i.alphaId ?? '(unset)'}\` |\n| alphaName | **${i.alphaName}** |\n| node | ${i.name} |\n| geoTier | ${i.tier} |\n\n## Mission\n\n${i.description || `${i.alphaName} is the Alpha of ${i.name}.`}\n`);
|
|
244
|
+
push('wiki/hot.md', `---\nname: Hot Cache\ndescription: Recent context — read at session start\ntype: hot\nupdated: ${today}\n---\n\n# Hot Cache\n\nBrain scaffolded ${today} by ${GENERATED_BY}. Observations and decisions accumulate here.\n`);
|
|
245
|
+
push('wiki/index.md', `---\nname: Index\ntype: index\nupdated: ${today}\n---\n\n# Index\n\n- [[identity]] · [[hot]] · [[log]]\n- entities/ · concepts/ · decisions/ · observations/ · goals/ · relationships/ · questions/ · sources/\n`);
|
|
246
|
+
push('wiki/log.md', `---\nname: Action Log\ntype: log\nupdated: ${today}\n---\n\n# Action Log\n\n## ${today}\n- Brain vault scaffolded by ${GENERATED_BY}.\n`);
|
|
247
|
+
for (const cat of ['entities', 'concepts', 'decisions', 'observations', 'goals', 'relationships', 'questions', 'sources']) {
|
|
248
|
+
push(`wiki/${cat}/_index.md`, `---\nname: ${cat} — Index\ntype: domain-index\nupdated: ${today}\n---\n\n# ${cat}\n\n*Populates as ${i.alphaName} accumulates ${cat}.*\n`);
|
|
249
|
+
}
|
|
250
|
+
return files;
|
|
251
|
+
}
|
|
252
|
+
// ─── cross-platform SessionStart hook (Node, not bash) ──────────────────────
|
|
253
|
+
/** The command wired into .claude/settings.json SessionStart. Relative (hooks
|
|
254
|
+
* run at project root) + the script self-locates via import.meta, so it is
|
|
255
|
+
* cwd- and OS-independent. Tier-aware because the brain root is. */
|
|
256
|
+
export function hookCommand(i) {
|
|
257
|
+
return `node ${brainRoot(i)}/scripts/load-brain.mjs`;
|
|
258
|
+
}
|
|
259
|
+
/** Regex matching any load-brain hook command (bash or node, any brain root) —
|
|
260
|
+
* used to detect + migrate a previously-wired hook. */
|
|
261
|
+
export const LOAD_BRAIN_HOOK_RE = /load-brain\.(mjs|sh)/;
|
|
262
|
+
export function loadBrainScript(i) {
|
|
263
|
+
const header = `# ${i.alphaName}'s brain — loaded at session start`;
|
|
264
|
+
const sub = `Persistent memory for ${i.name} (${i.tier}). Authoritative on identity, recent context, standing goals.`;
|
|
265
|
+
return {
|
|
266
|
+
path: `${brainRoot(i)}/scripts/load-brain.mjs`,
|
|
267
|
+
managed: false, // a runnable script; content-compare handles idempotency
|
|
268
|
+
chmod: 0o755,
|
|
269
|
+
content: `#!/usr/bin/env node
|
|
270
|
+
// load-brain.mjs — hydrate ${i.alphaName}'s brain at session start (${GENERATED_BY}).
|
|
271
|
+
// Cross-platform (macOS/Windows/Linux). Self-noops (exit 0, no output) with no brain.
|
|
272
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
273
|
+
import { fileURLToPath } from 'node:url';
|
|
274
|
+
import { dirname, join } from 'node:path';
|
|
275
|
+
|
|
276
|
+
const wiki = join(dirname(dirname(fileURLToPath(import.meta.url))), 'wiki');
|
|
277
|
+
if (!existsSync(wiki)) process.exit(0);
|
|
278
|
+
|
|
279
|
+
const out = [${JSON.stringify(header)}, '', ${JSON.stringify(sub)}, '---'];
|
|
280
|
+
for (const f of ['identity.md', 'hot.md']) {
|
|
281
|
+
const p = join(wiki, f);
|
|
282
|
+
if (existsSync(p)) { out.push('## From wiki/' + f, '', readFileSync(p, 'utf-8'), '---'); }
|
|
283
|
+
}
|
|
284
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
285
|
+
const obs = join(wiki, 'observations', today + '.md');
|
|
286
|
+
if (existsSync(obs)) { out.push("## Today's observations", '', readFileSync(obs, 'utf-8')); }
|
|
287
|
+
process.stdout.write(out.join('\\n') + '\\n');
|
|
288
|
+
process.exit(0);
|
|
289
|
+
`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ─── the full managed .claude surface for an identity ───────────────────────
|
|
293
|
+
/**
|
|
294
|
+
* Every managed file the engine authors into .claude/ + .brain/ for one
|
|
295
|
+
* identity, EXCEPT the merge-based artifacts (.mcp.json, settings.json hook,
|
|
296
|
+
* CLAUDE.md) which reconcile handles specially because they touch user content.
|
|
297
|
+
*/
|
|
298
|
+
export function claudeSurface(i) {
|
|
299
|
+
return [awarenessSkill(i), ...commandPack(i), loadBrainScript(i)];
|
|
300
|
+
}
|
|
301
|
+
/** All tier command directories the engine owns — used to prune stale packs
|
|
302
|
+
* from a different tier (e.g. a repo re-tiered clan → district). */
|
|
303
|
+
export const ALL_TIER_DIRS = ['clan', 'district', 'jungle', 'biome'];
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contract for the clan-engine. Every module imports from here; nothing
|
|
3
|
+
* else is cross-imported, so the pieces stay independently testable.
|
|
4
|
+
*/
|
|
5
|
+
/** Engine + layout versions. LAYOUT_VERSION is stamped into every generated
|
|
6
|
+
* file's marker so a future engine can tell "my old output" from "my current
|
|
7
|
+
* output" and run migrations. Bump LAYOUT_VERSION when the template SHAPE
|
|
8
|
+
* changes (a path moves, a format changes); bump ENGINE_VERSION freely. */
|
|
9
|
+
export declare const ENGINE_VERSION = "0.1.0";
|
|
10
|
+
export declare const LAYOUT_VERSION = 1;
|
|
11
|
+
/** The marker key written into generated-file frontmatter / headers. */
|
|
12
|
+
export declare const GENERATED_BY = "clan-engine";
|
|
13
|
+
export type Tier = 'clan' | 'district' | 'jungle' | 'biome';
|
|
14
|
+
export type Mode = 'connected' | 'standalone';
|
|
15
|
+
/** How the tier was decided (declared always wins over inferred). */
|
|
16
|
+
export type TierSource = 'flag' | 'marker' | 'district-dir' | 'brains-dir' | 'default';
|
|
17
|
+
export interface ClanIdentity {
|
|
18
|
+
/** Clan / node display name. */
|
|
19
|
+
name: string;
|
|
20
|
+
alphaName: string;
|
|
21
|
+
alphaId?: string;
|
|
22
|
+
description: string;
|
|
23
|
+
tier: Tier;
|
|
24
|
+
mode: Mode;
|
|
25
|
+
/** connected-mode credentials (drive .mcp.json). */
|
|
26
|
+
jungle?: {
|
|
27
|
+
apiUrl: string;
|
|
28
|
+
alphaId: string;
|
|
29
|
+
apiKey: string;
|
|
30
|
+
humanEmail?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** A file the templates want to exist, before reconciliation. */
|
|
34
|
+
export interface GeneratedFile {
|
|
35
|
+
path: string;
|
|
36
|
+
content: string;
|
|
37
|
+
/** true when this file carries a clan-engine marker (so reconciliation can
|
|
38
|
+
* tell our output from a user's hand-written file at the same path). */
|
|
39
|
+
managed: boolean;
|
|
40
|
+
/** POSIX chmod to apply after write (e.g. 0o755 for scripts). */
|
|
41
|
+
chmod?: number;
|
|
42
|
+
}
|
|
43
|
+
export type ActionKind = 'create' | 'update' | 'backup-replace' | 'merge' | 'append' | 'remove' | 'skip';
|
|
44
|
+
export interface PlanAction {
|
|
45
|
+
kind: ActionKind;
|
|
46
|
+
path: string;
|
|
47
|
+
content?: string;
|
|
48
|
+
note: string;
|
|
49
|
+
chmod?: number;
|
|
50
|
+
/** why this action was chosen — surfaced in dry-run output. */
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface Plan {
|
|
54
|
+
identity: ClanIdentity;
|
|
55
|
+
actions: PlanAction[];
|
|
56
|
+
/** repo root the plan targets. */
|
|
57
|
+
repo: string;
|
|
58
|
+
}
|
|
59
|
+
/** One inventoried Claude Code asset the user already has. */
|
|
60
|
+
export interface InventoryItem {
|
|
61
|
+
kind: 'agent' | 'skill' | 'command' | 'hook';
|
|
62
|
+
name: string;
|
|
63
|
+
path: string;
|
|
64
|
+
scope: 'project' | 'global';
|
|
65
|
+
description?: string;
|
|
66
|
+
}
|
|
67
|
+
export type NodeState = 'fresh' | 'partial' | 'current' | 'legacy' | 'drifted';
|
|
68
|
+
export interface McpServerInfo {
|
|
69
|
+
name: string;
|
|
70
|
+
isJungle: boolean;
|
|
71
|
+
misnamed: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface InspectResult {
|
|
74
|
+
repo: string;
|
|
75
|
+
os: string;
|
|
76
|
+
isGit: boolean;
|
|
77
|
+
tier: Tier;
|
|
78
|
+
tierSource: TierSource;
|
|
79
|
+
mode: Mode;
|
|
80
|
+
identity?: ClanIdentity;
|
|
81
|
+
/** .jungle/config.yaml present + parsed? */
|
|
82
|
+
hasConfig: boolean;
|
|
83
|
+
configError?: string;
|
|
84
|
+
/** .mcp.json present? which servers? */
|
|
85
|
+
hasMcp: boolean;
|
|
86
|
+
mcpServers: McpServerInfo[];
|
|
87
|
+
/** the user's existing Claude Code assets (project + global). */
|
|
88
|
+
inventory: InventoryItem[];
|
|
89
|
+
/** brain vault present + its layout version (from .brain-meta.json). */
|
|
90
|
+
hasBrain: boolean;
|
|
91
|
+
brainLayoutVersion?: number;
|
|
92
|
+
/** the reconciled state classification. */
|
|
93
|
+
state: NodeState;
|
|
94
|
+
/** actionable notes for the wizard / caller. */
|
|
95
|
+
notes: string[];
|
|
96
|
+
}
|
|
97
|
+
/** One recorded mutation, enabling undo. */
|
|
98
|
+
export interface JournalOp {
|
|
99
|
+
kind: ActionKind;
|
|
100
|
+
path: string;
|
|
101
|
+
/** backup location (repo-relative) if the prior file was saved. */
|
|
102
|
+
backup?: string;
|
|
103
|
+
/** true if this op CREATED the file (undo = delete). */
|
|
104
|
+
created?: boolean;
|
|
105
|
+
}
|
|
106
|
+
export interface Journal {
|
|
107
|
+
engineVersion: string;
|
|
108
|
+
layoutVersion: number;
|
|
109
|
+
/** stamped by the caller (the engine forbids Date.now for determinism in
|
|
110
|
+
* tests; the CLI passes a timestamp). */
|
|
111
|
+
at: string;
|
|
112
|
+
repo: string;
|
|
113
|
+
identity: ClanIdentity;
|
|
114
|
+
ops: JournalOp[];
|
|
115
|
+
}
|
|
116
|
+
export interface ApplyResult {
|
|
117
|
+
applied: PlanAction[];
|
|
118
|
+
journalPath?: string;
|
|
119
|
+
backupDir?: string;
|
|
120
|
+
}
|
|
121
|
+
export interface DoctorCheck {
|
|
122
|
+
name: string;
|
|
123
|
+
ok: boolean;
|
|
124
|
+
detail: string;
|
|
125
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contract for the clan-engine. Every module imports from here; nothing
|
|
3
|
+
* else is cross-imported, so the pieces stay independently testable.
|
|
4
|
+
*/
|
|
5
|
+
/** Engine + layout versions. LAYOUT_VERSION is stamped into every generated
|
|
6
|
+
* file's marker so a future engine can tell "my old output" from "my current
|
|
7
|
+
* output" and run migrations. Bump LAYOUT_VERSION when the template SHAPE
|
|
8
|
+
* changes (a path moves, a format changes); bump ENGINE_VERSION freely. */
|
|
9
|
+
export const ENGINE_VERSION = '0.1.0';
|
|
10
|
+
export const LAYOUT_VERSION = 1;
|
|
11
|
+
/** The marker key written into generated-file frontmatter / headers. */
|
|
12
|
+
export const GENERATED_BY = 'clan-engine';
|
package/dist/undo.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* undo — reverse an apply() run from its journal. Restores every backed-up file
|
|
3
|
+
* to its prior bytes and deletes every file the run created. The inverse of
|
|
4
|
+
* apply, so "try the clan and cleanly walk away" actually works.
|
|
5
|
+
*/
|
|
6
|
+
export interface UndoResult {
|
|
7
|
+
restored: string[];
|
|
8
|
+
removed: string[];
|
|
9
|
+
journal: string;
|
|
10
|
+
}
|
|
11
|
+
/** Undo the run described by a journal file (repo-relative or absolute path). */
|
|
12
|
+
export declare function undo(repo: string, journalPath: string): UndoResult;
|
|
13
|
+
/** List journals present in a repo (newest name last — they are timestamp-named). */
|
|
14
|
+
export declare function listJournals(repo: string): string[];
|
package/dist/undo.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* undo — reverse an apply() run from its journal. Restores every backed-up file
|
|
3
|
+
* to its prior bytes and deletes every file the run created. The inverse of
|
|
4
|
+
* apply, so "try the clan and cleanly walk away" actually works.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync, rmSync, mkdirSync, cpSync, readdirSync } from 'fs';
|
|
7
|
+
import { join, dirname, isAbsolute } from 'path';
|
|
8
|
+
/** Undo the run described by a journal file (repo-relative or absolute path). */
|
|
9
|
+
export function undo(repo, journalPath) {
|
|
10
|
+
const jAbs = isAbsolute(journalPath) ? journalPath : join(repo, journalPath);
|
|
11
|
+
const journal = JSON.parse(readFileSync(jAbs, 'utf-8'));
|
|
12
|
+
const restored = [];
|
|
13
|
+
const removed = [];
|
|
14
|
+
// Reverse order so appends/creates unwind cleanly.
|
|
15
|
+
for (const op of [...journal.ops].reverse()) {
|
|
16
|
+
const abs = join(repo, op.path);
|
|
17
|
+
if (op.backup) {
|
|
18
|
+
const bkpAbs = join(repo, op.backup);
|
|
19
|
+
if (existsSync(bkpAbs)) {
|
|
20
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
21
|
+
rmSync(abs, { recursive: true, force: true });
|
|
22
|
+
cpSync(bkpAbs, abs, { recursive: true });
|
|
23
|
+
restored.push(op.path);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (op.created) {
|
|
27
|
+
rmSync(abs, { recursive: true, force: true });
|
|
28
|
+
removed.push(op.path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { restored, removed, journal: journalPath };
|
|
32
|
+
}
|
|
33
|
+
/** List journals present in a repo (newest name last — they are timestamp-named). */
|
|
34
|
+
export function listJournals(repo) {
|
|
35
|
+
const dir = join(repo, '.claude', '.clan-engine');
|
|
36
|
+
try {
|
|
37
|
+
return readdirSync(dir).filter(f => f.startsWith('journal-') && f.endsWith('.json')).sort()
|
|
38
|
+
.map(f => join('.claude', '.clan-engine', f));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nfinitmonkeys/clan-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic setup engine for Nfinit Monkeys clans — the single template + reconciliation authority behind the published CLIs and the internal fleet tooling. Idempotent, dry-run-by-default, non-destructive.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clan-engine": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "npm run build && node test/engine.test.mjs",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"nfinitmonkeys",
|
|
25
|
+
"clan",
|
|
26
|
+
"jungle",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"mcp"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.20.1",
|
|
36
|
+
"typescript": "^5.6.3"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"yaml": "^2.9.0"
|
|
40
|
+
}
|
|
41
|
+
}
|