@wipcomputer/wip-ldm-os 0.4.85-alpha.9 → 0.4.86
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 +22 -2
- package/SKILL.md +137 -15
- package/bin/ldm.js +608 -75
- package/lib/deploy.mjs +36 -1
- package/lib/registry-migrations.mjs +296 -0
- package/package.json +20 -3
- package/scripts/test-boot-dir-truth.mjs +158 -0
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +9 -2
- package/scripts/test-crc-pair-login-flow.mjs +76 -1
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
- package/scripts/test-deploy-hook-ownership.mjs +160 -0
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-install-prompt-policy.mjs +84 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
- package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
- package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +96 -0
- package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
- package/scripts/test-readme-install-prompt.mjs +66 -0
- package/shared/boot/boot-config.json +18 -8
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
- package/shared/rules/security.md +4 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/boot/README.md +24 -1
- package/src/boot/boot-config.json +18 -8
- package/src/boot/boot-hook.mjs +118 -28
- package/src/boot/installer.mjs +33 -10
- package/src/hosted-mcp/.env.example +4 -0
- package/src/hosted-mcp/README.md +37 -0
- package/src/hosted-mcp/app/footer.js +2 -2
- package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
- package/src/hosted-mcp/app/pair.html +21 -3
- package/src/hosted-mcp/app/wip-logo.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
- package/src/hosted-mcp/demo/footer.js +2 -2
- package/src/hosted-mcp/demo/index.html +166 -44
- package/src/hosted-mcp/demo/login.html +87 -23
- package/src/hosted-mcp/demo/privacy.html +4 -214
- package/src/hosted-mcp/demo/tos.html +4 -189
- package/src/hosted-mcp/deploy.sh +2 -0
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
- package/src/hosted-mcp/legal/legal-footer.js +75 -0
- package/src/hosted-mcp/legal/legal.css +166 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
- package/src/hosted-mcp/legal/privacy/index.html +253 -0
- package/src/hosted-mcp/server.mjs +920 -74
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
5
|
+
const promptTemplate = readFileSync(
|
|
6
|
+
new URL("../shared/templates/install-prompt.md", import.meta.url),
|
|
7
|
+
"utf8",
|
|
8
|
+
);
|
|
9
|
+
const skill = readFileSync(new URL("../SKILL.md", import.meta.url), "utf8");
|
|
10
|
+
|
|
11
|
+
const promptStart = readme.indexOf("Read https://wip.computer/install/wip-ldm-os.txt");
|
|
12
|
+
const promptEnd = readme.indexOf("```", promptStart);
|
|
13
|
+
|
|
14
|
+
assert(promptStart >= 0, "README install prompt must point at the public install document");
|
|
15
|
+
assert(promptEnd > promptStart, "README install prompt must be fenced");
|
|
16
|
+
|
|
17
|
+
const readmePrompt = readme.slice(promptStart, promptEnd);
|
|
18
|
+
|
|
19
|
+
for (const [label, prompt] of [
|
|
20
|
+
["README install prompt", readmePrompt],
|
|
21
|
+
["shared install prompt template", promptTemplate],
|
|
22
|
+
]) {
|
|
23
|
+
assert(
|
|
24
|
+
prompt.includes("Read https://wip.computer/install/wip-ldm-os.txt"),
|
|
25
|
+
`${label} must delegate to the public install document`,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
assert(
|
|
29
|
+
prompt.includes("Use the install document and live local checks as the source of truth."),
|
|
30
|
+
`${label} must name the install document as source of truth`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
assert(
|
|
34
|
+
prompt.includes("use the selected track's dry-run path from the install document"),
|
|
35
|
+
`${label} must delegate dry-run command mapping to SKILL.md`,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
assert(
|
|
39
|
+
prompt.includes("use the selected track's install path from the install document"),
|
|
40
|
+
`${label} must delegate install command mapping to SKILL.md`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
for (const forbidden of [
|
|
44
|
+
"Track choices:",
|
|
45
|
+
"ldm install --alpha",
|
|
46
|
+
"ldm install --beta",
|
|
47
|
+
"ldm install --dry-run",
|
|
48
|
+
]) {
|
|
49
|
+
assert(!prompt.includes(forbidden), `${label} must not include ${forbidden}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const required of [
|
|
54
|
+
"## Tracks",
|
|
55
|
+
"npm view @wipcomputer/wip-ldm-os dist-tags --json",
|
|
56
|
+
"stable/current/latest: `ldm install --dry-run`",
|
|
57
|
+
"beta/latest beta: `ldm install --beta --dry-run`",
|
|
58
|
+
"alpha/latest alpha: `ldm install --alpha --dry-run`",
|
|
59
|
+
"beta/latest beta: `npm install -g @wipcomputer/wip-ldm-os@beta`",
|
|
60
|
+
"alpha/latest alpha: `npm install -g @wipcomputer/wip-ldm-os@alpha`",
|
|
61
|
+
"The README prompt should stay short. This install document owns the detailed track rules.",
|
|
62
|
+
]) {
|
|
63
|
+
assert(skill.includes(required), `SKILL.md must own track-selection logic: ${required}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log("readme-install-prompt: prompt stays short and delegates track rules to SKILL.md");
|
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
"agentId": "cc-mini",
|
|
3
3
|
"timezone": "America/Los_Angeles",
|
|
4
4
|
"maxTotalLines": 2000,
|
|
5
|
+
"stalenessDays": 14,
|
|
6
|
+
"dailyLogStalenessDays": 2,
|
|
5
7
|
"steps": {
|
|
6
8
|
"sharedContext": {
|
|
7
9
|
"path": "~/.openclaw/workspace/SHARED-CONTEXT.md",
|
|
8
10
|
"label": "SHARED-CONTEXT.md",
|
|
9
11
|
"stepNumber": 2,
|
|
10
|
-
"critical": true
|
|
12
|
+
"critical": true,
|
|
13
|
+
"maxLines": 80
|
|
11
14
|
},
|
|
12
15
|
"journals": {
|
|
13
16
|
"dir": "~/wipcomputerinc/team/cc-mini/documents/journals",
|
|
14
17
|
"label": "Most Recent Journal (Parker)",
|
|
15
18
|
"stepNumber": 3,
|
|
16
19
|
"maxLines": 80,
|
|
17
|
-
"strategy": "most-recent"
|
|
20
|
+
"strategy": "most-recent",
|
|
21
|
+
"stalenessDays": 14
|
|
18
22
|
},
|
|
19
23
|
"workspaceDailyLogs": {
|
|
20
24
|
"dir": "~/.openclaw/workspace/memory",
|
|
@@ -22,7 +26,8 @@
|
|
|
22
26
|
"stepNumber": 4,
|
|
23
27
|
"maxLines": 40,
|
|
24
28
|
"strategy": "daily-logs",
|
|
25
|
-
"days": ["today", "yesterday"]
|
|
29
|
+
"days": ["today", "yesterday"],
|
|
30
|
+
"dailyLogStalenessDays": 2
|
|
26
31
|
},
|
|
27
32
|
"fullHistory": {
|
|
28
33
|
"label": "Full History",
|
|
@@ -33,19 +38,22 @@
|
|
|
33
38
|
"path": "~/.ldm/agents/cc-mini/CONTEXT.md",
|
|
34
39
|
"label": "CC CONTEXT.md",
|
|
35
40
|
"stepNumber": 6,
|
|
36
|
-
"critical": true
|
|
41
|
+
"critical": true,
|
|
42
|
+
"maxLines": 60
|
|
37
43
|
},
|
|
38
44
|
"soul": {
|
|
39
45
|
"path": "~/.ldm/agents/cc-mini/SOUL.md",
|
|
40
46
|
"label": "CC SOUL.md",
|
|
41
|
-
"stepNumber": 7
|
|
47
|
+
"stepNumber": 7,
|
|
48
|
+
"maxLines": 80
|
|
42
49
|
},
|
|
43
50
|
"ccJournals": {
|
|
44
51
|
"dir": "~/.ldm/agents/cc-mini/memory/journals",
|
|
45
52
|
"label": "Most Recent CC Journal",
|
|
46
53
|
"stepNumber": 8,
|
|
47
54
|
"maxLines": 80,
|
|
48
|
-
"strategy": "most-recent"
|
|
55
|
+
"strategy": "most-recent",
|
|
56
|
+
"stalenessDays": 14
|
|
49
57
|
},
|
|
50
58
|
"ccDailyLog": {
|
|
51
59
|
"dir": "~/.ldm/agents/cc-mini/memory/daily",
|
|
@@ -53,13 +61,15 @@
|
|
|
53
61
|
"stepNumber": 9,
|
|
54
62
|
"maxLines": 60,
|
|
55
63
|
"strategy": "daily-logs",
|
|
56
|
-
"days": ["today", "yesterday"]
|
|
64
|
+
"days": ["today", "yesterday"],
|
|
65
|
+
"dailyLogStalenessDays": 2
|
|
57
66
|
},
|
|
58
67
|
"repoLocations": {
|
|
59
68
|
"path": "~/.claude/projects/-Users-lesa--openclaw/memory/repo-locations.md",
|
|
60
69
|
"label": "repo-locations.md",
|
|
61
70
|
"stepNumber": 10,
|
|
62
|
-
"critical": true
|
|
71
|
+
"critical": true,
|
|
72
|
+
"maxLines": 80
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
}
|
|
@@ -38,12 +38,13 @@ These are three distinct actions. Never combine them. Never skip the dogfooding
|
|
|
38
38
|
|
|
39
39
|
## Co-Authors on Every Commit
|
|
40
40
|
|
|
41
|
-
All
|
|
41
|
+
All four contributors must be listed on every commit. No exceptions. These trailers credit contributors in commit history and preserve the builder record across the team.
|
|
42
42
|
|
|
43
43
|
```
|
|
44
44
|
Co-Authored-By: Parker Todd Brooks <parkertoddbrooks@users.noreply.github.com>
|
|
45
45
|
Co-Authored-By: Lēsa <lesaai@icloud.com>
|
|
46
|
-
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
46
|
+
Co-Authored-By: Claude Code (Opus 4.7) <noreply@anthropic.com>
|
|
47
|
+
Co-Authored-By: Codex (GPT 5.5) <noreply@openai.com>
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
## Built-By Line
|
|
@@ -51,7 +52,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
|
51
52
|
Every repo README must include this exact attribution in the License section:
|
|
52
53
|
|
|
53
54
|
```
|
|
54
|
-
Built by Parker Todd Brooks, Lēsa
|
|
55
|
+
Built by Parker Todd Brooks, Lēsa, Claude Code (Opus 4.7), and Codex (GPT 5.5).
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
This is the standard. Use it everywhere. It credits the humans and identifies which AI runtimes built the software.
|
|
@@ -70,7 +71,7 @@ AGPL Commercial redistribution, marketplace listings, or bundling into paid se
|
|
|
70
71
|
|
|
71
72
|
AGPL for personal use is free. Commercial licenses available.
|
|
72
73
|
|
|
73
|
-
Built by Parker Todd Brooks, Lēsa
|
|
74
|
+
Built by Parker Todd Brooks, Lēsa, Claude Code (Opus 4.7), and Codex (GPT 5.5).
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
**Rules:**
|
package/shared/rules/security.md
CHANGED
|
@@ -15,3 +15,7 @@ Never overwrite shared workspace files. Always append or edit specific sections.
|
|
|
15
15
|
## Protected paths
|
|
16
16
|
|
|
17
17
|
Do not modify: secrets/, credentials/, auth-profiles.json, or any file containing API keys.
|
|
18
|
+
|
|
19
|
+
## Config doctors can silently strip keys
|
|
20
|
+
|
|
21
|
+
Never run `openclaw doctor --fix` (or any config doctor with strict-schema validation) without checking `git diff` on the config afterward. Doctor tools silently strip keys they do not recognize; git is the safety net.
|
|
@@ -6,7 +6,19 @@ Open your AI and paste this:
|
|
|
6
6
|
|
|
7
7
|
Read https://wip.computer/install/wip-ldm-os.txt
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Use the install document and live local checks as the source of truth.
|
|
10
|
+
Do not search memory or prior notes for this install. Do not pre-load context from `MEMORY.md`, `crystal_search`, local skill dev guides, or other local memory before fetching the install document.
|
|
11
|
+
Do not run GitHub commands during this install flow. Do not run or request approval for `gh release`, `gh api`, or `gh search`. If release notes are not available from local or npm metadata, say that and do not fetch them from GitHub.
|
|
12
|
+
|
|
13
|
+
Check if LDM OS is installed (`which ldm && ldm --version`). Branch.
|
|
14
|
+
|
|
15
|
+
If installed: run `ldm status`, check available npm tracks from the install document, and show me the version table, what's new for each component, and which stable/current, beta, and alpha versions are available. Then ask:
|
|
16
|
+
- Do you have questions?
|
|
17
|
+
- Want to see a dry run?
|
|
18
|
+
|
|
19
|
+
If yes to dry run, use the selected track's dry-run path from the install document.
|
|
20
|
+
|
|
21
|
+
If I say install, use the selected track's install path from the install document, then run `ldm doctor`.
|
|
10
22
|
|
|
11
23
|
If not, walk me through setup and explain:
|
|
12
24
|
|
|
@@ -19,7 +31,13 @@ Then ask:
|
|
|
19
31
|
- Do you have questions?
|
|
20
32
|
- Want to see a dry run?
|
|
21
33
|
|
|
22
|
-
If
|
|
34
|
+
If yes to dry run, install the CLI first using the selected track's bootstrap command from the install document.
|
|
35
|
+
|
|
36
|
+
Then run:
|
|
37
|
+
`ldm init --dry-run`
|
|
38
|
+
|
|
39
|
+
If I say install, run:
|
|
40
|
+
`ldm init`
|
|
23
41
|
|
|
24
42
|
Show me exactly what will change. Don't install anything until I say "install".
|
|
25
43
|
|
package/src/boot/README.md
CHANGED
|
@@ -8,7 +8,24 @@ Reads 9 files from the Dream Weaver Boot Sequence (SHARED-CONTEXT.md, SOUL.md, C
|
|
|
8
8
|
|
|
9
9
|
## Content Budget
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Large files are line-capped and stale journals are dropped to path-only lines, so the payload stays small. Missing files are skipped gracefully. The last line of every boot payload is a one-line summary (`== Boot payload: N bytes, M lines, K sections. Capped: ... Stale/path-only: ... ==`) so future slowness triage is trivial.
|
|
12
|
+
|
|
13
|
+
### Per-step line caps
|
|
14
|
+
|
|
15
|
+
Every step has a line cap. If `boot-config.json` sets `maxLines` for a step, that wins. Otherwise these code defaults apply. They live in `boot-hook.mjs`, not just the config template, because the installer preserves an existing user `boot-config.json` and never overwrites it, so a config-only change would never reach existing installs:
|
|
16
|
+
|
|
17
|
+
- `sharedContext`: 80
|
|
18
|
+
- `soul`: 80
|
|
19
|
+
- `context`: 60
|
|
20
|
+
- `repoLocations`: 80
|
|
21
|
+
|
|
22
|
+
When a file is truncated, the injected content ends with a marker naming the full path so the rest can be read on demand.
|
|
23
|
+
|
|
24
|
+
### Staleness cutoffs
|
|
25
|
+
|
|
26
|
+
`most-recent` steps (journals) inject the newest dated file only if it is fresh. If the newest file is older than `stalenessDays` (default 14, overridable top-level or per-step), the body is NOT injected: a single line with the file path, date, and age is emitted instead. This stops a four-month-old "most recent" journal from being re-injected every session.
|
|
27
|
+
|
|
28
|
+
`daily-logs` steps have the same guard via `dailyLogStalenessDays` (default 2). Day files are today/yesterday by construction, so this is defensive.
|
|
12
29
|
|
|
13
30
|
## Deploy
|
|
14
31
|
|
|
@@ -49,6 +66,12 @@ Should output JSON with `hookSpecificOutput.additionalContext` containing all bo
|
|
|
49
66
|
|
|
50
67
|
`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
68
|
|
|
69
|
+
Optional keys:
|
|
70
|
+
|
|
71
|
+
- Top-level `stalenessDays` (default 14) and `dailyLogStalenessDays` (default 2): staleness cutoffs for `most-recent` and `daily-logs` steps. Per-step keys of the same name override the top-level value.
|
|
72
|
+
- Per-step `maxLines`: line cap for that step. Overrides the code default (see Content Budget).
|
|
73
|
+
- Top-level `maxTotalLines` (default 2000): hard safety cap across all steps.
|
|
74
|
+
|
|
52
75
|
## Adding a Boot Step
|
|
53
76
|
|
|
54
77
|
1. Add an entry to `boot-config.json` under `steps`
|
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
"agentId": "cc-mini",
|
|
3
3
|
"timezone": "America/Los_Angeles",
|
|
4
4
|
"maxTotalLines": 2000,
|
|
5
|
+
"stalenessDays": 14,
|
|
6
|
+
"dailyLogStalenessDays": 2,
|
|
5
7
|
"steps": {
|
|
6
8
|
"sharedContext": {
|
|
7
9
|
"path": "~/.openclaw/workspace/SHARED-CONTEXT.md",
|
|
8
10
|
"label": "SHARED-CONTEXT.md",
|
|
9
11
|
"stepNumber": 2,
|
|
10
|
-
"critical": true
|
|
12
|
+
"critical": true,
|
|
13
|
+
"maxLines": 80
|
|
11
14
|
},
|
|
12
15
|
"journals": {
|
|
13
16
|
"dir": "~/Documents/wipcomputer--mac-mini-01/staff/Parker/Claude Code - Mini/documents/journals",
|
|
14
17
|
"label": "Most Recent Journal (Parker)",
|
|
15
18
|
"stepNumber": 3,
|
|
16
19
|
"maxLines": 80,
|
|
17
|
-
"strategy": "most-recent"
|
|
20
|
+
"strategy": "most-recent",
|
|
21
|
+
"stalenessDays": 14
|
|
18
22
|
},
|
|
19
23
|
"workspaceDailyLogs": {
|
|
20
24
|
"dir": "~/.openclaw/workspace/memory",
|
|
@@ -22,7 +26,8 @@
|
|
|
22
26
|
"stepNumber": 4,
|
|
23
27
|
"maxLines": 40,
|
|
24
28
|
"strategy": "daily-logs",
|
|
25
|
-
"days": ["today", "yesterday"]
|
|
29
|
+
"days": ["today", "yesterday"],
|
|
30
|
+
"dailyLogStalenessDays": 2
|
|
26
31
|
},
|
|
27
32
|
"fullHistory": {
|
|
28
33
|
"label": "Full History",
|
|
@@ -33,19 +38,22 @@
|
|
|
33
38
|
"path": "~/.ldm/agents/cc-mini/CONTEXT.md",
|
|
34
39
|
"label": "CC CONTEXT.md",
|
|
35
40
|
"stepNumber": 6,
|
|
36
|
-
"critical": true
|
|
41
|
+
"critical": true,
|
|
42
|
+
"maxLines": 60
|
|
37
43
|
},
|
|
38
44
|
"soul": {
|
|
39
45
|
"path": "~/.ldm/agents/cc-mini/SOUL.md",
|
|
40
46
|
"label": "CC SOUL.md",
|
|
41
|
-
"stepNumber": 7
|
|
47
|
+
"stepNumber": 7,
|
|
48
|
+
"maxLines": 80
|
|
42
49
|
},
|
|
43
50
|
"ccJournals": {
|
|
44
51
|
"dir": "~/.ldm/agents/cc-mini/memory/journals",
|
|
45
52
|
"label": "Most Recent CC Journal",
|
|
46
53
|
"stepNumber": 8,
|
|
47
54
|
"maxLines": 80,
|
|
48
|
-
"strategy": "most-recent"
|
|
55
|
+
"strategy": "most-recent",
|
|
56
|
+
"stalenessDays": 14
|
|
49
57
|
},
|
|
50
58
|
"ccDailyLog": {
|
|
51
59
|
"dir": "~/.ldm/agents/cc-mini/memory/daily",
|
|
@@ -53,13 +61,15 @@
|
|
|
53
61
|
"stepNumber": 9,
|
|
54
62
|
"maxLines": 60,
|
|
55
63
|
"strategy": "daily-logs",
|
|
56
|
-
"days": ["today", "yesterday"]
|
|
64
|
+
"days": ["today", "yesterday"],
|
|
65
|
+
"dailyLogStalenessDays": 2
|
|
57
66
|
},
|
|
58
67
|
"repoLocations": {
|
|
59
68
|
"path": "~/.claude/projects/-Users-lesa--openclaw/memory/repo-locations.md",
|
|
60
69
|
"label": "repo-locations.md",
|
|
61
70
|
"stepNumber": 10,
|
|
62
|
-
"critical": true
|
|
71
|
+
"critical": true,
|
|
72
|
+
"maxLines": 80
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
}
|
package/src/boot/boot-hook.mjs
CHANGED
|
@@ -38,18 +38,63 @@ function listDirSafe(dirPath) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Per-step default line caps applied when boot-config.json does not set
|
|
42
|
+
// maxLines for the step. Keyed by the step key in config.steps. Any of these
|
|
43
|
+
// can be overridden per-step via a maxLines value in boot-config.json.
|
|
44
|
+
// Lives in code (not just the config template) because the installer preserves
|
|
45
|
+
// an existing user boot-config.json, so config-template changes never reach
|
|
46
|
+
// existing installs. Defaults must ride along with the hook.
|
|
47
|
+
const DEFAULT_MAX_LINES = {
|
|
48
|
+
sharedContext: 80,
|
|
49
|
+
soul: 80,
|
|
50
|
+
context: 60,
|
|
51
|
+
repoLocations: 80,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Default staleness cutoffs. Overridable via top-level config keys
|
|
55
|
+
// (stalenessDays, dailyLogStalenessDays) or per-step keys of the same name.
|
|
56
|
+
const DEFAULT_STALENESS_DAYS = 14; // most-recent strategy (journals)
|
|
57
|
+
const DEFAULT_DAILY_LOG_STALENESS_DAYS = 2; // daily-logs strategy guard
|
|
58
|
+
|
|
59
|
+
function resolveMaxLines(key, step) {
|
|
60
|
+
if (step.maxLines != null) return step.maxLines;
|
|
61
|
+
if (DEFAULT_MAX_LINES[key] != null) return DEFAULT_MAX_LINES[key];
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Returns { text, capped }. When truncation happens, the marker names the
|
|
66
|
+
// full path so a future reader can pull the rest.
|
|
67
|
+
function truncateTop(content, maxLines, fullPath) {
|
|
68
|
+
if (!maxLines || !content) return { text: content, capped: false };
|
|
43
69
|
const lines = content.split('\n');
|
|
44
|
-
if (lines.length <= maxLines) return content;
|
|
45
|
-
|
|
70
|
+
if (lines.length <= maxLines) return { text: content, capped: false };
|
|
71
|
+
const where = fullPath || '(path unknown)';
|
|
72
|
+
const marker = `\n[... truncated at ${maxLines} of ${lines.length} lines. Read the rest: ${where} ...]`;
|
|
73
|
+
return { text: lines.slice(0, maxLines).join('\n') + marker, capped: true };
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
function truncateBottom(content, maxLines) {
|
|
49
|
-
if (!maxLines || !content) return content;
|
|
76
|
+
function truncateBottom(content, maxLines, fullPath) {
|
|
77
|
+
if (!maxLines || !content) return { text: content, capped: false };
|
|
50
78
|
const lines = content.split('\n');
|
|
51
|
-
if (lines.length <= maxLines) return content;
|
|
52
|
-
|
|
79
|
+
if (lines.length <= maxLines) return { text: content, capped: false };
|
|
80
|
+
const where = fullPath || '(path unknown)';
|
|
81
|
+
const marker = `[... showing last ${maxLines} of ${lines.length} lines. Read the rest: ${where} ...]\n`;
|
|
82
|
+
return { text: marker + lines.slice(-maxLines).join('\n'), capped: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract a YYYY-MM-DD prefix from a filename, or null if none.
|
|
86
|
+
function dateStrFromName(fileName) {
|
|
87
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(fileName || '');
|
|
88
|
+
return m ? m[1] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Whole-day difference (todayStr - dateStr) in days. Both are YYYY-MM-DD.
|
|
92
|
+
// Positive means dateStr is in the past. null if either is unparseable.
|
|
93
|
+
function ageInDays(dateStr, todayStr) {
|
|
94
|
+
const a = Date.parse(`${dateStr}T00:00:00Z`);
|
|
95
|
+
const b = Date.parse(`${todayStr}T00:00:00Z`);
|
|
96
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
|
97
|
+
return Math.round((b - a) / 86400000);
|
|
53
98
|
}
|
|
54
99
|
|
|
55
100
|
function getTodayAndYesterday(timezone) {
|
|
@@ -99,6 +144,8 @@ function getDefaultConfig() {
|
|
|
99
144
|
agentId: 'cc-mini',
|
|
100
145
|
timezone: 'America/Los_Angeles',
|
|
101
146
|
maxTotalLines: 2000,
|
|
147
|
+
stalenessDays: DEFAULT_STALENESS_DAYS,
|
|
148
|
+
dailyLogStalenessDays: DEFAULT_DAILY_LOG_STALENESS_DAYS,
|
|
102
149
|
steps: {
|
|
103
150
|
sharedContext: { path: '~/.openclaw/workspace/SHARED-CONTEXT.md', label: 'SHARED-CONTEXT.md', stepNumber: 2, critical: true },
|
|
104
151
|
journals: { dir: '~/.ldm/agents/cc-mini/memory/journals', label: 'Most Recent CC Journal (Legacy)', stepNumber: 3, maxLines: 80, strategy: 'most-recent' },
|
|
@@ -113,52 +160,79 @@ function getDefaultConfig() {
|
|
|
113
160
|
};
|
|
114
161
|
}
|
|
115
162
|
|
|
116
|
-
function processStep(key, step, dates) {
|
|
163
|
+
function processStep(key, step, dates, config) {
|
|
117
164
|
// Reminder-only step (e.g. full history)
|
|
118
165
|
if (step.reminder) {
|
|
119
|
-
return { content: step.reminder, loaded: true, fileName: null };
|
|
166
|
+
return { content: step.reminder, loaded: true, fileName: null, capped: false, stale: false };
|
|
120
167
|
}
|
|
121
168
|
|
|
169
|
+
const maxLines = resolveMaxLines(key, step);
|
|
170
|
+
|
|
122
171
|
// Single file step
|
|
123
172
|
if (step.path) {
|
|
173
|
+
const resolved = resolvePath(step.path);
|
|
124
174
|
const content = readFileSafe(step.path);
|
|
125
|
-
if (!content) return { content: null, loaded: false, fileName:
|
|
126
|
-
const
|
|
127
|
-
return { content:
|
|
175
|
+
if (!content) return { content: null, loaded: false, fileName: resolved, capped: false, stale: false };
|
|
176
|
+
const { text, capped } = truncateTop(content, maxLines, resolved);
|
|
177
|
+
return { content: text, loaded: true, fileName: resolved, capped, stale: false };
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
// Directory-based step
|
|
131
181
|
if (step.dir) {
|
|
132
182
|
if (step.strategy === 'most-recent') {
|
|
133
183
|
const fileName = findMostRecent(step.dir);
|
|
134
|
-
if (!fileName) return { content: null, loaded: false, fileName: resolvePath(step.dir) };
|
|
184
|
+
if (!fileName) return { content: null, loaded: false, fileName: resolvePath(step.dir), capped: false, stale: false };
|
|
135
185
|
const fullPath = join(resolvePath(step.dir), fileName);
|
|
136
186
|
const content = readFileSafe(fullPath);
|
|
137
|
-
if (!content) return { content: null, loaded: false, fileName: fullPath };
|
|
138
|
-
|
|
139
|
-
|
|
187
|
+
if (!content) return { content: null, loaded: false, fileName: fullPath, capped: false, stale: false };
|
|
188
|
+
|
|
189
|
+
// Staleness cutoff: if the newest file is older than the cutoff, do not
|
|
190
|
+
// inject its body. Emit a single line with the path and date instead.
|
|
191
|
+
const stalenessDays = step.stalenessDays ?? config.stalenessDays ?? DEFAULT_STALENESS_DAYS;
|
|
192
|
+
const fileDate = dateStrFromName(fileName);
|
|
193
|
+
const age = fileDate ? ageInDays(fileDate, dates.today) : null;
|
|
194
|
+
if (age != null && age > stalenessDays) {
|
|
195
|
+
const line = `[stale: newest file is ${fileDate} (${age}d old, cutoff ${stalenessDays}d). Body not injected. Read if needed: ${fullPath}]`;
|
|
196
|
+
return { content: line, loaded: true, fileName, capped: false, stale: true };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { text, capped } = truncateTop(content, maxLines, fullPath);
|
|
200
|
+
return { content: text, loaded: true, fileName, capped, stale: false };
|
|
140
201
|
}
|
|
141
202
|
|
|
142
203
|
if (step.strategy === 'daily-logs') {
|
|
204
|
+
const dailyStalenessDays = step.dailyLogStalenessDays ?? config.dailyLogStalenessDays ?? DEFAULT_DAILY_LOG_STALENESS_DAYS;
|
|
143
205
|
const parts = [];
|
|
144
206
|
let anyLoaded = false;
|
|
207
|
+
let anyCapped = false;
|
|
208
|
+
let anyStale = false;
|
|
145
209
|
for (const day of (step.days || ['today'])) {
|
|
146
210
|
const dateStr = day === 'today' ? dates.today : dates.yesterday;
|
|
147
211
|
const fileName = `${dateStr}.md`;
|
|
148
212
|
const fullPath = join(resolvePath(step.dir), fileName);
|
|
149
213
|
const content = readFileSafe(fullPath);
|
|
150
|
-
if (content)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
214
|
+
if (!content) continue;
|
|
215
|
+
anyLoaded = true;
|
|
216
|
+
|
|
217
|
+
// Guard: a day file dated older than the cutoff should not normally
|
|
218
|
+
// happen (days are today/yesterday), but if it does, emit path-only.
|
|
219
|
+
const age = ageInDays(dateStr, dates.today);
|
|
220
|
+
if (age != null && age > dailyStalenessDays) {
|
|
221
|
+
parts.push(`--- ${day} (${dateStr}) ---\n[stale: ${age}d old, cutoff ${dailyStalenessDays}d. Body not injected. Read if needed: ${fullPath}]`);
|
|
222
|
+
anyStale = true;
|
|
223
|
+
continue;
|
|
154
224
|
}
|
|
225
|
+
|
|
226
|
+
const { text, capped } = truncateBottom(content, maxLines, fullPath);
|
|
227
|
+
if (capped) anyCapped = true;
|
|
228
|
+
parts.push(`--- ${day} (${dateStr}) ---\n${text}`);
|
|
155
229
|
}
|
|
156
|
-
if (!anyLoaded) return { content: null, loaded: false, fileName: resolvePath(step.dir) };
|
|
157
|
-
return { content: parts.join('\n\n'), loaded: true, fileName: null };
|
|
230
|
+
if (!anyLoaded) return { content: null, loaded: false, fileName: resolvePath(step.dir), capped: false, stale: false };
|
|
231
|
+
return { content: parts.join('\n\n'), loaded: true, fileName: null, capped: anyCapped, stale: anyStale };
|
|
158
232
|
}
|
|
159
233
|
}
|
|
160
234
|
|
|
161
|
-
return { content: null, loaded: false, fileName: null };
|
|
235
|
+
return { content: null, loaded: false, fileName: null, capped: false, stale: false };
|
|
162
236
|
}
|
|
163
237
|
|
|
164
238
|
async function main() {
|
|
@@ -181,6 +255,8 @@ async function main() {
|
|
|
181
255
|
const sections = [];
|
|
182
256
|
const loaded = [];
|
|
183
257
|
const skipped = [];
|
|
258
|
+
const cappedSteps = [];
|
|
259
|
+
const staleSteps = [];
|
|
184
260
|
let totalLines = 0;
|
|
185
261
|
|
|
186
262
|
// Sort steps by stepNumber
|
|
@@ -189,7 +265,7 @@ async function main() {
|
|
|
189
265
|
);
|
|
190
266
|
|
|
191
267
|
for (const [key, step] of stepEntries) {
|
|
192
|
-
const result = processStep(key, step, dates);
|
|
268
|
+
const result = processStep(key, step, dates, config);
|
|
193
269
|
|
|
194
270
|
if (result.loaded && result.content) {
|
|
195
271
|
const criticalTag = step.critical ? ' (CRITICAL)' : '';
|
|
@@ -198,6 +274,8 @@ async function main() {
|
|
|
198
274
|
sections.push(`${header}\n${result.content}`);
|
|
199
275
|
loaded.push(`Step ${step.stepNumber}: ${step.label}`);
|
|
200
276
|
totalLines += result.content.split('\n').length;
|
|
277
|
+
if (result.capped) cappedSteps.push(`Step ${step.stepNumber}`);
|
|
278
|
+
if (result.stale) staleSteps.push(`Step ${step.stepNumber}`);
|
|
201
279
|
} else {
|
|
202
280
|
skipped.push(`Step ${step.stepNumber}: ${step.label}`);
|
|
203
281
|
if (result.fileName) {
|
|
@@ -275,16 +353,28 @@ async function main() {
|
|
|
275
353
|
} catch {}
|
|
276
354
|
|
|
277
355
|
const elapsed = Date.now() - startTime;
|
|
278
|
-
const footer = `== Boot complete. Loaded ${loaded.length}
|
|
356
|
+
const footer = `== Boot complete. Loaded ${loaded.length} sections in ${elapsed}ms. ==`;
|
|
279
357
|
if (skipped.length > 0) {
|
|
280
358
|
sections.push(`${footer}\nSkipped: ${skipped.join(', ')}`);
|
|
281
359
|
} else {
|
|
282
360
|
sections.push(footer);
|
|
283
361
|
}
|
|
284
362
|
|
|
285
|
-
|
|
363
|
+
// Body first, then a one-line payload summary so future slowness triage is
|
|
364
|
+
// trivial: total bytes/lines injected, which steps were line-capped, and
|
|
365
|
+
// which were skipped as stale (path-only).
|
|
366
|
+
const bodyText =
|
|
286
367
|
`== LDM OS BOOT SEQUENCE (loaded automatically by SessionStart hook) ==\n\n` +
|
|
287
368
|
sections.join('\n\n');
|
|
369
|
+
const payloadBytes = Buffer.byteLength(bodyText, 'utf-8');
|
|
370
|
+
const payloadLines = bodyText.split('\n').length;
|
|
371
|
+
const summary =
|
|
372
|
+
`== Boot payload: ${payloadBytes} bytes, ${payloadLines} lines, ${loaded.length} sections. ` +
|
|
373
|
+
`Capped: ${cappedSteps.length ? cappedSteps.join(', ') : 'none'}. ` +
|
|
374
|
+
`Stale/path-only: ${staleSteps.length ? staleSteps.join(', ') : 'none'}. ` +
|
|
375
|
+
`Missing: ${skipped.length ? skipped.length : 'none'}. ==`;
|
|
376
|
+
|
|
377
|
+
const additionalContext = `${bodyText}\n\n${summary}`;
|
|
288
378
|
|
|
289
379
|
const output = {
|
|
290
380
|
hookSpecificOutput: {
|
|
@@ -294,7 +384,7 @@ async function main() {
|
|
|
294
384
|
};
|
|
295
385
|
|
|
296
386
|
process.stdout.write(JSON.stringify(output));
|
|
297
|
-
process.stderr.write(`${TAG} loaded ${loaded.length}
|
|
387
|
+
process.stderr.write(`${TAG} loaded ${loaded.length} sections, ${payloadBytes} bytes in ${elapsed}ms\n`);
|
|
298
388
|
process.exit(0);
|
|
299
389
|
}
|
|
300
390
|
|
package/src/boot/installer.mjs
CHANGED
|
@@ -142,13 +142,20 @@ export function configureSessionStartHook() {
|
|
|
142
142
|
if (!settings.hooks) settings.hooks = {};
|
|
143
143
|
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
const existingIdx = settings.hooks.SessionStart.findIndex((entry) => {
|
|
145
|
+
const isBootEntry = (entry) => {
|
|
147
146
|
const hooks = entry?.hooks;
|
|
148
147
|
if (!Array.isArray(hooks)) return false;
|
|
149
148
|
return hooks.some((h) =>
|
|
150
149
|
h?.command?.includes('boot-hook') || h?.command?.includes('shared/boot')
|
|
151
150
|
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Find ALL boot-hook entries. Duplicate registrations accumulate across
|
|
154
|
+
// installs (10 copies observed 2026-07-04, each running the boot script
|
|
155
|
+
// once per session), so this owns the whole set, not just the first.
|
|
156
|
+
const ownedIdxs = [];
|
|
157
|
+
settings.hooks.SessionStart.forEach((entry, i) => {
|
|
158
|
+
if (isBootEntry(entry)) ownedIdxs.push(i);
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
const hookEntry = {
|
|
@@ -160,16 +167,32 @@ export function configureSessionStartHook() {
|
|
|
160
167
|
}],
|
|
161
168
|
};
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
// Collapse duplicates beyond the first. Walk right-to-left so earlier
|
|
171
|
+
// indices stay valid while splicing.
|
|
172
|
+
let removed = 0;
|
|
173
|
+
for (let j = ownedIdxs.length - 1; j >= 1; j--) {
|
|
174
|
+
settings.hooks.SessionStart.splice(ownedIdxs[j], 1);
|
|
175
|
+
removed++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ownedIdxs.length > 0) {
|
|
179
|
+
const idx = ownedIdxs[0];
|
|
180
|
+
const identical =
|
|
181
|
+
JSON.stringify(settings.hooks.SessionStart[idx]) === JSON.stringify(hookEntry);
|
|
182
|
+
if (identical && removed === 0) {
|
|
183
|
+
// Nothing to change; do not touch the file.
|
|
184
|
+
return 'SessionStart hook already configured';
|
|
185
|
+
}
|
|
186
|
+
settings.hooks.SessionStart[idx] = hookEntry;
|
|
170
187
|
writeJSON(CC_SETTINGS, settings);
|
|
171
|
-
return
|
|
188
|
+
return removed > 0
|
|
189
|
+
? `SessionStart hook updated in settings.json (removed ${removed} duplicate entr${removed === 1 ? 'y' : 'ies'})`
|
|
190
|
+
: 'SessionStart hook updated in settings.json';
|
|
172
191
|
}
|
|
192
|
+
|
|
193
|
+
settings.hooks.SessionStart.push(hookEntry);
|
|
194
|
+
writeJSON(CC_SETTINGS, settings);
|
|
195
|
+
return 'SessionStart hook added to settings.json';
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
// ── Registry ──
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
# Kaleidoscope server environment
|
|
2
2
|
# Copy to .env and fill in values. Never commit .env itself.
|
|
3
3
|
DATABASE_URL=postgresql://kaleidoscope:YOUR_PASSWORD@localhost:5432/kaleidoscope
|
|
4
|
+
KALEIDOSCOPE_LIVE_WALL_FILE=/var/lib/wip/kaleidoscope-live-wall.json
|
|
5
|
+
KALEIDOSCOPE_LIVE_WALL_MEDIA_DIR=/var/www/wip.computer/public_html/media/kaleidoscope/generated
|
|
6
|
+
KALEIDOSCOPE_LIVE_WALL_MAX_IMAGE_BYTES=10485760
|
|
7
|
+
KALEIDOSCOPE_LIVE_WALL_FETCH_TIMEOUT_MS=15000
|