@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.
@@ -0,0 +1,74 @@
1
+ declare const LDM_ROOT: string;
2
+ interface BridgeConfig {
3
+ openclawDir: string;
4
+ workspaceDir: string;
5
+ dbPath: string;
6
+ inboxPort: number;
7
+ embeddingModel: string;
8
+ embeddingDimensions: number;
9
+ }
10
+ interface GatewayConfig {
11
+ token: string;
12
+ port: number;
13
+ }
14
+ interface InboxMessage {
15
+ from: string;
16
+ message: string;
17
+ timestamp: string;
18
+ }
19
+ interface ConversationResult {
20
+ text: string;
21
+ role: string;
22
+ sessionKey: string;
23
+ date: string;
24
+ similarity?: number;
25
+ recencyScore?: number;
26
+ freshness?: "fresh" | "recent" | "aging" | "stale";
27
+ }
28
+ interface WorkspaceSearchResult {
29
+ path: string;
30
+ excerpts: string[];
31
+ score: number;
32
+ }
33
+ declare function resolveConfig(overrides?: Partial<BridgeConfig>): BridgeConfig;
34
+ /**
35
+ * Multi-config resolver. Checks ~/.ldm/config.json first, falls back to OPENCLAW_DIR.
36
+ * This is the LDM OS native path. resolveConfig() is the legacy OpenClaw path.
37
+ * Both return the same BridgeConfig shape.
38
+ */
39
+ declare function resolveConfigMulti(overrides?: Partial<BridgeConfig>): BridgeConfig;
40
+ declare function resolveApiKey(openclawDir: string): string | null;
41
+ declare function resolveGatewayConfig(openclawDir: string): GatewayConfig;
42
+ declare function pushInbox(msg: InboxMessage): number;
43
+ declare function drainInbox(): InboxMessage[];
44
+ declare function inboxCount(): number;
45
+ declare function sendMessage(openclawDir: string, message: string, options?: {
46
+ agentId?: string;
47
+ user?: string;
48
+ senderLabel?: string;
49
+ }): Promise<string>;
50
+ declare function getQueryEmbedding(text: string, apiKey: string, model?: string, dimensions?: number): Promise<number[]>;
51
+ declare function blobToEmbedding(blob: Buffer): number[];
52
+ declare function cosineSimilarity(a: number[], b: number[]): number;
53
+ declare function searchConversations(config: BridgeConfig, query: string, limit?: number): Promise<ConversationResult[]>;
54
+ declare function findMarkdownFiles(dir: string, maxDepth?: number, depth?: number): string[];
55
+ declare function searchWorkspace(workspaceDir: string, query: string): WorkspaceSearchResult[];
56
+ interface WorkspaceFileResult {
57
+ content: string;
58
+ relativePath: string;
59
+ }
60
+ interface SkillInfo {
61
+ name: string;
62
+ description: string;
63
+ skillDir: string;
64
+ hasScripts: boolean;
65
+ scripts: string[];
66
+ source: "builtin" | "custom";
67
+ emoji?: string;
68
+ requires?: Record<string, string[]>;
69
+ }
70
+ declare function discoverSkills(openclawDir: string): SkillInfo[];
71
+ declare function executeSkillScript(skillDir: string, scripts: string[], scriptName: string | undefined, args: string): Promise<string>;
72
+ declare function readWorkspaceFile(workspaceDir: string, filePath: string): WorkspaceFileResult;
73
+
74
+ export { type BridgeConfig, type ConversationResult, type GatewayConfig, type InboxMessage, LDM_ROOT, type SkillInfo, type WorkspaceFileResult, type WorkspaceSearchResult, blobToEmbedding, cosineSimilarity, discoverSkills, drainInbox, executeSkillScript, findMarkdownFiles, getQueryEmbedding, inboxCount, pushInbox, readWorkspaceFile, resolveApiKey, resolveConfig, resolveConfigMulti, resolveGatewayConfig, searchConversations, searchWorkspace, sendMessage };
@@ -0,0 +1,40 @@
1
+ import {
2
+ LDM_ROOT,
3
+ blobToEmbedding,
4
+ cosineSimilarity,
5
+ discoverSkills,
6
+ drainInbox,
7
+ executeSkillScript,
8
+ findMarkdownFiles,
9
+ getQueryEmbedding,
10
+ inboxCount,
11
+ pushInbox,
12
+ readWorkspaceFile,
13
+ resolveApiKey,
14
+ resolveConfig,
15
+ resolveConfigMulti,
16
+ resolveGatewayConfig,
17
+ searchConversations,
18
+ searchWorkspace,
19
+ sendMessage
20
+ } from "./chunk-KWGJCDGS.js";
21
+ export {
22
+ LDM_ROOT,
23
+ blobToEmbedding,
24
+ cosineSimilarity,
25
+ discoverSkills,
26
+ drainInbox,
27
+ executeSkillScript,
28
+ findMarkdownFiles,
29
+ getQueryEmbedding,
30
+ inboxCount,
31
+ pushInbox,
32
+ readWorkspaceFile,
33
+ resolveApiKey,
34
+ resolveConfig,
35
+ resolveConfigMulti,
36
+ resolveGatewayConfig,
37
+ searchConversations,
38
+ searchWorkspace,
39
+ sendMessage
40
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,284 @@
1
+ import {
2
+ discoverSkills,
3
+ drainInbox,
4
+ executeSkillScript,
5
+ inboxCount,
6
+ pushInbox,
7
+ readWorkspaceFile,
8
+ resolveConfig,
9
+ searchConversations,
10
+ searchWorkspace,
11
+ sendMessage
12
+ } from "./chunk-KWGJCDGS.js";
13
+
14
+ // mcp-server.ts
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { createServer } from "http";
18
+ import { appendFileSync, mkdirSync } from "fs";
19
+ import { join } from "path";
20
+ import { z } from "zod";
21
+ var config = resolveConfig();
22
+ var METRICS_DIR = join(process.env.HOME || "/Users/lesa", ".openclaw", "memory");
23
+ var METRICS_PATH = join(METRICS_DIR, "search-metrics.jsonl");
24
+ function logSearchMetric(tool, query, resultCount) {
25
+ try {
26
+ mkdirSync(METRICS_DIR, { recursive: true });
27
+ const entry = JSON.stringify({
28
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
29
+ tool,
30
+ query,
31
+ results: resultCount
32
+ });
33
+ appendFileSync(METRICS_PATH, entry + "\n");
34
+ } catch {
35
+ }
36
+ }
37
+ function readBody(req) {
38
+ return new Promise((resolve, reject) => {
39
+ const chunks = [];
40
+ req.on("data", (chunk) => chunks.push(chunk));
41
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
42
+ req.on("error", reject);
43
+ });
44
+ }
45
+ function startInboxServer(cfg) {
46
+ const httpServer = createServer(async (req, res) => {
47
+ const remoteAddr = req.socket.remoteAddress;
48
+ if (remoteAddr !== "127.0.0.1" && remoteAddr !== "::1" && remoteAddr !== "::ffff:127.0.0.1") {
49
+ res.writeHead(403, { "Content-Type": "application/json" });
50
+ res.end(JSON.stringify({ error: "localhost only" }));
51
+ return;
52
+ }
53
+ if (req.method === "POST" && req.url === "/message") {
54
+ try {
55
+ const body = JSON.parse(await readBody(req));
56
+ const msg = {
57
+ from: body.from || "agent",
58
+ message: body.message,
59
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
60
+ };
61
+ const queued = pushInbox(msg);
62
+ console.error(`lesa-bridge inbox: message from ${msg.from}`);
63
+ try {
64
+ server.sendLoggingMessage({
65
+ level: "info",
66
+ logger: "lesa-bridge",
67
+ data: `[OpenClaw \u2192 Claude Code] ${msg.from}: ${msg.message}`
68
+ });
69
+ } catch {
70
+ }
71
+ res.writeHead(200, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({ ok: true, queued }));
73
+ } catch (err) {
74
+ res.writeHead(400, { "Content-Type": "application/json" });
75
+ res.end(JSON.stringify({ error: err.message }));
76
+ }
77
+ return;
78
+ }
79
+ if (req.method === "GET" && req.url === "/status") {
80
+ res.writeHead(200, { "Content-Type": "application/json" });
81
+ res.end(JSON.stringify({ ok: true, pending: inboxCount() }));
82
+ return;
83
+ }
84
+ res.writeHead(404, { "Content-Type": "application/json" });
85
+ res.end(JSON.stringify({ error: "not found" }));
86
+ });
87
+ httpServer.listen(cfg.inboxPort, "127.0.0.1", () => {
88
+ console.error(`lesa-bridge inbox listening on 127.0.0.1:${cfg.inboxPort}`);
89
+ });
90
+ httpServer.on("error", (err) => {
91
+ console.error(`lesa-bridge inbox server error: ${err.message}`);
92
+ });
93
+ }
94
+ var server = new McpServer({
95
+ name: "lesa-bridge",
96
+ version: "0.3.0"
97
+ });
98
+ server.registerTool(
99
+ "lesa_conversation_search",
100
+ {
101
+ description: "Search embedded conversation history. Returns semantically similar excerpts from past conversations. Use this to find what was discussed about a topic, decisions made, or technical details from earlier sessions.",
102
+ inputSchema: {
103
+ query: z.string().describe("What to search for in past conversations"),
104
+ limit: z.number().optional().default(5).describe("Max results to return (default: 5)")
105
+ }
106
+ },
107
+ async ({ query, limit }) => {
108
+ try {
109
+ const results = await searchConversations(config, query, limit);
110
+ logSearchMetric("lesa_conversation_search", query, results.length);
111
+ if (results.length === 0) {
112
+ return { content: [{ type: "text", text: "No conversation history found." }] };
113
+ }
114
+ const hasEmbeddings = results[0].similarity !== void 0;
115
+ const freshnessIcon = { fresh: "\u{1F7E2}", recent: "\u{1F7E1}", aging: "\u{1F7E0}", stale: "\u{1F534}" };
116
+ const text = results.map((r, i) => {
117
+ const sim = r.similarity !== void 0 ? `score: ${r.similarity.toFixed(3)}, ` : "";
118
+ const fresh = r.freshness ? `${freshnessIcon[r.freshness]} ${r.freshness}, ` : "";
119
+ return `[${i + 1}] (${fresh}${sim}session: ${r.sessionKey}, date: ${r.date})
120
+ ${r.text}`;
121
+ }).join("\n\n---\n\n");
122
+ const prefix = hasEmbeddings ? "(Recency-weighted. \u{1F7E2} fresh <3d, \u{1F7E1} recent <7d, \u{1F7E0} aging <14d, \u{1F534} stale 14d+)\n\n" : "(Text search; no API key for semantic search)\n\n";
123
+ return { content: [{ type: "text", text: `${prefix}${text}` }] };
124
+ } catch (err) {
125
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
126
+ }
127
+ }
128
+ );
129
+ server.registerTool(
130
+ "lesa_memory_search",
131
+ {
132
+ description: "Search workspace memory files (MEMORY.md, daily logs, notes, identity docs). Returns matching excerpts from .md files. Use this to find written memory, observations, todos, and notes.",
133
+ inputSchema: {
134
+ query: z.string().describe("Keywords to search for in workspace files")
135
+ }
136
+ },
137
+ async ({ query }) => {
138
+ try {
139
+ const results = searchWorkspace(config.workspaceDir, query);
140
+ logSearchMetric("lesa_memory_search", query, results.length);
141
+ if (results.length === 0) {
142
+ return { content: [{ type: "text", text: `No workspace files matched "${query}".` }] };
143
+ }
144
+ const text = results.map((r) => {
145
+ const excerpts = r.excerpts.map((e) => ` ${e.replace(/\n/g, "\n ")}`).join("\n ...\n");
146
+ return `### ${r.path}
147
+ ${excerpts}`;
148
+ }).join("\n\n---\n\n");
149
+ return { content: [{ type: "text", text }] };
150
+ } catch (err) {
151
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
152
+ }
153
+ }
154
+ );
155
+ server.registerTool(
156
+ "lesa_read_workspace",
157
+ {
158
+ description: "Read a specific file from the agent workspace. Paths are relative to the workspace directory. Common files: MEMORY.md, IDENTITY.md, SOUL.md, USER.md, AGENTS.md, TOOLS.md, memory/YYYY-MM-DD.md (daily logs), memory/todos.md, memory/observations.md, notes/*.",
159
+ inputSchema: {
160
+ path: z.string().describe("File path relative to workspace/ (e.g. 'MEMORY.md', 'memory/2026-02-07.md')")
161
+ }
162
+ },
163
+ async ({ path: filePath }) => {
164
+ try {
165
+ const result = readWorkspaceFile(config.workspaceDir, filePath);
166
+ return { content: [{ type: "text", text: `# ${result.relativePath}
167
+
168
+ ${result.content}` }] };
169
+ } catch (err) {
170
+ return { content: [{ type: "text", text: err.message }], isError: !err.message.includes("Available files") };
171
+ }
172
+ }
173
+ );
174
+ server.registerTool(
175
+ "lesa_send_message",
176
+ {
177
+ description: "Send a message to the OpenClaw agent through the gateway. Routes through the agent's full pipeline: memory, tools, personality, workspace. Use this for direct communication: asking questions, sharing findings, coordinating work, or having a discussion. Messages are prefixed with [Claude Code] so the agent knows the source.",
178
+ inputSchema: {
179
+ message: z.string().describe("Message to send to the OpenClaw agent")
180
+ }
181
+ },
182
+ async ({ message }) => {
183
+ try {
184
+ const reply = await sendMessage(config.openclawDir, message);
185
+ return { content: [{ type: "text", text: reply }] };
186
+ } catch (err) {
187
+ return { content: [{ type: "text", text: `Error sending message: ${err.message}` }], isError: true };
188
+ }
189
+ }
190
+ );
191
+ server.registerTool(
192
+ "lesa_check_inbox",
193
+ {
194
+ description: "Check for pending messages from the OpenClaw agent. The agent can push messages via the inbox HTTP endpoint (POST localhost:18790/message). Call this to see if the agent has sent anything. Returns all pending messages and clears the queue.",
195
+ inputSchema: {}
196
+ },
197
+ async () => {
198
+ const messages = drainInbox();
199
+ if (messages.length === 0) {
200
+ return { content: [{ type: "text", text: "No pending messages." }] };
201
+ }
202
+ const text = messages.map((m) => `**${m.from}** (${m.timestamp}):
203
+ ${m.message}`).join("\n\n---\n\n");
204
+ return { content: [{ type: "text", text: `${messages.length} message(s):
205
+
206
+ ${text}` }] };
207
+ }
208
+ );
209
+ function registerSkillTools(skills) {
210
+ const executableSkills = skills.filter((s) => s.hasScripts);
211
+ const toolNameMap = /* @__PURE__ */ new Map();
212
+ for (const skill of executableSkills) {
213
+ const toolName = `oc_skill_${skill.name.replace(/-/g, "_")}`;
214
+ toolNameMap.set(toolName, skill);
215
+ const scriptList = skill.scripts.length > 1 ? ` Available scripts: ${skill.scripts.join(", ")}.` : "";
216
+ server.registerTool(
217
+ toolName,
218
+ {
219
+ description: `[OpenClaw skill] ${skill.description}${scriptList}`,
220
+ inputSchema: {
221
+ args: z.string().describe("Arguments to pass to the skill script (e.g. file paths, flags)"),
222
+ script: z.string().optional().describe(
223
+ skill.scripts.length > 1 ? `Which script to run: ${skill.scripts.join(", ")}. Defaults to first .sh script.` : "Script to run (optional, defaults to the only available script)"
224
+ )
225
+ }
226
+ },
227
+ async ({ args, script }) => {
228
+ try {
229
+ const result = await executeSkillScript(skill.skillDir, skill.scripts, script, args);
230
+ return { content: [{ type: "text", text: result }] };
231
+ } catch (err) {
232
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
233
+ }
234
+ }
235
+ );
236
+ }
237
+ server.registerTool(
238
+ "oc_skills_list",
239
+ {
240
+ description: "List all available OpenClaw skills and their descriptions. Skills with scripts can be called directly as oc_skill_{name} tools. Instruction-only skills describe how to use external CLIs.",
241
+ inputSchema: {
242
+ filter: z.string().optional().describe("Filter skills by name or description keyword")
243
+ }
244
+ },
245
+ async ({ filter }) => {
246
+ let filtered = skills;
247
+ if (filter) {
248
+ const f = filter.toLowerCase();
249
+ filtered = skills.filter(
250
+ (s) => s.name.toLowerCase().includes(f) || s.description.toLowerCase().includes(f)
251
+ );
252
+ }
253
+ if (filtered.length === 0) {
254
+ return { content: [{ type: "text", text: "No skills matched the filter." }] };
255
+ }
256
+ const lines = filtered.map((s) => {
257
+ const prefix = s.hasScripts ? `oc_skill_${s.name.replace(/-/g, "_")}` : "(instruction-only)";
258
+ const emoji = s.emoji ? `${s.emoji} ` : "";
259
+ return `- ${emoji}**${s.name}** [${prefix}]: ${s.description}`;
260
+ });
261
+ const header = `${filtered.length} skill(s)` + (filter ? ` matching "${filter}"` : "") + ` (${executableSkills.length} executable, ${skills.length - executableSkills.length} instruction-only)`;
262
+ return { content: [{ type: "text", text: `${header}
263
+
264
+ ${lines.join("\n")}` }] };
265
+ }
266
+ );
267
+ console.error(`lesa-bridge: registered ${executableSkills.length} skill tools + oc_skills_list (${skills.length} total skills)`);
268
+ }
269
+ async function main() {
270
+ startInboxServer(config);
271
+ try {
272
+ const skills = discoverSkills(config.openclawDir);
273
+ registerSkillTools(skills);
274
+ } catch (err) {
275
+ console.error(`lesa-bridge: skill discovery failed: ${err.message}`);
276
+ }
277
+ const transport = new StdioServerTransport();
278
+ await server.connect(transport);
279
+ console.error(`lesa-bridge MCP server running (openclaw: ${config.openclawDir})`);
280
+ }
281
+ main().catch((error) => {
282
+ console.error("Fatal error:", error);
283
+ process.exit(1);
284
+ });
@@ -0,0 +1,290 @@
1
+ ###### WIP Computer
2
+ # Universal Interface ... Technical Reference
3
+
4
+ Every tool is a sensor, an actuator, or both. Every tool should be accessible through multiple interfaces. We call this the Universal Interface.
5
+
6
+ ## Sensors and Actuators
7
+
8
+ **Sensors** convert state into data:
9
+ - Search the web (wip-grok search_web)
10
+ - Search X/Twitter (wip-grok search_x, wip-x search_recent)
11
+ - Fetch a post (wip-x fetch_post)
12
+ - Read bookmarks (wip-x get_bookmarks)
13
+ - Check system health (wip-healthcheck)
14
+
15
+ **Actuators** convert intent into action:
16
+ - Generate an image (wip-grok generate_image)
17
+ - Post a tweet (wip-x post_tweet)
18
+ - Guard a file from edits (wip-file-guard)
19
+ - Generate a video (wip-grok generate_video)
20
+
21
+ ## The Seven Interfaces
22
+
23
+ Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP. Some read markdown instructions.
24
+
25
+ So every tool should expose multiple interfaces into the same core logic:
26
+
27
+ | Interface | What | Who uses it |
28
+ |-----------|------|-------------|
29
+ | **CLI** | Shell command | Humans, any agent with bash |
30
+ | **Module** | ES import | Other tools, scripts |
31
+ | **MCP Server** | JSON-RPC over stdio | Claude Code, Cursor, any MCP client |
32
+ | **OpenClaw Plugin** | Lifecycle hooks + tools | OpenClaw agents |
33
+ | **Skill** | Markdown instructions (SKILL.md) | Any agent that reads files |
34
+ | **Claude Code Hook** | PreToolUse/Stop events | Claude Code |
35
+ | **Claude Code Plugin** | Distributable package (skills, agents, hooks, MCP, LSP) | Claude Code marketplace |
36
+
37
+ Not every tool needs all seven. Build what makes sense. But the more interfaces you expose, the more agents can use your tool.
38
+
39
+ ### 1. CLI
40
+
41
+ A shell command. The most universal interface. If it has a terminal, it works.
42
+
43
+ **Convention:** `package.json` with a `bin` field.
44
+
45
+ **Detection:** `pkg.bin` exists.
46
+
47
+ **Install:** `npm install -g .` or `npm link`.
48
+
49
+ ```json
50
+ {
51
+ "bin": {
52
+ "wip-grok": "./cli.mjs"
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### 2. Module
58
+
59
+ An importable ES module. The programmatic interface. Other tools compose with it.
60
+
61
+ **Convention:** `package.json` with `main` or `exports` field. File is `core.mjs` by convention.
62
+
63
+ **Detection:** `pkg.main` or `pkg.exports` exists.
64
+
65
+ **Install:** `npm install <package>` or import directly from path.
66
+
67
+ ```json
68
+ {
69
+ "type": "module",
70
+ "main": "core.mjs",
71
+ "exports": {
72
+ ".": "./core.mjs",
73
+ "./cli": "./cli.mjs"
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### 3. MCP Server
79
+
80
+ A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible agent can use it.
81
+
82
+ **Convention:** `mcp-server.mjs` (or `.js`, `.ts`) at the repo root. Uses `@modelcontextprotocol/sdk`.
83
+
84
+ **Detection:** One of `mcp-server.mjs`, `mcp-server.js`, `mcp-server.ts`, `dist/mcp-server.js` exists.
85
+
86
+ **Install:** Add to `.mcp.json`:
87
+
88
+ ```json
89
+ {
90
+ "tool-name": {
91
+ "command": "node",
92
+ "args": ["/path/to/mcp-server.mjs"]
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### 4. OpenClaw Plugin
98
+
99
+ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
100
+
101
+ **Convention:** `openclaw.plugin.json` at the repo root.
102
+
103
+ **Detection:** `openclaw.plugin.json` exists.
104
+
105
+ **Install:** Copy to `~/.openclaw/extensions/<name>/`, run `npm install --omit=dev`.
106
+
107
+ ### 5. Skill (SKILL.md)
108
+
109
+ A markdown file that teaches agents when and how to use the tool. The instruction interface.
110
+
111
+ **Convention:** `SKILL.md` at the repo root. YAML frontmatter with name, version, description, metadata.
112
+
113
+ **Detection:** `SKILL.md` exists.
114
+
115
+ **Install:** Referenced by path. Agents read it when they need the tool.
116
+
117
+ ```yaml
118
+ ---
119
+ name: wip-grok
120
+ version: 1.0.0
121
+ description: xAI Grok API. Search the web, search X, generate images.
122
+ metadata:
123
+ category: search,media
124
+ capabilities:
125
+ - web-search
126
+ - image-generation
127
+ ---
128
+ ```
129
+
130
+ ### 6. Claude Code Hook
131
+
132
+ A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
133
+
134
+ **Convention:** `guard.mjs` at repo root, or `claudeCode.hook` in `package.json`.
135
+
136
+ **Detection:** `guard.mjs` exists, or `pkg.claudeCode.hook` is defined.
137
+
138
+ **Install:** Added to `~/.claude/settings.json` under `hooks`.
139
+
140
+ ```json
141
+ {
142
+ "hooks": {
143
+ "PreToolUse": [{
144
+ "matcher": "Edit|Write",
145
+ "hooks": [{
146
+ "type": "command",
147
+ "command": "node /path/to/guard.mjs",
148
+ "timeout": 5
149
+ }]
150
+ }]
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### 7. Claude Code Plugin
156
+
157
+ A distributable plugin for Claude Code. Bundles skills, agents, hooks, MCP servers, and LSP servers into one installable package. Shareable via marketplaces.
158
+
159
+ **Convention:** `.claude-plugin/plugin.json` at the repo root.
160
+
161
+ **Detection:** `.claude-plugin/plugin.json` exists.
162
+
163
+ **Install:** Registered with Claude Code via `/plugin install` or marketplace.
164
+
165
+ ```
166
+ your-plugin/
167
+ .claude-plugin/
168
+ plugin.json manifest (name, version, description)
169
+ skills/ SKILL.md files
170
+ agents/ subagent definitions
171
+ hooks/
172
+ hooks.json event handlers
173
+ .mcp.json MCP server configs
174
+ .lsp.json LSP server configs
175
+ ```
176
+
177
+ ```json
178
+ {
179
+ "name": "your-plugin",
180
+ "version": "1.0.0",
181
+ "description": "What it does",
182
+ "author": { "name": "Your Name" }
183
+ }
184
+ ```
185
+
186
+ ## How to Build It
187
+
188
+ The architecture is simple. Four files:
189
+
190
+ ```
191
+ your-tool/
192
+ core.mjs <- pure logic, zero deps if possible
193
+ cli.mjs <- thin CLI wrapper
194
+ mcp-server.mjs <- MCP server wrapping core as tools
195
+ SKILL.md <- when/how to use it, for agents
196
+ ```
197
+
198
+ `core.mjs` does the work. Everything else is a thin wrapper. CLI parses argv and calls core. MCP server maps tools to core functions. SKILL.md teaches agents when to call what.
199
+
200
+ This means one codebase, one set of tests, multiple interfaces.
201
+
202
+ ## Install Prompt Template
203
+
204
+ Every product gets an install prompt. Paste it into any AI. The AI reads the spec, explains it, checks what's installed, and walks you through a dry run.
205
+
206
+ ```
207
+ Read wip.computer/install/{URL}
208
+
209
+ Then explain:
210
+ 1. What is {name of product}?
211
+ 2. What does it install on my system?
212
+ 3. What changes for us? (this AI)
213
+ 4. What changes across all my AIs?
214
+
215
+ Check if {name of product} is already installed.
216
+
217
+ If it is, show me what I have and what's new.
218
+
219
+ Then ask:
220
+ - Do you have questions?
221
+ - Want to see a dry run?
222
+
223
+ If I say yes, run: {product-init} init --dry-run
224
+
225
+ Show me exactly what will change. Don't install anything until I say "install".
226
+ ```
227
+
228
+ ## The `ai/` Folder
229
+
230
+ Every repo should have an `ai/` folder. This is where agents and humans collaborate on the project ... plans, todos, dev updates, research notes, conversations.
231
+
232
+ ```
233
+ ai/
234
+ plan/ architecture plans, roadmaps
235
+ dev-updates/ what was built, session logs
236
+ todos/
237
+ PUNCHLIST.md blockers to ship
238
+ inboxes/ per-agent action items
239
+ notes/ research, references, raw conversation logs
240
+ ```
241
+
242
+ The `ai/` folder is the development process. It is not part of the published product.
243
+
244
+ **Public/private split:** If a repo is public, the `ai/` folder should not ship. The recommended pattern is to maintain a private working repo (with `ai/`) and a public repo (everything except `ai/`). The public repo has everything an LLM or human needs to understand and use the tool. The `ai/` folder is operational context for the team building it.
245
+
246
+ ## The Installer
247
+
248
+ `ldm install` scans any repo, detects which interfaces exist, and installs them all. One command.
249
+
250
+ ```bash
251
+ ldm install wipcomputer/wip-grok # from GitHub
252
+ ldm install /path/to/repo # from a local path
253
+ ldm install --dry-run # detect only
254
+ ldm install # update all
255
+ ```
256
+
257
+ ### What It Detects
258
+
259
+ | Pattern | Interface | Install action |
260
+ |---------|-----------|---------------|
261
+ | `package.json` with `bin` | CLI | `npm install -g` |
262
+ | `main` or `exports` in `package.json` | Module | Reports import path |
263
+ | `mcp-server.mjs` | MCP | Prints `.mcp.json` config |
264
+ | `openclaw.plugin.json` | OpenClaw | Copies to `~/.openclaw/extensions/` |
265
+ | `SKILL.md` | Skill | Reports path |
266
+ | `guard.mjs` or `claudeCode.hook` | CC Hook | Adds to `~/.claude/settings.json` |
267
+ | `.claude-plugin/plugin.json` | CC Plugin | Registers with Claude Code marketplace |
268
+
269
+ ## Examples
270
+
271
+ | Tool | Type | Interfaces | What it does |
272
+ |------|------|------------|-------------|
273
+ | [wip-grok](https://github.com/wipcomputer/wip-grok) | Sensor + Actuator | CLI + Module + MCP + Skill | xAI Grok API: search web/X, generate images/video |
274
+ | [wip-x](https://github.com/wipcomputer/wip-x) | Sensor + Actuator | CLI + Module + MCP + Skill | X Platform API: read/write tweets, bookmarks |
275
+ | [wip-file-guard](https://github.com/wipcomputer/wip-ai-devops-toolbox/tree/main/tools/wip-file-guard) | Actuator | CLI + OpenClaw + CC Hook | Protect files from AI edits |
276
+ | [wip-healthcheck](https://github.com/wipcomputer/wip-healthcheck) | Sensor | CLI + Module | System health monitoring |
277
+ | [wip-markdown-viewer](https://github.com/wipcomputer/wip-markdown-viewer) | Actuator | CLI + Module | Live markdown viewer |
278
+
279
+ ## Supported Tools
280
+
281
+ Works with any AI agent or coding tool that can run shell commands:
282
+
283
+ | Tool | How |
284
+ |------|-----|
285
+ | Claude Code | CLI via bash, hooks via settings.json, MCP via .mcp.json, plugins via marketplace |
286
+ | OpenAI Codex CLI | CLI via bash, skills via AGENTS.md |
287
+ | Cursor | CLI via terminal, MCP via config |
288
+ | Windsurf | CLI via terminal, MCP via config |
289
+ | OpenClaw | Plugins, skills, MCP |
290
+ | Any agent | CLI works everywhere. If it has a shell, it works. |