@wipcomputer/wip-ldm-os 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/SKILL.md +162 -0
- package/bin/ldm.mjs +671 -0
- package/bin/scaffold.sh +97 -0
- package/catalog.json +95 -0
- package/lib/deploy.mjs +697 -0
- package/lib/detect.mjs +130 -0
- package/package.json +35 -0
- package/src/boot/README.md +61 -0
- package/src/boot/boot-config.json +65 -0
- package/src/boot/boot-hook.mjs +242 -0
- package/src/boot/install-cli.mjs +27 -0
- package/src/boot/installer.mjs +279 -0
- package/templates/cc/CONTEXT.md +18 -0
- package/templates/cc/REFERENCE.md +22 -0
- package/templates/cc/config.json +14 -0
- package/templates/config.json +5 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// LDM OS Boot Hook Installer
|
|
2
|
+
// Follows Memory Crystal installer.ts pattern: detect state, deploy, configure, register.
|
|
3
|
+
// Pure ESM, zero dependencies. Never nuke and replace.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const HOME = homedir();
|
|
11
|
+
const LDM_ROOT = join(HOME, '.ldm');
|
|
12
|
+
const BOOT_DIR = join(LDM_ROOT, 'shared', 'boot');
|
|
13
|
+
const CC_SETTINGS = join(HOME, '.claude', 'settings.json');
|
|
14
|
+
const REGISTRY = join(LDM_ROOT, 'extensions', 'registry.json');
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// ── Helpers ──
|
|
19
|
+
|
|
20
|
+
function readJSON(path) {
|
|
21
|
+
try {
|
|
22
|
+
if (existsSync(path)) return JSON.parse(readFileSync(path, 'utf-8'));
|
|
23
|
+
} catch {}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeJSON(path, data) {
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readVersion(pkgPath) {
|
|
33
|
+
const pkg = readJSON(pkgPath);
|
|
34
|
+
return pkg?.version || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getRepoRoot() {
|
|
38
|
+
// Walk up from src/boot/ to find package.json
|
|
39
|
+
let dir = __dirname;
|
|
40
|
+
for (let i = 0; i < 5; i++) {
|
|
41
|
+
const pkgPath = join(dir, 'package.json');
|
|
42
|
+
if (existsSync(pkgPath)) {
|
|
43
|
+
const pkg = readJSON(pkgPath);
|
|
44
|
+
if (pkg?.name === '@wipcomputer/ldm-os') return dir;
|
|
45
|
+
}
|
|
46
|
+
dir = dirname(dir);
|
|
47
|
+
}
|
|
48
|
+
// Fallback: two levels up from src/boot/
|
|
49
|
+
return dirname(dirname(__dirname));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Install State Detection ──
|
|
53
|
+
|
|
54
|
+
export function detectInstallState() {
|
|
55
|
+
const repoRoot = getRepoRoot();
|
|
56
|
+
const repoVersion = readVersion(join(repoRoot, 'package.json')) || '0.0.0';
|
|
57
|
+
const installedVersion = readVersion(join(BOOT_DIR, 'package.json'));
|
|
58
|
+
|
|
59
|
+
const hookDeployed = existsSync(join(BOOT_DIR, 'boot-hook.mjs'));
|
|
60
|
+
const configDeployed = existsSync(join(BOOT_DIR, 'boot-config.json'));
|
|
61
|
+
|
|
62
|
+
// Check if SessionStart hook is configured in settings.json
|
|
63
|
+
let hookConfigured = false;
|
|
64
|
+
const settings = readJSON(CC_SETTINGS);
|
|
65
|
+
if (settings?.hooks?.SessionStart) {
|
|
66
|
+
const entries = settings.hooks.SessionStart;
|
|
67
|
+
if (Array.isArray(entries)) {
|
|
68
|
+
hookConfigured = entries.some((entry) => {
|
|
69
|
+
const hooks = entry?.hooks;
|
|
70
|
+
if (!Array.isArray(hooks)) return false;
|
|
71
|
+
return hooks.some((h) => h?.command?.includes('boot-hook') || h?.command?.includes('shared/boot'));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const needsUpdate = installedVersion !== null && installedVersion !== repoVersion;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
bootDirExists: existsSync(BOOT_DIR),
|
|
80
|
+
hookDeployed,
|
|
81
|
+
hookConfigured,
|
|
82
|
+
configDeployed,
|
|
83
|
+
installedVersion,
|
|
84
|
+
repoVersion,
|
|
85
|
+
needsUpdate,
|
|
86
|
+
isFresh: installedVersion === null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Deployment ──
|
|
91
|
+
|
|
92
|
+
export function deployToLdm(options = {}) {
|
|
93
|
+
const repoRoot = getRepoRoot();
|
|
94
|
+
const srcBoot = join(repoRoot, 'src', 'boot');
|
|
95
|
+
const steps = [];
|
|
96
|
+
|
|
97
|
+
// Create boot directory
|
|
98
|
+
if (!existsSync(BOOT_DIR)) {
|
|
99
|
+
mkdirSync(BOOT_DIR, { recursive: true });
|
|
100
|
+
steps.push('Created ~/.ldm/shared/boot/');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Always copy boot-hook.mjs (code updates always deploy)
|
|
104
|
+
const hookSrc = join(srcBoot, 'boot-hook.mjs');
|
|
105
|
+
if (existsSync(hookSrc)) {
|
|
106
|
+
copyFileSync(hookSrc, join(BOOT_DIR, 'boot-hook.mjs'));
|
|
107
|
+
steps.push('Deployed boot-hook.mjs');
|
|
108
|
+
} else {
|
|
109
|
+
steps.push('MISSING: src/boot/boot-hook.mjs (cannot deploy hook)');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Copy package.json for version tracking (always update)
|
|
113
|
+
const pkgSrc = join(repoRoot, 'package.json');
|
|
114
|
+
if (existsSync(pkgSrc)) {
|
|
115
|
+
copyFileSync(pkgSrc, join(BOOT_DIR, 'package.json'));
|
|
116
|
+
steps.push('Updated package.json (version tracking)');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// boot-config.json: only deploy if not present (user customizations survive)
|
|
120
|
+
const configDst = join(BOOT_DIR, 'boot-config.json');
|
|
121
|
+
const configSrc = join(srcBoot, 'boot-config.json');
|
|
122
|
+
if (!existsSync(configDst)) {
|
|
123
|
+
if (existsSync(configSrc)) {
|
|
124
|
+
copyFileSync(configSrc, configDst);
|
|
125
|
+
steps.push('Seeded boot-config.json (new install)');
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
steps.push('boot-config.json exists (preserved, not overwritten)');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return steps;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Hook Configuration ──
|
|
135
|
+
|
|
136
|
+
export function configureSessionStartHook() {
|
|
137
|
+
const hookCommand = `node ${join(BOOT_DIR, 'boot-hook.mjs')}`;
|
|
138
|
+
|
|
139
|
+
let settings = readJSON(CC_SETTINGS) || {};
|
|
140
|
+
|
|
141
|
+
// Ensure hooks.SessionStart exists
|
|
142
|
+
if (!settings.hooks) settings.hooks = {};
|
|
143
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
144
|
+
|
|
145
|
+
// Find existing boot-hook entry
|
|
146
|
+
const existingIdx = settings.hooks.SessionStart.findIndex((entry) => {
|
|
147
|
+
const hooks = entry?.hooks;
|
|
148
|
+
if (!Array.isArray(hooks)) return false;
|
|
149
|
+
return hooks.some((h) =>
|
|
150
|
+
h?.command?.includes('boot-hook') || h?.command?.includes('shared/boot')
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const hookEntry = {
|
|
155
|
+
matcher: '*',
|
|
156
|
+
hooks: [{
|
|
157
|
+
type: 'command',
|
|
158
|
+
command: hookCommand,
|
|
159
|
+
timeout: 15,
|
|
160
|
+
}],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (existingIdx >= 0) {
|
|
164
|
+
// Update in place
|
|
165
|
+
settings.hooks.SessionStart[existingIdx] = hookEntry;
|
|
166
|
+
return 'SessionStart hook updated in settings.json';
|
|
167
|
+
} else {
|
|
168
|
+
// Append
|
|
169
|
+
settings.hooks.SessionStart.push(hookEntry);
|
|
170
|
+
writeJSON(CC_SETTINGS, settings);
|
|
171
|
+
return 'SessionStart hook added to settings.json';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Registry ──
|
|
176
|
+
|
|
177
|
+
export function updateRegistry(version) {
|
|
178
|
+
let registry = readJSON(REGISTRY) || { _format: 'v1', extensions: {} };
|
|
179
|
+
|
|
180
|
+
registry.extensions['ldm-os-boot'] = {
|
|
181
|
+
name: 'ldm-os-boot',
|
|
182
|
+
version,
|
|
183
|
+
source: getRepoRoot(),
|
|
184
|
+
interfaces: ['claude-code-hook'],
|
|
185
|
+
ldmPath: BOOT_DIR,
|
|
186
|
+
updatedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
writeJSON(REGISTRY, registry);
|
|
190
|
+
return 'Registry updated';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Orchestrator ──
|
|
194
|
+
|
|
195
|
+
export function runInstallOrUpdate(options = {}) {
|
|
196
|
+
const state = detectInstallState();
|
|
197
|
+
const steps = [];
|
|
198
|
+
const dryRun = options.dryRun || false;
|
|
199
|
+
|
|
200
|
+
// Check if already up to date
|
|
201
|
+
if (!state.isFresh && !state.needsUpdate) {
|
|
202
|
+
return {
|
|
203
|
+
action: 'up-to-date',
|
|
204
|
+
version: state.repoVersion,
|
|
205
|
+
steps: [`Already at v${state.repoVersion}. Nothing to do.`],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const action = state.isFresh ? 'installed' : 'updated';
|
|
210
|
+
|
|
211
|
+
if (dryRun) {
|
|
212
|
+
steps.push(`[DRY RUN] Would ${action === 'installed' ? 'install' : 'update'} v${state.repoVersion}`);
|
|
213
|
+
if (state.isFresh) {
|
|
214
|
+
steps.push('[DRY RUN] Would create ~/.ldm/shared/boot/');
|
|
215
|
+
steps.push('[DRY RUN] Would deploy boot-hook.mjs');
|
|
216
|
+
steps.push('[DRY RUN] Would seed boot-config.json');
|
|
217
|
+
} else {
|
|
218
|
+
steps.push(`[DRY RUN] Would update from v${state.installedVersion} to v${state.repoVersion}`);
|
|
219
|
+
steps.push('[DRY RUN] Would update boot-hook.mjs');
|
|
220
|
+
steps.push('[DRY RUN] Would preserve boot-config.json');
|
|
221
|
+
}
|
|
222
|
+
if (!state.hookConfigured) {
|
|
223
|
+
steps.push('[DRY RUN] Would add SessionStart hook to settings.json');
|
|
224
|
+
} else {
|
|
225
|
+
steps.push('[DRY RUN] Would update SessionStart hook in settings.json');
|
|
226
|
+
}
|
|
227
|
+
steps.push('[DRY RUN] Would update registry');
|
|
228
|
+
return { action: 'dry-run', version: state.repoVersion, steps };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 1: Deploy files
|
|
232
|
+
const deploySteps = deployToLdm();
|
|
233
|
+
steps.push(...deploySteps);
|
|
234
|
+
|
|
235
|
+
// Step 2: Configure hook
|
|
236
|
+
try {
|
|
237
|
+
const hookResult = configureSessionStartHook();
|
|
238
|
+
steps.push(hookResult);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
steps.push(`Hook config failed: ${err.message}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Step 3: Update registry
|
|
244
|
+
try {
|
|
245
|
+
const regResult = updateRegistry(state.repoVersion);
|
|
246
|
+
steps.push(regResult);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
steps.push(`Registry update failed: ${err.message}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { action, version: state.repoVersion, steps };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Format helpers ──
|
|
255
|
+
|
|
256
|
+
export function formatStatus(state) {
|
|
257
|
+
const lines = [];
|
|
258
|
+
lines.push('LDM OS Boot Hook Status');
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push(` Boot directory: ${state.bootDirExists ? 'exists' : 'MISSING'}`);
|
|
261
|
+
lines.push(` Hook deployed: ${state.hookDeployed ? 'yes' : 'no'}`);
|
|
262
|
+
lines.push(` Hook configured: ${state.hookConfigured ? 'yes' : 'no'}`);
|
|
263
|
+
lines.push(` Config deployed: ${state.configDeployed ? 'yes' : 'no'}`);
|
|
264
|
+
lines.push(` Installed version: ${state.installedVersion || 'none'}`);
|
|
265
|
+
lines.push(` Repo version: ${state.repoVersion}`);
|
|
266
|
+
lines.push(` Needs update: ${state.needsUpdate ? 'YES' : 'no'}`);
|
|
267
|
+
lines.push(` Fresh install: ${state.isFresh ? 'YES' : 'no'}`);
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatResult(result) {
|
|
272
|
+
const lines = [];
|
|
273
|
+
lines.push(`Action: ${result.action} (v${result.version})`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
for (const step of result.steps) {
|
|
276
|
+
lines.push(` ${step}`);
|
|
277
|
+
}
|
|
278
|
+
return lines.join('\n');
|
|
279
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# CC Context
|
|
2
|
+
|
|
3
|
+
## Right Now
|
|
4
|
+
[First boot from ~/.ldm/agents/cc/. Populate after Dream Weaver recovery.]
|
|
5
|
+
|
|
6
|
+
## Last Session
|
|
7
|
+
[To be populated]
|
|
8
|
+
|
|
9
|
+
## Infrastructure
|
|
10
|
+
- Stop hooks: memory-crystal cc-hook (captures turns to crystal), cc-session-export (exports to markdown)
|
|
11
|
+
- Crystal: agent_id "claude-code" chunks searchable via crystal_search
|
|
12
|
+
- Daily log: ~/.ldm/agents/cc/memory/daily/ (auto-populated by hook)
|
|
13
|
+
|
|
14
|
+
## Broken / Blocked
|
|
15
|
+
[To be populated]
|
|
16
|
+
|
|
17
|
+
## Coming Next
|
|
18
|
+
[To be populated]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# CC Reference
|
|
2
|
+
|
|
3
|
+
## Key Documents
|
|
4
|
+
- Full history narrative: staff/Parker/Claude Code - Mini/documents/cc-full-history.md
|
|
5
|
+
- Dropped threads: staff/Parker/Claude Code - Mini/documents/cc-TODO-from-history.md
|
|
6
|
+
- Memory architecture proposal: staff/Parker/Claude Code - Mini/documents/cc-how-we-remember.md
|
|
7
|
+
- Dream Weaver Protocol: repos/dream-weaver-protocol/IMPLEMENTATION.md
|
|
8
|
+
- LDM OS README: repos/wip-ldm-os/README.md
|
|
9
|
+
|
|
10
|
+
## Session Exports
|
|
11
|
+
staff/Parker/Claude Code - Mini/documents/sessions/
|
|
12
|
+
|
|
13
|
+
## Journals
|
|
14
|
+
~/.ldm/agents/cc/memory/journals/
|
|
15
|
+
|
|
16
|
+
## Crystal Search
|
|
17
|
+
Use crystal_search with agent_id: "claude-code" for semantic search across all CC turns.
|
|
18
|
+
|
|
19
|
+
## Lesa's State (read-only)
|
|
20
|
+
- SHARED-CONTEXT.md: ~/.openclaw/workspace/SHARED-CONTEXT.md
|
|
21
|
+
- Daily logs: ~/.openclaw/workspace/memory/YYYY-MM-DD.md
|
|
22
|
+
- Send messages: lesa_send_message MCP tool
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agent": "cc",
|
|
3
|
+
"harness": "claude-code-cli",
|
|
4
|
+
"model": "claude-opus-4-6",
|
|
5
|
+
"created": "2026-02-18",
|
|
6
|
+
"memoryPaths": {
|
|
7
|
+
"daily": "~/.ldm/agents/cc/memory/daily",
|
|
8
|
+
"journals": "~/.ldm/agents/cc/memory/journals"
|
|
9
|
+
},
|
|
10
|
+
"dreamWeaver": {
|
|
11
|
+
"lastConsolidation": null,
|
|
12
|
+
"schedule": "weekly"
|
|
13
|
+
}
|
|
14
|
+
}
|