@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
package/lib/detect.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/detect.mjs
|
|
3
|
+
* Interface detection logic. Scans a repo and reports which interfaces it exposes.
|
|
4
|
+
* Adapted from wip-universal-installer/detect.mjs. Zero dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { join, basename } from 'node:path';
|
|
9
|
+
|
|
10
|
+
function readJSON(path) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect all interfaces in a repo.
|
|
20
|
+
* Returns { interfaces, pkg } where interfaces is an object keyed by interface type.
|
|
21
|
+
*/
|
|
22
|
+
export function detectInterfaces(repoPath) {
|
|
23
|
+
const interfaces = {};
|
|
24
|
+
const pkg = readJSON(join(repoPath, 'package.json'));
|
|
25
|
+
|
|
26
|
+
// 1. CLI: package.json has bin entry
|
|
27
|
+
if (pkg?.bin) {
|
|
28
|
+
interfaces.cli = { bin: pkg.bin, name: pkg.name };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Module: package.json has main or exports
|
|
32
|
+
if (pkg?.main || pkg?.exports) {
|
|
33
|
+
interfaces.module = { main: pkg.main || pkg.exports };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. MCP Server: mcp-server.mjs/js/ts or dist/mcp-server.js
|
|
37
|
+
const mcpFiles = ['mcp-server.mjs', 'mcp-server.js', 'mcp-server.ts', 'dist/mcp-server.js'];
|
|
38
|
+
for (const f of mcpFiles) {
|
|
39
|
+
if (existsSync(join(repoPath, f))) {
|
|
40
|
+
interfaces.mcp = { file: f, name: pkg?.name || basename(repoPath) };
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 4. OpenClaw Plugin: openclaw.plugin.json exists
|
|
46
|
+
const ocPlugin = join(repoPath, 'openclaw.plugin.json');
|
|
47
|
+
if (existsSync(ocPlugin)) {
|
|
48
|
+
interfaces.openclaw = { config: readJSON(ocPlugin), path: ocPlugin };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 5. Skill: SKILL.md exists
|
|
52
|
+
if (existsSync(join(repoPath, 'SKILL.md'))) {
|
|
53
|
+
interfaces.skill = { path: join(repoPath, 'SKILL.md') };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 6. Claude Code Hook: guard.mjs or claudeCode.hook in package.json
|
|
57
|
+
if (pkg?.claudeCode?.hook) {
|
|
58
|
+
interfaces.claudeCodeHook = pkg.claudeCode.hook;
|
|
59
|
+
} else if (existsSync(join(repoPath, 'guard.mjs'))) {
|
|
60
|
+
interfaces.claudeCodeHook = {
|
|
61
|
+
event: 'PreToolUse',
|
|
62
|
+
matcher: 'Edit|Write',
|
|
63
|
+
command: `node "${join(repoPath, 'guard.mjs')}"`,
|
|
64
|
+
timeout: 5,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { interfaces, pkg };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Describe detected interfaces as a human-readable summary.
|
|
73
|
+
*/
|
|
74
|
+
export function describeInterfaces(interfaces) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
const names = Object.keys(interfaces);
|
|
77
|
+
|
|
78
|
+
if (names.length === 0) {
|
|
79
|
+
return 'No interfaces detected.';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (interfaces.cli) {
|
|
83
|
+
const bins = typeof interfaces.cli.bin === 'string' ? [interfaces.cli.name] : Object.keys(interfaces.cli.bin);
|
|
84
|
+
lines.push(`CLI: ${bins.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
if (interfaces.module) lines.push(`Module: ${JSON.stringify(interfaces.module.main)}`);
|
|
87
|
+
if (interfaces.mcp) lines.push(`MCP Server: ${interfaces.mcp.file}`);
|
|
88
|
+
if (interfaces.openclaw) lines.push(`OpenClaw Plugin: ${interfaces.openclaw.config?.name || 'detected'}`);
|
|
89
|
+
if (interfaces.skill) lines.push(`Skill: SKILL.md`);
|
|
90
|
+
if (interfaces.claudeCodeHook) lines.push(`Claude Code Hook: ${interfaces.claudeCodeHook.event || 'PreToolUse'}`);
|
|
91
|
+
|
|
92
|
+
return `${names.length} interface(s): ${names.join(', ')}\n${lines.map(l => ` ${l}`).join('\n')}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detect if a repo is a toolbox (has tools/ subdirectories with package.json).
|
|
97
|
+
* Returns array of { name, path } for each sub-tool, or empty array if not a toolbox.
|
|
98
|
+
*/
|
|
99
|
+
export function detectToolbox(repoPath) {
|
|
100
|
+
const toolsDir = join(repoPath, 'tools');
|
|
101
|
+
if (!existsSync(toolsDir)) return [];
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const entries = readdirSync(toolsDir, { withFileTypes: true });
|
|
105
|
+
return entries
|
|
106
|
+
.filter(e => e.isDirectory() && existsSync(join(toolsDir, e.name, 'package.json')))
|
|
107
|
+
.map(e => ({ name: e.name, path: join(toolsDir, e.name) }));
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Detect interfaces and return a structured JSON-serializable result.
|
|
115
|
+
*/
|
|
116
|
+
export function detectInterfacesJSON(repoPath) {
|
|
117
|
+
const { interfaces, pkg } = detectInterfaces(repoPath);
|
|
118
|
+
return {
|
|
119
|
+
repo: basename(repoPath),
|
|
120
|
+
package: pkg?.name || null,
|
|
121
|
+
version: pkg?.version || null,
|
|
122
|
+
interfaces: Object.fromEntries(
|
|
123
|
+
Object.entries(interfaces).map(([type, info]) => [type, {
|
|
124
|
+
detected: true,
|
|
125
|
+
...info,
|
|
126
|
+
}])
|
|
127
|
+
),
|
|
128
|
+
interfaceCount: Object.keys(interfaces).length,
|
|
129
|
+
};
|
|
130
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wipcomputer/wip-ldm-os",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
|
+
"main": "src/boot/boot-hook.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ldm": "./bin/ldm.mjs",
|
|
9
|
+
"wip-ldm-os": "./bin/ldm.mjs",
|
|
10
|
+
"ldm-scaffold": "./bin/scaffold.sh",
|
|
11
|
+
"ldm-boot-install": "./src/boot/install-cli.mjs"
|
|
12
|
+
},
|
|
13
|
+
"claudeCode": {
|
|
14
|
+
"hook": {
|
|
15
|
+
"event": "SessionStart",
|
|
16
|
+
"matcher": "*",
|
|
17
|
+
"command": "node /Users/lesa/.ldm/shared/boot/boot-hook.mjs",
|
|
18
|
+
"timeout": 15
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/",
|
|
23
|
+
"lib/",
|
|
24
|
+
"bin/",
|
|
25
|
+
"templates/",
|
|
26
|
+
"catalog.json",
|
|
27
|
+
"SKILL.md"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/wipcomputer/wip-ldm-os-private.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "WIP Computer, Inc.",
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# LDM OS Boot Sequence Hook
|
|
2
|
+
|
|
3
|
+
SessionStart hook for Claude Code. Reads boot files and injects them into the agent's context before the first user message. No dependencies. No build step.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Reads 9 files from the Dream Weaver Boot Sequence (SHARED-CONTEXT.md, SOUL.md, CONTEXT.md, daily logs, journals, repo-locations.md) and injects them as `additionalContext` in the SessionStart response. The agent wakes up already knowing who it is, what's happening, and where things live.
|
|
8
|
+
|
|
9
|
+
## Content Budget
|
|
10
|
+
|
|
11
|
+
~700 lines, ~3,500 tokens. Under 2% of the context window. Large files (journals, daily logs) are truncated. Missing files are skipped gracefully.
|
|
12
|
+
|
|
13
|
+
## Deploy
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mkdir -p ~/.ldm/shared/boot
|
|
17
|
+
cp src/boot/boot-hook.mjs ~/.ldm/shared/boot/
|
|
18
|
+
cp src/boot/boot-config.json ~/.ldm/shared/boot/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then add to `~/.claude/settings.json` inside the `hooks` object:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"SessionStart": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "*",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node /Users/lesa/.ldm/shared/boot/boot-hook.mjs",
|
|
31
|
+
"timeout": 15
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Restart Claude Code to pick up the hook.
|
|
39
|
+
|
|
40
|
+
## Test
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
echo '{"session_id":"test","hook_event_name":"SessionStart"}' | node ~/.ldm/shared/boot/boot-hook.mjs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Should output JSON with `hookSpecificOutput.additionalContext` containing all boot content. Check stderr for the load summary.
|
|
47
|
+
|
|
48
|
+
## Config
|
|
49
|
+
|
|
50
|
+
`boot-config.json` defines paths and limits for each boot step. Uses `~` shorthand (resolved at runtime). To support a different agent (cc-air), deploy a different config alongside the same script.
|
|
51
|
+
|
|
52
|
+
## Adding a Boot Step
|
|
53
|
+
|
|
54
|
+
1. Add an entry to `boot-config.json` under `steps`
|
|
55
|
+
2. Set `path` (single file) or `dir` + `strategy` (directory scan)
|
|
56
|
+
3. Set `stepNumber`, `label`, and optionally `maxLines` and `critical`
|
|
57
|
+
4. The hook picks it up automatically. No code changes needed.
|
|
58
|
+
|
|
59
|
+
## Error Philosophy
|
|
60
|
+
|
|
61
|
+
Partial boot > no boot > blocked session. The hook exits 0 no matter what. Missing files are logged to stderr and skipped. The session always starts.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agentId": "cc-mini",
|
|
3
|
+
"timezone": "America/Los_Angeles",
|
|
4
|
+
"maxTotalLines": 2000,
|
|
5
|
+
"steps": {
|
|
6
|
+
"sharedContext": {
|
|
7
|
+
"path": "~/.openclaw/workspace/SHARED-CONTEXT.md",
|
|
8
|
+
"label": "SHARED-CONTEXT.md",
|
|
9
|
+
"stepNumber": 2,
|
|
10
|
+
"critical": true
|
|
11
|
+
},
|
|
12
|
+
"journals": {
|
|
13
|
+
"dir": "~/Documents/wipcomputer--mac-mini-01/staff/Parker/Claude Code - Mini/documents/journals",
|
|
14
|
+
"label": "Most Recent Journal (Parker)",
|
|
15
|
+
"stepNumber": 3,
|
|
16
|
+
"maxLines": 80,
|
|
17
|
+
"strategy": "most-recent"
|
|
18
|
+
},
|
|
19
|
+
"workspaceDailyLogs": {
|
|
20
|
+
"dir": "~/.openclaw/workspace/memory",
|
|
21
|
+
"label": "Workspace Daily Logs",
|
|
22
|
+
"stepNumber": 4,
|
|
23
|
+
"maxLines": 40,
|
|
24
|
+
"strategy": "daily-logs",
|
|
25
|
+
"days": ["today", "yesterday"]
|
|
26
|
+
},
|
|
27
|
+
"fullHistory": {
|
|
28
|
+
"label": "Full History",
|
|
29
|
+
"stepNumber": 5,
|
|
30
|
+
"reminder": "Read on cold start: staff/Parker/Claude Code - Mini/documents/cc-full-history.md"
|
|
31
|
+
},
|
|
32
|
+
"context": {
|
|
33
|
+
"path": "~/.ldm/agents/cc-mini/CONTEXT.md",
|
|
34
|
+
"label": "CC CONTEXT.md",
|
|
35
|
+
"stepNumber": 6,
|
|
36
|
+
"critical": true
|
|
37
|
+
},
|
|
38
|
+
"soul": {
|
|
39
|
+
"path": "~/.ldm/agents/cc-mini/SOUL.md",
|
|
40
|
+
"label": "CC SOUL.md",
|
|
41
|
+
"stepNumber": 7
|
|
42
|
+
},
|
|
43
|
+
"ccJournals": {
|
|
44
|
+
"dir": "~/.ldm/agents/cc-mini/memory/journals",
|
|
45
|
+
"label": "Most Recent CC Journal",
|
|
46
|
+
"stepNumber": 8,
|
|
47
|
+
"maxLines": 80,
|
|
48
|
+
"strategy": "most-recent"
|
|
49
|
+
},
|
|
50
|
+
"ccDailyLog": {
|
|
51
|
+
"dir": "~/.ldm/agents/cc-mini/memory/daily",
|
|
52
|
+
"label": "CC Daily Log",
|
|
53
|
+
"stepNumber": 9,
|
|
54
|
+
"maxLines": 60,
|
|
55
|
+
"strategy": "daily-logs",
|
|
56
|
+
"days": ["today", "yesterday"]
|
|
57
|
+
},
|
|
58
|
+
"repoLocations": {
|
|
59
|
+
"path": "~/.claude/projects/-Users-lesa--openclaw/memory/repo-locations.md",
|
|
60
|
+
"label": "repo-locations.md",
|
|
61
|
+
"stepNumber": 10,
|
|
62
|
+
"critical": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// LDM OS Boot Sequence Hook
|
|
3
|
+
// SessionStart hook for Claude Code.
|
|
4
|
+
// Reads boot files and injects them into context via additionalContext.
|
|
5
|
+
// Follows guard.mjs pattern: stdin JSON in, stdout JSON out, exit 0 always.
|
|
6
|
+
|
|
7
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join, dirname, resolve } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const HOME = homedir();
|
|
14
|
+
const TAG = '[boot-hook]';
|
|
15
|
+
|
|
16
|
+
function resolvePath(p) {
|
|
17
|
+
if (p.startsWith('~/')) return join(HOME, p.slice(2));
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readFileSafe(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
const resolved = resolvePath(filePath);
|
|
24
|
+
if (!existsSync(resolved)) return null;
|
|
25
|
+
return readFileSync(resolved, 'utf-8');
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listDirSafe(dirPath) {
|
|
32
|
+
try {
|
|
33
|
+
const resolved = resolvePath(dirPath);
|
|
34
|
+
if (!existsSync(resolved)) return [];
|
|
35
|
+
return readdirSync(resolved).sort();
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function truncateTop(content, maxLines) {
|
|
42
|
+
if (!maxLines || !content) return content;
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
if (lines.length <= maxLines) return content;
|
|
45
|
+
return lines.slice(0, maxLines).join('\n') + `\n[... truncated at ${maxLines} lines, ${lines.length} total ...]`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function truncateBottom(content, maxLines) {
|
|
49
|
+
if (!maxLines || !content) return content;
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
if (lines.length <= maxLines) return content;
|
|
52
|
+
return `[... showing last ${maxLines} of ${lines.length} lines ...]\n` + lines.slice(-maxLines).join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getTodayAndYesterday(timezone) {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
58
|
+
timeZone: timezone,
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
month: '2-digit',
|
|
61
|
+
day: '2-digit',
|
|
62
|
+
});
|
|
63
|
+
const today = formatter.format(now);
|
|
64
|
+
|
|
65
|
+
const yesterday = new Date(now);
|
|
66
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
67
|
+
const yesterdayStr = formatter.format(yesterday);
|
|
68
|
+
|
|
69
|
+
return { today, yesterday: yesterdayStr };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findMostRecent(dirPath) {
|
|
73
|
+
const files = listDirSafe(dirPath);
|
|
74
|
+
// Filter to .md files with date-like names, sort descending
|
|
75
|
+
const dated = files
|
|
76
|
+
.filter(f => f.endsWith('.md') && /^\d{4}-\d{2}-\d{2}/.test(f))
|
|
77
|
+
.sort()
|
|
78
|
+
.reverse();
|
|
79
|
+
|
|
80
|
+
if (dated.length > 0) return dated[0];
|
|
81
|
+
|
|
82
|
+
// Fallback: any .md file, most recent by name
|
|
83
|
+
const mds = files.filter(f => f.endsWith('.md')).sort().reverse();
|
|
84
|
+
return mds[0] || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadConfig() {
|
|
88
|
+
const configPath = join(__dirname, 'boot-config.json');
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
91
|
+
} catch {
|
|
92
|
+
process.stderr.write(`${TAG} boot-config.json not found, using hardcoded defaults\n`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getDefaultConfig() {
|
|
98
|
+
return {
|
|
99
|
+
agentId: 'cc-mini',
|
|
100
|
+
timezone: 'America/Los_Angeles',
|
|
101
|
+
maxTotalLines: 2000,
|
|
102
|
+
steps: {
|
|
103
|
+
sharedContext: { path: '~/.openclaw/workspace/SHARED-CONTEXT.md', label: 'SHARED-CONTEXT.md', stepNumber: 2, critical: true },
|
|
104
|
+
journals: { dir: '~/Documents/wipcomputer--mac-mini-01/staff/Parker/Claude Code - Mini/documents/journals', label: 'Most Recent Journal (Parker)', stepNumber: 3, maxLines: 80, strategy: 'most-recent' },
|
|
105
|
+
workspaceDailyLogs: { dir: '~/.openclaw/workspace/memory', label: 'Workspace Daily Logs', stepNumber: 4, maxLines: 40, strategy: 'daily-logs', days: ['today', 'yesterday'] },
|
|
106
|
+
fullHistory: { label: 'Full History', stepNumber: 5, reminder: 'Read on cold start: staff/Parker/Claude Code - Mini/documents/cc-full-history.md' },
|
|
107
|
+
context: { path: '~/.ldm/agents/cc-mini/CONTEXT.md', label: 'CC CONTEXT.md', stepNumber: 6, critical: true },
|
|
108
|
+
soul: { path: '~/.ldm/agents/cc-mini/SOUL.md', label: 'CC SOUL.md', stepNumber: 7 },
|
|
109
|
+
ccJournals: { dir: '~/.ldm/agents/cc-mini/memory/journals', label: 'Most Recent CC Journal', stepNumber: 8, maxLines: 80, strategy: 'most-recent' },
|
|
110
|
+
ccDailyLog: { dir: '~/.ldm/agents/cc-mini/memory/daily', label: 'CC Daily Log', stepNumber: 9, maxLines: 60, strategy: 'daily-logs', days: ['today', 'yesterday'] },
|
|
111
|
+
repoLocations: { path: '~/.claude/projects/-Users-lesa--openclaw/memory/repo-locations.md', label: 'repo-locations.md', stepNumber: 10, critical: true },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function processStep(key, step, dates) {
|
|
117
|
+
// Reminder-only step (e.g. full history)
|
|
118
|
+
if (step.reminder) {
|
|
119
|
+
return { content: step.reminder, loaded: true, fileName: null };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Single file step
|
|
123
|
+
if (step.path) {
|
|
124
|
+
const content = readFileSafe(step.path);
|
|
125
|
+
if (!content) return { content: null, loaded: false, fileName: resolvePath(step.path) };
|
|
126
|
+
const trimmed = step.maxLines ? truncateTop(content, step.maxLines) : content;
|
|
127
|
+
return { content: trimmed, loaded: true, fileName: resolvePath(step.path) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Directory-based step
|
|
131
|
+
if (step.dir) {
|
|
132
|
+
if (step.strategy === 'most-recent') {
|
|
133
|
+
const fileName = findMostRecent(step.dir);
|
|
134
|
+
if (!fileName) return { content: null, loaded: false, fileName: resolvePath(step.dir) };
|
|
135
|
+
const fullPath = join(resolvePath(step.dir), fileName);
|
|
136
|
+
const content = readFileSafe(fullPath);
|
|
137
|
+
if (!content) return { content: null, loaded: false, fileName: fullPath };
|
|
138
|
+
const trimmed = step.maxLines ? truncateTop(content, step.maxLines) : content;
|
|
139
|
+
return { content: trimmed, loaded: true, fileName: `${fileName}` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (step.strategy === 'daily-logs') {
|
|
143
|
+
const parts = [];
|
|
144
|
+
let anyLoaded = false;
|
|
145
|
+
for (const day of (step.days || ['today'])) {
|
|
146
|
+
const dateStr = day === 'today' ? dates.today : dates.yesterday;
|
|
147
|
+
const fileName = `${dateStr}.md`;
|
|
148
|
+
const fullPath = join(resolvePath(step.dir), fileName);
|
|
149
|
+
const content = readFileSafe(fullPath);
|
|
150
|
+
if (content) {
|
|
151
|
+
const trimmed = step.maxLines ? truncateBottom(content, step.maxLines) : content;
|
|
152
|
+
parts.push(`--- ${day} (${dateStr}) ---\n${trimmed}`);
|
|
153
|
+
anyLoaded = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!anyLoaded) return { content: null, loaded: false, fileName: resolvePath(step.dir) };
|
|
157
|
+
return { content: parts.join('\n\n'), loaded: true, fileName: null };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { content: null, loaded: false, fileName: null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
let raw = '';
|
|
167
|
+
for await (const chunk of process.stdin) {
|
|
168
|
+
raw += chunk;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let input;
|
|
172
|
+
try {
|
|
173
|
+
input = JSON.parse(raw);
|
|
174
|
+
} catch {
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const config = loadConfig() || getDefaultConfig();
|
|
179
|
+
const dates = getTodayAndYesterday(config.timezone || 'America/Los_Angeles');
|
|
180
|
+
|
|
181
|
+
const sections = [];
|
|
182
|
+
const loaded = [];
|
|
183
|
+
const skipped = [];
|
|
184
|
+
let totalLines = 0;
|
|
185
|
+
|
|
186
|
+
// Sort steps by stepNumber
|
|
187
|
+
const stepEntries = Object.entries(config.steps).sort(
|
|
188
|
+
([, a], [, b]) => (a.stepNumber || 0) - (b.stepNumber || 0)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
for (const [key, step] of stepEntries) {
|
|
192
|
+
const result = processStep(key, step, dates);
|
|
193
|
+
|
|
194
|
+
if (result.loaded && result.content) {
|
|
195
|
+
const criticalTag = step.critical ? ' (CRITICAL)' : '';
|
|
196
|
+
const fileTag = result.fileName ? `: ${result.fileName}` : '';
|
|
197
|
+
const header = `== [Step ${step.stepNumber}] ${step.label}${criticalTag}${fileTag} ==`;
|
|
198
|
+
sections.push(`${header}\n${result.content}`);
|
|
199
|
+
loaded.push(`Step ${step.stepNumber}: ${step.label}`);
|
|
200
|
+
totalLines += result.content.split('\n').length;
|
|
201
|
+
} else {
|
|
202
|
+
skipped.push(`Step ${step.stepNumber}: ${step.label}`);
|
|
203
|
+
if (result.fileName) {
|
|
204
|
+
process.stderr.write(`${TAG} skipped step ${step.stepNumber}: ${result.fileName} not found\n`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Safety cap
|
|
209
|
+
if (totalLines > (config.maxTotalLines || 2000)) {
|
|
210
|
+
process.stderr.write(`${TAG} hit line cap at ${totalLines} lines, stopping\n`);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const elapsed = Date.now() - startTime;
|
|
216
|
+
const footer = `== Boot complete. Loaded ${loaded.length}/9 files in ${elapsed}ms. ==`;
|
|
217
|
+
if (skipped.length > 0) {
|
|
218
|
+
sections.push(`${footer}\nSkipped: ${skipped.join(', ')}`);
|
|
219
|
+
} else {
|
|
220
|
+
sections.push(footer);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const additionalContext =
|
|
224
|
+
`== LDM OS BOOT SEQUENCE (loaded automatically by SessionStart hook) ==\n\n` +
|
|
225
|
+
sections.join('\n\n');
|
|
226
|
+
|
|
227
|
+
const output = {
|
|
228
|
+
hookSpecificOutput: {
|
|
229
|
+
hookEventName: 'SessionStart',
|
|
230
|
+
additionalContext,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
process.stdout.write(JSON.stringify(output));
|
|
235
|
+
process.stderr.write(`${TAG} loaded ${loaded.length}/9 files in ${elapsed}ms\n`);
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
main().catch((err) => {
|
|
240
|
+
process.stderr.write(`${TAG} fatal: ${err.message}\n`);
|
|
241
|
+
process.exit(0);
|
|
242
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// LDM OS Boot Hook Installer CLI
|
|
3
|
+
// Usage:
|
|
4
|
+
// node install-cli.mjs # install or update
|
|
5
|
+
// node install-cli.mjs --status # show current state
|
|
6
|
+
// node install-cli.mjs --dry-run # preview without changes
|
|
7
|
+
|
|
8
|
+
import { detectInstallState, runInstallOrUpdate, formatStatus, formatResult } from './installer.mjs';
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
if (args.includes('--status')) {
|
|
13
|
+
const state = detectInstallState();
|
|
14
|
+
console.log(formatStatus(state));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (args.includes('--dry-run')) {
|
|
19
|
+
const result = runInstallOrUpdate({ dryRun: true });
|
|
20
|
+
console.log(formatResult(result));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Run install/update
|
|
25
|
+
const result = runInstallOrUpdate();
|
|
26
|
+
console.log(formatResult(result));
|
|
27
|
+
process.exit(0);
|