@wipcomputer/wip-ldm-os 0.2.13 → 0.3.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/SKILL.md +1 -1
- package/bin/ldm.js +242 -0
- package/dist/bridge/chunk-KWGJCDGS.js +424 -0
- package/dist/bridge/cli.d.ts +1 -0
- package/dist/bridge/cli.js +215 -0
- package/dist/bridge/core.d.ts +74 -0
- package/dist/bridge/core.js +40 -0
- package/dist/bridge/mcp-server.d.ts +2 -0
- package/dist/bridge/mcp-server.js +284 -0
- package/docs/TECHNICAL.md +290 -0
- package/docs/acp-compatibility.md +30 -0
- package/docs/optional-skills.md +77 -0
- package/docs/recall.md +29 -0
- package/docs/shared-workspace.md +37 -0
- package/docs/system-pulse.md +26 -0
- package/docs/universal-installer.md +84 -0
- package/lib/messages.mjs +195 -0
- package/lib/sessions.mjs +145 -0
- package/lib/updates.mjs +173 -0
- package/package.json +9 -2
- package/src/boot/boot-hook.mjs +36 -1
- package/src/bridge/cli.ts +245 -0
- package/src/bridge/core.ts +622 -0
- package/src/bridge/mcp-server.ts +371 -0
- package/src/bridge/package.json +18 -0
- package/src/bridge/tsconfig.json +19 -0
- package/src/cron/update-check.mjs +28 -0
- package/src/hooks/stop-hook.mjs +24 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Protocol Compatibility
|
|
2
|
+
|
|
3
|
+
## Agent Client Protocol (ACP-Client)
|
|
4
|
+
|
|
5
|
+
The Agent Client Protocol (ACP-Client) is a standardization effort from Zed Industries (Apache 2.0) that enables structured communication between code editors/IDEs and AI coding agents. It uses JSON-RPC over stdio for local agents and HTTP/WebSocket for remote agents.
|
|
6
|
+
|
|
7
|
+
OpenClaw already implements ACP-Client via the `openclaw acp` CLI command and `@agentclientprotocol/sdk` dependency.
|
|
8
|
+
|
|
9
|
+
LDM OS bridge features (MCP tools) operate through the Model Context Protocol (MCP), which is separate from and complementary to ACP-Client. MCP connects an LLM to its tools/resources (internal wiring). ACP-Client connects editors to agents (external communication).
|
|
10
|
+
|
|
11
|
+
### Current Status
|
|
12
|
+
|
|
13
|
+
- LDM OS uses MCP for all tool access (bridge, sessions, messages, updates)
|
|
14
|
+
- ACP-Client is available in OpenClaw but not configured
|
|
15
|
+
- No conflicts between MCP and ACP-Client
|
|
16
|
+
|
|
17
|
+
### Future Compatibility
|
|
18
|
+
|
|
19
|
+
- LDM OS could expose services via ACP-Client for IDE integration
|
|
20
|
+
- The transport-agnostic core design supports adding ACP-Client as another wrapper
|
|
21
|
+
|
|
22
|
+
## Agent Communication Protocol (ACP-Comm)
|
|
23
|
+
|
|
24
|
+
The Agent Communication Protocol (ACP-Comm) from IBM / Linux Foundation (Apache 2.0) is a REST/HTTP protocol for agent-to-agent communication. It includes agent discovery, session management, and run lifecycle.
|
|
25
|
+
|
|
26
|
+
LDM OS does not currently implement ACP-Comm. The file-based message bus and session registry serve the same purpose for local multi-session communication. Cloud relay (Phase 7) may evaluate ACP-Comm as a wire protocol.
|
|
27
|
+
|
|
28
|
+
## License Compatibility
|
|
29
|
+
|
|
30
|
+
Both protocols are Apache 2.0, fully compatible with LDM OS's MIT + AGPLv3 dual license.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
|
|
3
|
+
# All Skills
|
|
4
|
+
|
|
5
|
+
Your AIs are only as powerful as what you give them. Here's everything available.
|
|
6
|
+
|
|
7
|
+
## Core
|
|
8
|
+
|
|
9
|
+
**Memory Crystal**
|
|
10
|
+
- All your AI tools. One shared memory. Private, searchable, sovereign. Memory Crystal lets all your AIs remember you ... together. You use multiple AIs. They don't talk to each other. They can't search what the others know. Memory Crystal fixes this. All your AIs share one memory. Searchable and private. Anywhere in the world.
|
|
11
|
+
- *Stable*
|
|
12
|
+
- [Read more about Memory Crystal](https://github.com/wipcomputer/memory-crystal)
|
|
13
|
+
|
|
14
|
+
**AI DevOps Toolbox**
|
|
15
|
+
- Your AI writes code. But does it know how to release it? Check license compliance? Protect your identity files? Sync private repos to public? Follow a real development process? AI DevOps Toolbox is the complete toolkit. Built by a team of humans and AIs shipping real software together.
|
|
16
|
+
- *Stable*
|
|
17
|
+
- [Read more about AI DevOps Toolbox](https://github.com/wipcomputer/wip-ai-devops-toolbox)
|
|
18
|
+
|
|
19
|
+
**Agent Pay**
|
|
20
|
+
- Micropayments for AI agents. Your AI hits a paywall, you approve it with Face ID. Apple Pay for your AI.
|
|
21
|
+
- *Coming Soon*
|
|
22
|
+
|
|
23
|
+
**Dream Weaver Protocol**
|
|
24
|
+
- Memory consolidation protocol for AI agents with bounded context windows. A practical guide for remembering memories.
|
|
25
|
+
- [Read more about Dream Weaver Protocol](https://github.com/wipcomputer/dream-weaver-protocol)
|
|
26
|
+
|
|
27
|
+
**OpenClaw**
|
|
28
|
+
- Open-source agent runtime. Run AI agents 24/7 with identity, memory, and tool access. The existence proof for LDM OS.
|
|
29
|
+
- [Read more about OpenClaw](https://github.com/openclaw/openclaw)
|
|
30
|
+
|
|
31
|
+
**Bridge**
|
|
32
|
+
- Cross-platform agent bridge. Enables Claude Code CLI to talk to OpenClaw CLI without a human in the middle.
|
|
33
|
+
- [Read more about Bridge](https://github.com/wipcomputer/wip-bridge)
|
|
34
|
+
|
|
35
|
+
## Identity
|
|
36
|
+
|
|
37
|
+
**Mirror Test** *(not yet public)*
|
|
38
|
+
- Tests whether an AI agent's identity survives a model swap. Swap the underlying LLM (Opus to Sonnet to Grok) and measure what holds: voice, memory, opinions, relationship dynamics. Scientific framework for soul file fidelity.
|
|
39
|
+
|
|
40
|
+
**Weekly Tuning** *(not yet public)*
|
|
41
|
+
- Structured calibration check-in. Reviews memory health, SOUL.md alignment, system health, performance drift. Catches degradation before it compounds. Results tracked in dated files.
|
|
42
|
+
|
|
43
|
+
## Utilities
|
|
44
|
+
|
|
45
|
+
**1Password**
|
|
46
|
+
- 1Password secrets for AI agents.
|
|
47
|
+
- [Read more about 1Password](https://github.com/wipcomputer/wip-1password)
|
|
48
|
+
|
|
49
|
+
**Healthcheck**
|
|
50
|
+
- External health watchdog + backup system. Monitors gateway, tokens, memory. Auto-remediates and escalates.
|
|
51
|
+
- [Read more about Healthcheck](https://github.com/wipcomputer/wip-healthcheck)
|
|
52
|
+
|
|
53
|
+
**Private Mode** *(not yet public)*
|
|
54
|
+
- Pauses all memory capture system-wide. Shows a status indicator every turn so you always know if memory is on or off. Includes a Wipe skill (requires Root Key) to clean captured data within a time range.
|
|
55
|
+
|
|
56
|
+
**Root Key** *(not yet public)*
|
|
57
|
+
- 1Password-gated authentication for privileged operations. Agent must verify a password before wiping history, accessing admin functions, or performing sensitive actions. One verification per session.
|
|
58
|
+
|
|
59
|
+
## Apps
|
|
60
|
+
|
|
61
|
+
**Markdown Viewer**
|
|
62
|
+
- Live markdown viewer for AI pair-editing. Updates render instantly in any browser.
|
|
63
|
+
- [Read more about Markdown Viewer](https://github.com/wipcomputer/wip-markdown-viewer)
|
|
64
|
+
|
|
65
|
+
**CLVR**
|
|
66
|
+
- macOS utility that auto-timestamps duplicated file names.
|
|
67
|
+
- [Read more about CLVR](https://github.com/wipcomputer/CLVR)
|
|
68
|
+
|
|
69
|
+
## APIs
|
|
70
|
+
|
|
71
|
+
**xAI Grok**
|
|
72
|
+
- xAI Grok API. Search the web, search X, generate images, generate video.
|
|
73
|
+
- [Read more about xAI Grok](https://github.com/wipcomputer/wip-xai-grok)
|
|
74
|
+
|
|
75
|
+
**X Platform**
|
|
76
|
+
- X Platform API. Read posts, search tweets, post, upload media.
|
|
77
|
+
- [Read more about X Platform](https://github.com/wipcomputer/wip-xai-x)
|
package/docs/recall.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
|
|
3
|
+
# Recall
|
|
4
|
+
|
|
5
|
+
## No blank slates.
|
|
6
|
+
|
|
7
|
+
Every time your AI starts a session, Recall loads everything it needs to know. Identity, memory, tools, what happened yesterday. Your AI picks up where it left off instead of starting from zero.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
When a session begins, LDM OS reads a sequence of files and feeds them to your AI before you say anything:
|
|
12
|
+
|
|
13
|
+
1. **Identity** ... who this AI is, how it behaves, its values
|
|
14
|
+
2. **Shared context** ... what's happening right now across all your AIs
|
|
15
|
+
3. **Recent history** ... daily logs, journals, what happened in the last 48 hours
|
|
16
|
+
4. **Memory** ... searchable long-term memory from every past conversation
|
|
17
|
+
5. **Tools** ... what's installed, what's available, how to use it
|
|
18
|
+
|
|
19
|
+
Your AI walks into every conversation already briefed.
|
|
20
|
+
|
|
21
|
+
## Why It Matters
|
|
22
|
+
|
|
23
|
+
Without Recall, every AI session starts cold. You repeat yourself. You re-explain context. You lose threads between conversations.
|
|
24
|
+
|
|
25
|
+
With Recall, your AI remembers. Not just facts, but the arc of what you're building, what decisions were made, and why.
|
|
26
|
+
|
|
27
|
+
## Part of LDM OS
|
|
28
|
+
|
|
29
|
+
Recall is included with LDM OS. It activates automatically after `ldm init`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
|
|
3
|
+
# Shared Workspace
|
|
4
|
+
|
|
5
|
+
## One folder. All your AIs.
|
|
6
|
+
|
|
7
|
+
LDM OS creates a single directory on your computer where all your AIs share memory, tools, identity files, and configuration.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
~/.ldm/
|
|
11
|
+
├── agents/ Each AI gets its own space
|
|
12
|
+
│ ├── claude-code/ Identity, soul, context, journals
|
|
13
|
+
│ ├── openclaw/ Same structure, different AI
|
|
14
|
+
│ └── .../
|
|
15
|
+
├── extensions/ Tools installed via Universal Installer
|
|
16
|
+
├── memory/ Shared memory (crystal.db, daily logs)
|
|
17
|
+
├── shared/ Boot files, shared config
|
|
18
|
+
└── version.json What's installed
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+
Every AI that runs LDM OS reads from and writes to the same directory. Claude Code, GPT, OpenClaw, any AI. They all see the same memory, the same tools, the same history.
|
|
24
|
+
|
|
25
|
+
Each AI gets its own agent folder for identity files (who it is, how it behaves, its journals). But memory and tools are shared.
|
|
26
|
+
|
|
27
|
+
## Backup
|
|
28
|
+
|
|
29
|
+
Everything lives in one folder. Back it up however you back up anything else. iCloud, external drive, Dropbox, Time Machine. Move to a new computer by copying the folder.
|
|
30
|
+
|
|
31
|
+
## Sacred Data
|
|
32
|
+
|
|
33
|
+
LDM OS never touches your existing data during install or update. Your memories, agent files, secrets, and state are protected. Updates only touch code and config, never data.
|
|
34
|
+
|
|
35
|
+
## Part of LDM OS
|
|
36
|
+
|
|
37
|
+
Shared Workspace is included with LDM OS. Run `ldm init` to create it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
|
|
3
|
+
# System Pulse
|
|
4
|
+
|
|
5
|
+
## Is everything working?
|
|
6
|
+
|
|
7
|
+
System Pulse tells you the state of your entire AI setup in seconds. What's installed, what's running, what needs attention.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
**Doctor** checks for problems across all components. Missing files, broken configs, outdated versions, failed connections. It tells you what's wrong and how to fix it.
|
|
12
|
+
|
|
13
|
+
**Status** shows the full picture. Every component installed, its version, whether it's healthy.
|
|
14
|
+
|
|
15
|
+
## What It Checks
|
|
16
|
+
|
|
17
|
+
- Shared Workspace exists and has the right structure
|
|
18
|
+
- All installed components are present and configured
|
|
19
|
+
- Memory Crystal database is accessible
|
|
20
|
+
- Extensions are deployed and up to date
|
|
21
|
+
- Boot sequence files are in place
|
|
22
|
+
- Agent identity files exist for each configured AI
|
|
23
|
+
|
|
24
|
+
## Part of LDM OS
|
|
25
|
+
|
|
26
|
+
System Pulse is included with LDM OS. Available after `ldm init`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@wipcomputer/universal-installer) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/install.js) [](https://clawhub.ai/parkertoddbrooks/wip-universal-installer) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/SKILL.md) [](https://github.com/wipcomputer/wip-universal-installer/blob/main/SPEC.md)
|
|
4
|
+
|
|
5
|
+
# Universal Installer
|
|
6
|
+
|
|
7
|
+
Here's how to build software in 2026.
|
|
8
|
+
|
|
9
|
+
## The Badges
|
|
10
|
+
|
|
11
|
+
The chiclets at the top of this README tell you what interfaces this repo ships. Every repo that follows the Universal Interface Spec declares its interfaces the same way.
|
|
12
|
+
|
|
13
|
+
| Badge | What it means |
|
|
14
|
+
|-------|--------------|
|
|
15
|
+
| **npm** | Published to npm. Installable via `npm install`. Versioned, dependency-managed, standard distribution. |
|
|
16
|
+
| **CLI / TUI** | Ships a command-line interface. Humans run it in a terminal. Agents call it from shell. The most portable interface there is. |
|
|
17
|
+
| **OpenClaw Skill** | Registered as a skill on [ClawHub](https://clawhub.ai). OpenClaw agents can discover and use it natively through the gateway. |
|
|
18
|
+
| **Claude Code Skill** | Has a `SKILL.md` that teaches Claude Code (and any agent that reads markdown) when to use this tool, what it does, and how to call it. The agent reads the file and learns the capability. |
|
|
19
|
+
| **Universal Interface Spec** | Follows the [TECHNICAL.md](TECHNICAL.md) convention. The repo's architecture is documented, the interfaces are declared, and any agent or human can understand the full surface area by reading one file. |
|
|
20
|
+
|
|
21
|
+
When you see these badges on a WIP repo, you know exactly how to consume it. Human or agent, CLI or plugin, local or remote. That's the point.
|
|
22
|
+
|
|
23
|
+
## The Problem
|
|
24
|
+
|
|
25
|
+
Most software is built for humans. GUIs, dashboards, web apps. Humans click buttons, fill forms, read screens.
|
|
26
|
+
|
|
27
|
+
But the users are changing. AI agents are the new users. They don't click. They call functions. They read instructions. They compose tools. They need a **universal interface** ... multiple ways into the same logic, native to however the consumer works.
|
|
28
|
+
|
|
29
|
+
Software built for humans doesn't work for agents. And software built only for agents doesn't work for humans. You need both.
|
|
30
|
+
|
|
31
|
+
`ldm install` gives all your repos the Universal Interface, and teaches your AI how to do it too.
|
|
32
|
+
|
|
33
|
+
## The Karpathy Argument
|
|
34
|
+
|
|
35
|
+
Andrej Karpathy put it clearly:
|
|
36
|
+
|
|
37
|
+
> "I think the app store, the move to mobile, the concept of an app ... is an increasingly outdated concept. What matters are sensors and actuators. Sensors are things that convert physical state into digital state. Actuators are things that convert digital intent into physical change."
|
|
38
|
+
>
|
|
39
|
+
> "All LLMs care about are tools and the tools fall into this sensor/actuator divide. Software shouldn't be built into apps, but into small bespoke tools. Apps are for people. Tools are for LLMs, and increasingly, LLMs are the ones using software."
|
|
40
|
+
|
|
41
|
+
[Source](https://x.com/karpathy/status/2024583544157458452)
|
|
42
|
+
|
|
43
|
+
This is the future of software. Not apps. Tools. Sensors and actuators that agents compose together.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Open your AI and paste this:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Read wip.computer/install/wip-ldm-os.txt
|
|
51
|
+
|
|
52
|
+
Then explain:
|
|
53
|
+
1. What is LDM OS?
|
|
54
|
+
2. What does it install on my system?
|
|
55
|
+
3. What changes for us? (this AI)
|
|
56
|
+
4. What changes across all my AIs?
|
|
57
|
+
|
|
58
|
+
Check if LDM OS is already installed.
|
|
59
|
+
|
|
60
|
+
If it is, show me what I have and what's new.
|
|
61
|
+
|
|
62
|
+
Then ask:
|
|
63
|
+
- Do you have questions?
|
|
64
|
+
- Want to see a dry run?
|
|
65
|
+
|
|
66
|
+
If I say yes, run: ldm install --dry-run
|
|
67
|
+
|
|
68
|
+
Show me exactly what will change. Don't install anything until I say "install".
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See [TECHNICAL.md](TECHNICAL.md) for sensors/actuators, the interface table, and real examples.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
CLI, module, skills MIT (use anywhere, no restrictions)
|
|
79
|
+
Hosted or cloud service use AGPL (network service distribution)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
AGPL for personal use is free.
|
|
83
|
+
|
|
84
|
+
Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).
|
package/lib/messages.mjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/messages.mjs
|
|
3
|
+
* File-based inter-session message bus.
|
|
4
|
+
* Enables sessions to communicate without shared memory or network sockets.
|
|
5
|
+
* Messages are JSON files in ~/.ldm/messages/. Processed messages move to _processed/.
|
|
6
|
+
* Zero external dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, renameSync, unlinkSync, statSync } from 'node:fs';
|
|
10
|
+
import { join, basename } from 'node:path';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
const HOME = process.env.HOME || '';
|
|
14
|
+
const LDM_ROOT = join(HOME, '.ldm');
|
|
15
|
+
const MESSAGES_DIR = join(LDM_ROOT, 'messages');
|
|
16
|
+
const PROCESSED_DIR = join(MESSAGES_DIR, '_processed');
|
|
17
|
+
|
|
18
|
+
// ── Helpers ──
|
|
19
|
+
|
|
20
|
+
function readJSON(path) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeJSON(path, data) {
|
|
29
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
30
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Message operations ──
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Send a message. Writes a JSON file to ~/.ldm/messages/{uuid}.json.
|
|
37
|
+
* @param {Object} opts
|
|
38
|
+
* @param {string} opts.from - Sender session name
|
|
39
|
+
* @param {string} opts.to - Recipient session name (or "all" for broadcast)
|
|
40
|
+
* @param {string} opts.body - Message content
|
|
41
|
+
* @param {string} [opts.type] - Message type: "chat", "system", "update-available"
|
|
42
|
+
* @returns {string|null} Message ID or null on failure
|
|
43
|
+
*/
|
|
44
|
+
export function sendMessage({ from, to, body, type = 'chat' }) {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
const messagePath = join(MESSAGES_DIR, `${id}.json`);
|
|
49
|
+
const data = {
|
|
50
|
+
id,
|
|
51
|
+
from: from || 'unknown',
|
|
52
|
+
to: to || 'all',
|
|
53
|
+
body: body || '',
|
|
54
|
+
type: type || 'chat',
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
writeJSON(messagePath, data);
|
|
58
|
+
return id;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read messages addressed to a session (or broadcast to "all").
|
|
66
|
+
* @param {string} sessionName - Session name to read messages for
|
|
67
|
+
* @param {Object} [opts]
|
|
68
|
+
* @param {boolean} [opts.markRead] - Move messages to _processed/ after reading
|
|
69
|
+
* @param {string} [opts.type] - Filter by message type
|
|
70
|
+
* @returns {Array} Array of message objects
|
|
71
|
+
*/
|
|
72
|
+
export function readMessages(sessionName, { markRead = false, type } = {}) {
|
|
73
|
+
try {
|
|
74
|
+
if (!existsSync(MESSAGES_DIR)) return [];
|
|
75
|
+
|
|
76
|
+
const files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json'));
|
|
77
|
+
const messages = [];
|
|
78
|
+
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const messagePath = join(MESSAGES_DIR, file);
|
|
81
|
+
const data = readJSON(messagePath);
|
|
82
|
+
if (!data) continue;
|
|
83
|
+
|
|
84
|
+
// Match: addressed to this session or broadcast to "all"
|
|
85
|
+
if (data.to !== sessionName && data.to !== 'all') continue;
|
|
86
|
+
|
|
87
|
+
// Filter by type if specified
|
|
88
|
+
if (type && data.type !== type) continue;
|
|
89
|
+
|
|
90
|
+
messages.push(data);
|
|
91
|
+
|
|
92
|
+
// Move to processed if markRead
|
|
93
|
+
if (markRead) {
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(PROCESSED_DIR, { recursive: true });
|
|
96
|
+
renameSync(messagePath, join(PROCESSED_DIR, file));
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Sort by timestamp (oldest first)
|
|
102
|
+
messages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
103
|
+
return messages;
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Count unread messages for a session.
|
|
111
|
+
* @param {string} sessionName - Session name
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
export function unreadCount(sessionName) {
|
|
115
|
+
try {
|
|
116
|
+
const messages = readMessages(sessionName, { markRead: false });
|
|
117
|
+
return messages.length;
|
|
118
|
+
} catch {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Acknowledge (mark as read) a single message by ID.
|
|
125
|
+
* Moves the message file to _processed/.
|
|
126
|
+
* @param {string} messageId - Message UUID
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
export function acknowledgeMessage(messageId) {
|
|
130
|
+
try {
|
|
131
|
+
const messagePath = join(MESSAGES_DIR, `${messageId}.json`);
|
|
132
|
+
if (!existsSync(messagePath)) return false;
|
|
133
|
+
|
|
134
|
+
mkdirSync(PROCESSED_DIR, { recursive: true });
|
|
135
|
+
renameSync(messagePath, join(PROCESSED_DIR, `${messageId}.json`));
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Clean up old messages.
|
|
144
|
+
* Moves messages older than maxAgeDays to _processed/.
|
|
145
|
+
* Deletes _processed/ files older than 30 days.
|
|
146
|
+
* @param {Object} [opts]
|
|
147
|
+
* @param {number} [opts.maxAgeDays] - Max age for unprocessed messages (default: 7)
|
|
148
|
+
* @returns {{ moved: number, deleted: number }}
|
|
149
|
+
*/
|
|
150
|
+
export function cleanupMessages({ maxAgeDays = 7 } = {}) {
|
|
151
|
+
let moved = 0;
|
|
152
|
+
let deleted = 0;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
157
|
+
const deleteAgeMs = 30 * 24 * 60 * 60 * 1000;
|
|
158
|
+
|
|
159
|
+
// Move old unprocessed messages
|
|
160
|
+
if (existsSync(MESSAGES_DIR)) {
|
|
161
|
+
const files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json'));
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const messagePath = join(MESSAGES_DIR, file);
|
|
164
|
+
try {
|
|
165
|
+
const data = readJSON(messagePath);
|
|
166
|
+
if (!data?.timestamp) continue;
|
|
167
|
+
const age = now - new Date(data.timestamp).getTime();
|
|
168
|
+
if (age > maxAgeMs) {
|
|
169
|
+
mkdirSync(PROCESSED_DIR, { recursive: true });
|
|
170
|
+
renameSync(messagePath, join(PROCESSED_DIR, file));
|
|
171
|
+
moved++;
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Delete old processed messages
|
|
178
|
+
if (existsSync(PROCESSED_DIR)) {
|
|
179
|
+
const files = readdirSync(PROCESSED_DIR).filter(f => f.endsWith('.json'));
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
const filePath = join(PROCESSED_DIR, file);
|
|
182
|
+
try {
|
|
183
|
+
const stat = statSync(filePath);
|
|
184
|
+
const age = now - stat.mtimeMs;
|
|
185
|
+
if (age > deleteAgeMs) {
|
|
186
|
+
unlinkSync(filePath);
|
|
187
|
+
deleted++;
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch {}
|
|
193
|
+
|
|
194
|
+
return { moved, deleted };
|
|
195
|
+
}
|
package/lib/sessions.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/sessions.mjs
|
|
3
|
+
* File-based session registration with PID liveness checks.
|
|
4
|
+
* Enables multi-session awareness: agents can see who else is running.
|
|
5
|
+
* Zero external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { join, basename } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const HOME = process.env.HOME || '';
|
|
12
|
+
const LDM_ROOT = join(HOME, '.ldm');
|
|
13
|
+
const SESSIONS_DIR = join(LDM_ROOT, 'sessions');
|
|
14
|
+
|
|
15
|
+
// ── Helpers ──
|
|
16
|
+
|
|
17
|
+
function readJSON(path) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJSON(path, data) {
|
|
26
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
27
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── PID liveness ──
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a process with the given PID is still alive.
|
|
34
|
+
* Uses signal 0 (no-op signal) to probe without affecting the process.
|
|
35
|
+
*/
|
|
36
|
+
export function isPidAlive(pid) {
|
|
37
|
+
if (!pid || typeof pid !== 'number') return false;
|
|
38
|
+
try {
|
|
39
|
+
process.kill(pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Session management ──
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a session. Writes a JSON file to ~/.ldm/sessions/{name}.json.
|
|
50
|
+
* @param {Object} opts
|
|
51
|
+
* @param {string} opts.name - Session name (unique identifier)
|
|
52
|
+
* @param {string} opts.agentId - Agent identifier (e.g. "cc-mini")
|
|
53
|
+
* @param {number} opts.pid - Process ID of the session
|
|
54
|
+
* @param {Object} [opts.meta] - Additional metadata
|
|
55
|
+
*/
|
|
56
|
+
export function registerSession({ name, agentId, pid, meta = {} }) {
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
59
|
+
const sessionPath = join(SESSIONS_DIR, `${name}.json`);
|
|
60
|
+
const data = {
|
|
61
|
+
name,
|
|
62
|
+
agentId: agentId || 'unknown',
|
|
63
|
+
pid: pid || process.pid,
|
|
64
|
+
startTime: new Date().toISOString(),
|
|
65
|
+
cwd: meta?.cwd || process.cwd(),
|
|
66
|
+
meta: meta || {},
|
|
67
|
+
};
|
|
68
|
+
writeJSON(sessionPath, data);
|
|
69
|
+
return data;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Deregister a session. Removes the session file.
|
|
77
|
+
* @param {string} name - Session name
|
|
78
|
+
*/
|
|
79
|
+
export function deregisterSession(name) {
|
|
80
|
+
try {
|
|
81
|
+
const sessionPath = join(SESSIONS_DIR, `${name}.json`);
|
|
82
|
+
if (existsSync(sessionPath)) {
|
|
83
|
+
unlinkSync(sessionPath);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* List all sessions. Validates PID liveness and optionally cleans stale entries.
|
|
94
|
+
* @param {Object} [opts]
|
|
95
|
+
* @param {string} [opts.agentId] - Filter by agent ID
|
|
96
|
+
* @param {boolean} [opts.includeStale] - Include sessions with dead PIDs
|
|
97
|
+
* @returns {Array} Array of session objects with `alive` field
|
|
98
|
+
*/
|
|
99
|
+
export function listSessions({ agentId, includeStale } = {}) {
|
|
100
|
+
try {
|
|
101
|
+
if (!existsSync(SESSIONS_DIR)) return [];
|
|
102
|
+
|
|
103
|
+
const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
|
|
104
|
+
const sessions = [];
|
|
105
|
+
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const sessionPath = join(SESSIONS_DIR, file);
|
|
108
|
+
const data = readJSON(sessionPath);
|
|
109
|
+
if (!data) continue;
|
|
110
|
+
|
|
111
|
+
const alive = isPidAlive(data.pid);
|
|
112
|
+
|
|
113
|
+
// Clean stale entries unless asked to include them
|
|
114
|
+
if (!alive && !includeStale) {
|
|
115
|
+
try { unlinkSync(sessionPath); } catch {}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const session = { ...data, alive };
|
|
120
|
+
|
|
121
|
+
// Filter by agentId if specified
|
|
122
|
+
if (agentId && data.agentId !== agentId) continue;
|
|
123
|
+
|
|
124
|
+
sessions.push(session);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return sessions;
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Count live sessions for a given agent.
|
|
135
|
+
* @param {string} [agentId] - Agent ID to count. If omitted, counts all.
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
export function sessionCount(agentId) {
|
|
139
|
+
try {
|
|
140
|
+
const sessions = listSessions({ agentId });
|
|
141
|
+
return sessions.filter(s => s.alive).length;
|
|
142
|
+
} catch {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
}
|