@wipcomputer/wip-ldm-os 0.4.14 → 0.4.16
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 +84 -2
- package/catalog.json +2 -2
- package/dist/bridge/chunk-LT4KM3AD.js +423 -0
- package/dist/bridge/cli.js +4 -4
- package/dist/bridge/core.js +1 -1
- package/dist/bridge/mcp-server.js +9 -9
- package/lib/deploy.mjs +16 -2
- package/package.json +1 -1
- package/src/bridge/core.ts +4 -2
package/SKILL.md
CHANGED
package/bin/ldm.js
CHANGED
|
@@ -447,6 +447,23 @@ async function cmdInstall() {
|
|
|
447
447
|
|
|
448
448
|
setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT });
|
|
449
449
|
|
|
450
|
+
// --help flag (#81)
|
|
451
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
452
|
+
console.log(`
|
|
453
|
+
ldm install Update all registered extensions + CLIs
|
|
454
|
+
ldm install <org/repo> Install from GitHub
|
|
455
|
+
ldm install <npm-package> Install from npm
|
|
456
|
+
ldm install <path> Install from local directory
|
|
457
|
+
|
|
458
|
+
Flags:
|
|
459
|
+
--dry-run Show what would change, don't install
|
|
460
|
+
--json JSON output
|
|
461
|
+
--yes Auto-accept catalog prompts
|
|
462
|
+
--none Skip catalog prompts
|
|
463
|
+
`);
|
|
464
|
+
process.exit(0);
|
|
465
|
+
}
|
|
466
|
+
|
|
450
467
|
// Find the target (skip flags)
|
|
451
468
|
const target = args.slice(1).find(a => !a.startsWith('--'));
|
|
452
469
|
|
|
@@ -493,6 +510,11 @@ async function cmdInstall() {
|
|
|
493
510
|
}
|
|
494
511
|
|
|
495
512
|
await installFromPath(repoPath);
|
|
513
|
+
|
|
514
|
+
// Clean up /tmp/ clone after install (#32)
|
|
515
|
+
if (!DRY_RUN && (repoPath.startsWith('/tmp/') || repoPath.startsWith('/private/tmp/'))) {
|
|
516
|
+
try { execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' }); } catch {}
|
|
517
|
+
}
|
|
496
518
|
return;
|
|
497
519
|
}
|
|
498
520
|
|
|
@@ -570,6 +592,11 @@ async function cmdInstall() {
|
|
|
570
592
|
}
|
|
571
593
|
|
|
572
594
|
await installFromPath(repoPath);
|
|
595
|
+
|
|
596
|
+
// Clean up /tmp/ clone after install (#32)
|
|
597
|
+
if (!DRY_RUN && (repoPath.startsWith('/tmp/') || repoPath.startsWith('/private/tmp/'))) {
|
|
598
|
+
try { execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' }); } catch {}
|
|
599
|
+
}
|
|
573
600
|
}
|
|
574
601
|
|
|
575
602
|
// ── Auto-detect unregistered extensions ──
|
|
@@ -689,6 +716,16 @@ async function cmdInstallCatalog() {
|
|
|
689
716
|
return matches.includes(name) || c.id === name;
|
|
690
717
|
});
|
|
691
718
|
|
|
719
|
+
// Fallback: use repository.url from extension's package.json (#82)
|
|
720
|
+
let repoUrl = catalogEntry?.repo || null;
|
|
721
|
+
if (!repoUrl && extPkg?.repository) {
|
|
722
|
+
const raw = typeof extPkg.repository === 'string'
|
|
723
|
+
? extPkg.repository
|
|
724
|
+
: extPkg.repository.url || '';
|
|
725
|
+
const ghMatch = raw.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
726
|
+
if (ghMatch) repoUrl = ghMatch[1];
|
|
727
|
+
}
|
|
728
|
+
|
|
692
729
|
const currentVersion = entry.ldmVersion || entry.ocVersion;
|
|
693
730
|
if (!currentVersion) continue;
|
|
694
731
|
|
|
@@ -700,7 +737,7 @@ async function cmdInstallCatalog() {
|
|
|
700
737
|
if (latestVersion && latestVersion !== currentVersion) {
|
|
701
738
|
npmUpdates.push({
|
|
702
739
|
...entry,
|
|
703
|
-
catalogRepo:
|
|
740
|
+
catalogRepo: repoUrl,
|
|
704
741
|
catalogNpm: npmPkg,
|
|
705
742
|
currentVersion,
|
|
706
743
|
latestVersion,
|
|
@@ -710,6 +747,39 @@ async function cmdInstallCatalog() {
|
|
|
710
747
|
} catch {}
|
|
711
748
|
}
|
|
712
749
|
|
|
750
|
+
// Check global CLIs not tracked by extension loop (#81)
|
|
751
|
+
for (const [binName, binInfo] of Object.entries(state.cliBinaries || {})) {
|
|
752
|
+
const catalogComp = components.find(c =>
|
|
753
|
+
(c.cliMatches || []).includes(binName)
|
|
754
|
+
);
|
|
755
|
+
if (!catalogComp || !catalogComp.npm) continue;
|
|
756
|
+
// Skip if already covered by extension loop
|
|
757
|
+
if (npmUpdates.some(e =>
|
|
758
|
+
e.catalogNpm === catalogComp.npm ||
|
|
759
|
+
(catalogComp.registryMatches || []).includes(e.name)
|
|
760
|
+
)) continue;
|
|
761
|
+
|
|
762
|
+
const currentVersion = binInfo.version;
|
|
763
|
+
if (!currentVersion) continue;
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
const latestVersion = execSync(`npm view ${catalogComp.npm} version 2>/dev/null`, {
|
|
767
|
+
encoding: 'utf8', timeout: 10000,
|
|
768
|
+
}).trim();
|
|
769
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
770
|
+
npmUpdates.push({
|
|
771
|
+
name: binName,
|
|
772
|
+
catalogRepo: catalogComp.repo,
|
|
773
|
+
catalogNpm: catalogComp.npm,
|
|
774
|
+
currentVersion,
|
|
775
|
+
latestVersion,
|
|
776
|
+
hasUpdate: true,
|
|
777
|
+
cliOnly: true,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
} catch {}
|
|
781
|
+
}
|
|
782
|
+
|
|
713
783
|
const totalUpdates = npmUpdates.length;
|
|
714
784
|
|
|
715
785
|
if (DRY_RUN) {
|
|
@@ -802,8 +872,20 @@ async function cmdInstallCatalog() {
|
|
|
802
872
|
|
|
803
873
|
let updated = 0;
|
|
804
874
|
|
|
805
|
-
// Update from npm via catalog repos (#55)
|
|
875
|
+
// Update from npm via catalog repos (#55) and CLIs (#81)
|
|
806
876
|
for (const entry of npmUpdates) {
|
|
877
|
+
// CLI-only entries: install directly from npm (#81)
|
|
878
|
+
if (entry.cliOnly) {
|
|
879
|
+
console.log(` Updating CLI ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion}...`);
|
|
880
|
+
try {
|
|
881
|
+
execSync(`npm install -g ${entry.catalogNpm}@${entry.latestVersion}`, { stdio: 'inherit' });
|
|
882
|
+
updated++;
|
|
883
|
+
} catch (e) {
|
|
884
|
+
console.error(` x Failed to update CLI ${entry.name}: ${e.message}`);
|
|
885
|
+
}
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
|
|
807
889
|
if (!entry.catalogRepo) {
|
|
808
890
|
console.log(` Skipping ${entry.name}: no catalog repo (install manually with ldm install <org/repo>)`);
|
|
809
891
|
continue;
|
package/catalog.json
CHANGED
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"description": "Release pipeline, license compliance, repo management, identity file protection.",
|
|
54
54
|
"npm": "@wipcomputer/universal-installer",
|
|
55
55
|
"repo": "wipcomputer/wip-ai-devops-toolbox",
|
|
56
|
-
"registryMatches": ["wip-repos", "wip-release", "wip-file-guard", "wip-license-hook", "wip-repo-permissions-hook", "universal-installer", "deploy-public", "post-merge-rename", "wip-license-guard", "wip-repo-init", "wip-readme-format"],
|
|
57
|
-
"cliMatches": ["wip-release", "wip-repos", "wip-file-guard", "wip-install"],
|
|
56
|
+
"registryMatches": ["wip-repos", "wip-release", "wip-file-guard", "wip-license-hook", "wip-repo-permissions-hook", "universal-installer", "deploy-public", "post-merge-rename", "wip-license-guard", "wip-repo-init", "wip-readme-format", "wip-branch-guard"],
|
|
57
|
+
"cliMatches": ["wip-release", "wip-repos", "wip-file-guard", "wip-install", "wip-branch-guard"],
|
|
58
58
|
"recommended": false,
|
|
59
59
|
"status": "stable",
|
|
60
60
|
"postInstall": null,
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// core.ts
|
|
2
|
+
import { execSync, exec } from "child_process";
|
|
3
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "fs";
|
|
4
|
+
import { join, relative, resolve } from "path";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
var execAsync = promisify(exec);
|
|
7
|
+
var HOME = process.env.HOME || "/Users/lesa";
|
|
8
|
+
var LDM_ROOT = process.env.LDM_ROOT || join(HOME, ".ldm");
|
|
9
|
+
function resolveConfig(overrides) {
|
|
10
|
+
const openclawDir = overrides?.openclawDir || process.env.OPENCLAW_DIR || join(process.env.HOME || "~", ".openclaw");
|
|
11
|
+
return {
|
|
12
|
+
openclawDir,
|
|
13
|
+
workspaceDir: overrides?.workspaceDir || join(openclawDir, "workspace"),
|
|
14
|
+
dbPath: overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
|
|
15
|
+
inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
|
|
16
|
+
embeddingModel: overrides?.embeddingModel || "text-embedding-3-small",
|
|
17
|
+
embeddingDimensions: overrides?.embeddingDimensions || 1536
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function resolveConfigMulti(overrides) {
|
|
21
|
+
const ldmConfig = join(LDM_ROOT, "config.json");
|
|
22
|
+
if (existsSync(ldmConfig)) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(readFileSync(ldmConfig, "utf-8"));
|
|
25
|
+
const openclawDir = raw.openclawDir || process.env.OPENCLAW_DIR || join(HOME, ".openclaw");
|
|
26
|
+
return {
|
|
27
|
+
openclawDir,
|
|
28
|
+
workspaceDir: raw.workspaceDir || overrides?.workspaceDir || join(openclawDir, "workspace"),
|
|
29
|
+
dbPath: raw.dbPath || overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
|
|
30
|
+
inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
|
|
31
|
+
embeddingModel: raw.embeddingModel || overrides?.embeddingModel || "text-embedding-3-small",
|
|
32
|
+
embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || 1536
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return resolveConfig(overrides);
|
|
38
|
+
}
|
|
39
|
+
var cachedApiKey = void 0;
|
|
40
|
+
function resolveApiKey(openclawDir) {
|
|
41
|
+
if (cachedApiKey !== void 0) return cachedApiKey;
|
|
42
|
+
if (process.env.OPENAI_API_KEY) {
|
|
43
|
+
cachedApiKey = process.env.OPENAI_API_KEY;
|
|
44
|
+
return cachedApiKey;
|
|
45
|
+
}
|
|
46
|
+
const tokenPath = join(openclawDir, "secrets", "op-sa-token");
|
|
47
|
+
if (existsSync(tokenPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const saToken = readFileSync(tokenPath, "utf-8").trim();
|
|
50
|
+
const key = execSync(
|
|
51
|
+
`op read "op://Agent Secrets/OpenAI API/api key" 2>/dev/null`,
|
|
52
|
+
{
|
|
53
|
+
env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
|
|
54
|
+
timeout: 1e4,
|
|
55
|
+
encoding: "utf-8"
|
|
56
|
+
}
|
|
57
|
+
).trim();
|
|
58
|
+
if (key && key.startsWith("sk-")) {
|
|
59
|
+
cachedApiKey = key;
|
|
60
|
+
return cachedApiKey;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
cachedApiKey = null;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
var cachedGatewayConfig = null;
|
|
69
|
+
function resolveGatewayConfig(openclawDir) {
|
|
70
|
+
if (cachedGatewayConfig) return cachedGatewayConfig;
|
|
71
|
+
const configPath = join(openclawDir, "openclaw.json");
|
|
72
|
+
if (!existsSync(configPath)) {
|
|
73
|
+
throw new Error(`OpenClaw config not found: ${configPath}`);
|
|
74
|
+
}
|
|
75
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
76
|
+
const token = config?.gateway?.auth?.token;
|
|
77
|
+
const port = config?.gateway?.port || 18789;
|
|
78
|
+
if (!token) {
|
|
79
|
+
throw new Error("No gateway.auth.token found in openclaw.json");
|
|
80
|
+
}
|
|
81
|
+
cachedGatewayConfig = { token, port };
|
|
82
|
+
return cachedGatewayConfig;
|
|
83
|
+
}
|
|
84
|
+
var inboxQueue = [];
|
|
85
|
+
function pushInbox(msg) {
|
|
86
|
+
inboxQueue.push(msg);
|
|
87
|
+
return inboxQueue.length;
|
|
88
|
+
}
|
|
89
|
+
function drainInbox() {
|
|
90
|
+
const messages = [...inboxQueue];
|
|
91
|
+
inboxQueue.length = 0;
|
|
92
|
+
return messages;
|
|
93
|
+
}
|
|
94
|
+
function inboxCount() {
|
|
95
|
+
return inboxQueue.length;
|
|
96
|
+
}
|
|
97
|
+
async function sendMessage(openclawDir, message, options) {
|
|
98
|
+
const { token, port } = resolveGatewayConfig(openclawDir);
|
|
99
|
+
const agentId = options?.agentId || "main";
|
|
100
|
+
const senderLabel = options?.senderLabel || "Claude Code";
|
|
101
|
+
const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token}`,
|
|
105
|
+
"Content-Type": "application/json"
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
model: agentId,
|
|
109
|
+
user: "main",
|
|
110
|
+
messages: [
|
|
111
|
+
{
|
|
112
|
+
role: "user",
|
|
113
|
+
content: `[${senderLabel}]: ${message}`
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const body = await response.text();
|
|
120
|
+
throw new Error(`Gateway returned ${response.status}: ${body}`);
|
|
121
|
+
}
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
const reply = data.choices?.[0]?.message?.content;
|
|
124
|
+
if (!reply) {
|
|
125
|
+
throw new Error("No response content from gateway");
|
|
126
|
+
}
|
|
127
|
+
return reply;
|
|
128
|
+
}
|
|
129
|
+
async function getQueryEmbedding(text, apiKey, model = "text-embedding-3-small", dimensions = 1536) {
|
|
130
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${apiKey}`,
|
|
134
|
+
"Content-Type": "application/json"
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
input: [text],
|
|
138
|
+
model,
|
|
139
|
+
dimensions
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const body = await response.text();
|
|
144
|
+
throw new Error(`OpenAI embeddings failed (${response.status}): ${body}`);
|
|
145
|
+
}
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
return data.data[0].embedding;
|
|
148
|
+
}
|
|
149
|
+
function blobToEmbedding(blob) {
|
|
150
|
+
const floats = [];
|
|
151
|
+
for (let i = 0; i < blob.length; i += 4) {
|
|
152
|
+
floats.push(blob.readFloatLE(i));
|
|
153
|
+
}
|
|
154
|
+
return floats;
|
|
155
|
+
}
|
|
156
|
+
function cosineSimilarity(a, b) {
|
|
157
|
+
let dot = 0;
|
|
158
|
+
let normA = 0;
|
|
159
|
+
let normB = 0;
|
|
160
|
+
for (let i = 0; i < a.length; i++) {
|
|
161
|
+
dot += a[i] * b[i];
|
|
162
|
+
normA += a[i] * a[i];
|
|
163
|
+
normB += b[i] * b[i];
|
|
164
|
+
}
|
|
165
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
166
|
+
return denom === 0 ? 0 : dot / denom;
|
|
167
|
+
}
|
|
168
|
+
function recencyWeight(ageDays) {
|
|
169
|
+
return Math.max(0.5, 1 - ageDays * 0.01);
|
|
170
|
+
}
|
|
171
|
+
function freshnessLabel(ageDays) {
|
|
172
|
+
if (ageDays < 3) return "fresh";
|
|
173
|
+
if (ageDays < 7) return "recent";
|
|
174
|
+
if (ageDays < 14) return "aging";
|
|
175
|
+
return "stale";
|
|
176
|
+
}
|
|
177
|
+
async function searchConversations(config, query, limit = 5) {
|
|
178
|
+
const Database = (await import("better-sqlite3")).default;
|
|
179
|
+
if (!existsSync(config.dbPath)) {
|
|
180
|
+
throw new Error(`Database not found: ${config.dbPath}`);
|
|
181
|
+
}
|
|
182
|
+
const db = new Database(config.dbPath, { readonly: true });
|
|
183
|
+
db.pragma("journal_mode = WAL");
|
|
184
|
+
try {
|
|
185
|
+
const apiKey = resolveApiKey(config.openclawDir);
|
|
186
|
+
if (apiKey) {
|
|
187
|
+
const queryEmbedding = await getQueryEmbedding(
|
|
188
|
+
query,
|
|
189
|
+
apiKey,
|
|
190
|
+
config.embeddingModel,
|
|
191
|
+
config.embeddingDimensions
|
|
192
|
+
);
|
|
193
|
+
const rows = db.prepare(
|
|
194
|
+
`SELECT chunk_text, role, session_key, timestamp, embedding
|
|
195
|
+
FROM conversation_chunks
|
|
196
|
+
WHERE embedding IS NOT NULL
|
|
197
|
+
ORDER BY timestamp DESC
|
|
198
|
+
LIMIT 1000`
|
|
199
|
+
).all();
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
return rows.map((row) => {
|
|
202
|
+
const cosine = cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding));
|
|
203
|
+
const ageDays = (now - row.timestamp) / (1e3 * 60 * 60 * 24);
|
|
204
|
+
const weight = recencyWeight(ageDays);
|
|
205
|
+
return {
|
|
206
|
+
text: row.chunk_text,
|
|
207
|
+
role: row.role,
|
|
208
|
+
sessionKey: row.session_key,
|
|
209
|
+
date: new Date(row.timestamp).toISOString().split("T")[0],
|
|
210
|
+
similarity: cosine * weight,
|
|
211
|
+
recencyScore: weight,
|
|
212
|
+
freshness: freshnessLabel(ageDays)
|
|
213
|
+
};
|
|
214
|
+
}).sort((a, b) => (b.similarity || 0) - (a.similarity || 0)).slice(0, limit);
|
|
215
|
+
} else {
|
|
216
|
+
const rows = db.prepare(
|
|
217
|
+
`SELECT chunk_text, role, session_key, timestamp
|
|
218
|
+
FROM conversation_chunks
|
|
219
|
+
WHERE chunk_text LIKE ?
|
|
220
|
+
ORDER BY timestamp DESC
|
|
221
|
+
LIMIT ?`
|
|
222
|
+
).all(`%${query}%`, limit);
|
|
223
|
+
return rows.map((row) => ({
|
|
224
|
+
text: row.chunk_text,
|
|
225
|
+
role: row.role,
|
|
226
|
+
sessionKey: row.session_key,
|
|
227
|
+
date: new Date(row.timestamp).toISOString().split("T")[0]
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
} finally {
|
|
231
|
+
db.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function findMarkdownFiles(dir, maxDepth = 4, depth = 0) {
|
|
235
|
+
if (depth > maxDepth || !existsSync(dir)) return [];
|
|
236
|
+
const files = [];
|
|
237
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
238
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
239
|
+
const fullPath = join(dir, entry.name);
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
files.push(...findMarkdownFiles(fullPath, maxDepth, depth + 1));
|
|
242
|
+
} else if (entry.name.endsWith(".md")) {
|
|
243
|
+
files.push(fullPath);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return files;
|
|
247
|
+
}
|
|
248
|
+
function searchWorkspace(workspaceDir, query) {
|
|
249
|
+
const files = findMarkdownFiles(workspaceDir);
|
|
250
|
+
const queryLower = query.toLowerCase();
|
|
251
|
+
const words = queryLower.split(/\s+/).filter((w) => w.length > 2);
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const filePath of files) {
|
|
254
|
+
try {
|
|
255
|
+
const content = readFileSync(filePath, "utf-8");
|
|
256
|
+
const contentLower = content.toLowerCase();
|
|
257
|
+
let score = 0;
|
|
258
|
+
for (const word of words) {
|
|
259
|
+
if (contentLower.indexOf(word) !== -1) score++;
|
|
260
|
+
}
|
|
261
|
+
if (score === 0) continue;
|
|
262
|
+
const lines = content.split("\n");
|
|
263
|
+
const excerpts = [];
|
|
264
|
+
for (let i = 0; i < lines.length && excerpts.length < 5; i++) {
|
|
265
|
+
const lineLower = lines[i].toLowerCase();
|
|
266
|
+
if (words.some((w) => lineLower.includes(w))) {
|
|
267
|
+
const start = Math.max(0, i - 1);
|
|
268
|
+
const end = Math.min(lines.length, i + 2);
|
|
269
|
+
excerpts.push(lines.slice(start, end).join("\n"));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
results.push({ path: relative(workspaceDir, filePath), excerpts, score });
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return results.sort((a, b) => b.score - a.score).slice(0, 10);
|
|
277
|
+
}
|
|
278
|
+
function parseSkillFrontmatter(content) {
|
|
279
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
280
|
+
if (!match) return {};
|
|
281
|
+
const yaml = match[1];
|
|
282
|
+
const name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
283
|
+
const description = yaml.match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
|
284
|
+
let emoji;
|
|
285
|
+
const emojiMatch = yaml.match(/"emoji":\s*"([^"]+)"/);
|
|
286
|
+
if (emojiMatch) emoji = emojiMatch[1];
|
|
287
|
+
let requires;
|
|
288
|
+
const requiresMatch = yaml.match(/"requires":\s*\{([^}]+)\}/);
|
|
289
|
+
if (requiresMatch) {
|
|
290
|
+
requires = {};
|
|
291
|
+
const pairs = requiresMatch[1].matchAll(/"(\w+)":\s*\[([^\]]*)\]/g);
|
|
292
|
+
for (const pair of pairs) {
|
|
293
|
+
const values = pair[2].match(/"([^"]+)"/g)?.map((v) => v.replace(/"/g, "")) || [];
|
|
294
|
+
requires[pair[1]] = values;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { name, description, emoji, requires };
|
|
298
|
+
}
|
|
299
|
+
function discoverSkills(openclawDir) {
|
|
300
|
+
const skills = [];
|
|
301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
302
|
+
const extensionsDir = join(openclawDir, "extensions");
|
|
303
|
+
if (!existsSync(extensionsDir)) return skills;
|
|
304
|
+
for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
305
|
+
if (!ext.isDirectory() || ext.name.startsWith(".")) continue;
|
|
306
|
+
const extDir = join(extensionsDir, ext.name);
|
|
307
|
+
const searchDirs = [
|
|
308
|
+
{ dir: join(extDir, "node_modules", "openclaw", "skills"), source: "builtin" },
|
|
309
|
+
{ dir: join(extDir, "skills"), source: "custom" }
|
|
310
|
+
];
|
|
311
|
+
for (const { dir, source } of searchDirs) {
|
|
312
|
+
if (!existsSync(dir)) continue;
|
|
313
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
314
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
315
|
+
const skillDir = join(dir, entry.name);
|
|
316
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
317
|
+
if (!existsSync(skillMd)) continue;
|
|
318
|
+
if (seen.has(entry.name)) continue;
|
|
319
|
+
seen.add(entry.name);
|
|
320
|
+
try {
|
|
321
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
322
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
323
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
324
|
+
let scripts = [];
|
|
325
|
+
let hasScripts = false;
|
|
326
|
+
if (existsSync(scriptsDir) && statSync(scriptsDir).isDirectory()) {
|
|
327
|
+
scripts = readdirSync(scriptsDir).filter(
|
|
328
|
+
(f) => f.endsWith(".sh") || f.endsWith(".py")
|
|
329
|
+
);
|
|
330
|
+
hasScripts = scripts.length > 0;
|
|
331
|
+
}
|
|
332
|
+
skills.push({
|
|
333
|
+
name: frontmatter.name || entry.name,
|
|
334
|
+
description: frontmatter.description || `OpenClaw skill: ${entry.name}`,
|
|
335
|
+
skillDir,
|
|
336
|
+
hasScripts,
|
|
337
|
+
scripts,
|
|
338
|
+
source,
|
|
339
|
+
emoji: frontmatter.emoji,
|
|
340
|
+
requires: frontmatter.requires
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
348
|
+
}
|
|
349
|
+
async function executeSkillScript(skillDir, scripts, scriptName, args) {
|
|
350
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
351
|
+
let script;
|
|
352
|
+
if (scriptName) {
|
|
353
|
+
if (!scripts.includes(scriptName)) {
|
|
354
|
+
throw new Error(`Script "${scriptName}" not found. Available: ${scripts.join(", ")}`);
|
|
355
|
+
}
|
|
356
|
+
script = scriptName;
|
|
357
|
+
} else if (scripts.length === 1) {
|
|
358
|
+
script = scripts[0];
|
|
359
|
+
} else {
|
|
360
|
+
const sh = scripts.find((s) => s.endsWith(".sh"));
|
|
361
|
+
script = sh || scripts[0];
|
|
362
|
+
}
|
|
363
|
+
const scriptPath = join(scriptsDir, script);
|
|
364
|
+
const interpreter = script.endsWith(".py") ? "python3" : "bash";
|
|
365
|
+
try {
|
|
366
|
+
const { stdout, stderr } = await execAsync(
|
|
367
|
+
`${interpreter} "${scriptPath}" ${args}`,
|
|
368
|
+
{
|
|
369
|
+
env: { ...process.env },
|
|
370
|
+
timeout: 12e4,
|
|
371
|
+
maxBuffer: 10 * 1024 * 1024
|
|
372
|
+
// 10MB
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
return stdout || stderr || "(no output)";
|
|
376
|
+
} catch (err) {
|
|
377
|
+
const output = err.stdout || err.stderr || err.message;
|
|
378
|
+
throw new Error(`Script failed (exit ${err.code || "?"}): ${output}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function readWorkspaceFile(workspaceDir, filePath) {
|
|
382
|
+
const resolved = resolve(workspaceDir, filePath);
|
|
383
|
+
if (!resolved.startsWith(resolve(workspaceDir))) {
|
|
384
|
+
throw new Error("Path must be within workspace/");
|
|
385
|
+
}
|
|
386
|
+
if (!existsSync(resolved)) {
|
|
387
|
+
const dir = resolved.endsWith(".md") ? join(resolved, "..") : resolved;
|
|
388
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
389
|
+
const files = findMarkdownFiles(dir, 1);
|
|
390
|
+
const listing = files.map((f) => relative(workspaceDir, f)).join("\n");
|
|
391
|
+
throw new Error(`File not found: ${filePath}
|
|
392
|
+
|
|
393
|
+
Available files:
|
|
394
|
+
${listing}`);
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`File not found: ${filePath}`);
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
content: readFileSync(resolved, "utf-8"),
|
|
400
|
+
relativePath: relative(workspaceDir, resolved)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export {
|
|
405
|
+
LDM_ROOT,
|
|
406
|
+
resolveConfig,
|
|
407
|
+
resolveConfigMulti,
|
|
408
|
+
resolveApiKey,
|
|
409
|
+
resolveGatewayConfig,
|
|
410
|
+
pushInbox,
|
|
411
|
+
drainInbox,
|
|
412
|
+
inboxCount,
|
|
413
|
+
sendMessage,
|
|
414
|
+
getQueryEmbedding,
|
|
415
|
+
blobToEmbedding,
|
|
416
|
+
cosineSimilarity,
|
|
417
|
+
searchConversations,
|
|
418
|
+
findMarkdownFiles,
|
|
419
|
+
searchWorkspace,
|
|
420
|
+
discoverSkills,
|
|
421
|
+
executeSkillScript,
|
|
422
|
+
readWorkspaceFile
|
|
423
|
+
};
|
package/dist/bridge/cli.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
searchConversations,
|
|
9
9
|
searchWorkspace,
|
|
10
10
|
sendMessage
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LT4KM3AD.js";
|
|
12
12
|
|
|
13
13
|
// cli.ts
|
|
14
14
|
import { existsSync, statSync } from "fs";
|
|
15
15
|
var config = resolveConfig();
|
|
16
16
|
function usage() {
|
|
17
|
-
console.log(`
|
|
17
|
+
console.log(`wip-bridge: Claude Code CLI \u2194 OpenClaw TUI agent bridge
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
20
20
|
lesa send <message> Send a message to the OpenClaw agent
|
|
@@ -119,7 +119,7 @@ async function main() {
|
|
|
119
119
|
break;
|
|
120
120
|
}
|
|
121
121
|
case "status": {
|
|
122
|
-
console.log(`
|
|
122
|
+
console.log(`wip-bridge status`);
|
|
123
123
|
console.log(` OpenClaw dir: ${config.openclawDir}`);
|
|
124
124
|
console.log(` Workspace: ${config.workspaceDir}`);
|
|
125
125
|
console.log(` Database: ${config.dbPath}`);
|
|
@@ -128,7 +128,7 @@ async function main() {
|
|
|
128
128
|
break;
|
|
129
129
|
}
|
|
130
130
|
case "diagnose": {
|
|
131
|
-
console.log("
|
|
131
|
+
console.log("wip-bridge diagnose\n");
|
|
132
132
|
let issues = 0;
|
|
133
133
|
if (existsSync(config.openclawDir)) {
|
|
134
134
|
console.log(` \u2713 OpenClaw dir exists: ${config.openclawDir}`);
|
package/dist/bridge/core.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
searchConversations,
|
|
10
10
|
searchWorkspace,
|
|
11
11
|
sendMessage
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LT4KM3AD.js";
|
|
13
13
|
|
|
14
14
|
// mcp-server.ts
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -59,11 +59,11 @@ function startInboxServer(cfg) {
|
|
|
59
59
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
60
60
|
};
|
|
61
61
|
const queued = pushInbox(msg);
|
|
62
|
-
console.error(`
|
|
62
|
+
console.error(`wip-bridge inbox: message from ${msg.from}`);
|
|
63
63
|
try {
|
|
64
64
|
server.sendLoggingMessage({
|
|
65
65
|
level: "info",
|
|
66
|
-
logger: "
|
|
66
|
+
logger: "wip-bridge",
|
|
67
67
|
data: `[OpenClaw \u2192 Claude Code] ${msg.from}: ${msg.message}`
|
|
68
68
|
});
|
|
69
69
|
} catch {
|
|
@@ -85,14 +85,14 @@ function startInboxServer(cfg) {
|
|
|
85
85
|
res.end(JSON.stringify({ error: "not found" }));
|
|
86
86
|
});
|
|
87
87
|
httpServer.listen(cfg.inboxPort, "127.0.0.1", () => {
|
|
88
|
-
console.error(`
|
|
88
|
+
console.error(`wip-bridge inbox listening on 127.0.0.1:${cfg.inboxPort}`);
|
|
89
89
|
});
|
|
90
90
|
httpServer.on("error", (err) => {
|
|
91
|
-
console.error(`
|
|
91
|
+
console.error(`wip-bridge inbox server error: ${err.message}`);
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
var server = new McpServer({
|
|
95
|
-
name: "
|
|
95
|
+
name: "wip-bridge",
|
|
96
96
|
version: "0.3.0"
|
|
97
97
|
});
|
|
98
98
|
server.registerTool(
|
|
@@ -264,7 +264,7 @@ function registerSkillTools(skills) {
|
|
|
264
264
|
${lines.join("\n")}` }] };
|
|
265
265
|
}
|
|
266
266
|
);
|
|
267
|
-
console.error(`
|
|
267
|
+
console.error(`wip-bridge: registered ${executableSkills.length} skill tools + oc_skills_list (${skills.length} total skills)`);
|
|
268
268
|
}
|
|
269
269
|
async function main() {
|
|
270
270
|
startInboxServer(config);
|
|
@@ -272,11 +272,11 @@ async function main() {
|
|
|
272
272
|
const skills = discoverSkills(config.openclawDir);
|
|
273
273
|
registerSkillTools(skills);
|
|
274
274
|
} catch (err) {
|
|
275
|
-
console.error(`
|
|
275
|
+
console.error(`wip-bridge: skill discovery failed: ${err.message}`);
|
|
276
276
|
}
|
|
277
277
|
const transport = new StdioServerTransport();
|
|
278
278
|
await server.connect(transport);
|
|
279
|
-
console.error(`
|
|
279
|
+
console.error(`wip-bridge MCP server running (openclaw: ${config.openclawDir})`);
|
|
280
280
|
}
|
|
281
281
|
main().catch((error) => {
|
|
282
282
|
console.error("Fatal error:", error);
|
package/lib/deploy.mjs
CHANGED
|
@@ -372,11 +372,25 @@ function installCLI(repoPath, door) {
|
|
|
372
372
|
return true;
|
|
373
373
|
}
|
|
374
374
|
} catch {
|
|
375
|
-
// Registry check failed, fall through
|
|
375
|
+
// Registry check failed, fall through
|
|
376
376
|
}
|
|
377
|
+
|
|
378
|
+
// Exact version not on npm. Try latest from registry instead of local install (#32, #81)
|
|
379
|
+
try {
|
|
380
|
+
const latestVersion = execSync(`npm view ${packageName} version 2>/dev/null`, {
|
|
381
|
+
encoding: 'utf8', timeout: 15000,
|
|
382
|
+
}).trim();
|
|
383
|
+
if (latestVersion) {
|
|
384
|
+
execSync(`npm install -g ${packageName}@${latestVersion}`, { stdio: 'pipe', timeout: 60000 });
|
|
385
|
+
ensureBinExecutable(binNames);
|
|
386
|
+
ok(`CLI: ${binNames.join(', ')} installed from registry (v${latestVersion}, repo has v${packageVersion})`);
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
} catch {}
|
|
377
390
|
}
|
|
378
391
|
|
|
379
|
-
//
|
|
392
|
+
// Last resort: local install (creates symlinks ... warns user)
|
|
393
|
+
console.log(` ! Warning: installing locally from ${repoPath} (creates symlinks to source dir)`);
|
|
380
394
|
try {
|
|
381
395
|
execSync('npm install -g .', { cwd: repoPath, stdio: 'pipe' });
|
|
382
396
|
ensureBinExecutable(binNames);
|
package/package.json
CHANGED
package/src/bridge/core.ts
CHANGED
|
@@ -185,9 +185,11 @@ export async function sendMessage(
|
|
|
185
185
|
): Promise<string> {
|
|
186
186
|
const { token, port } = resolveGatewayConfig(openclawDir);
|
|
187
187
|
const agentId = options?.agentId || "main";
|
|
188
|
-
const user = options?.user || "claude-code";
|
|
189
188
|
const senderLabel = options?.senderLabel || "Claude Code";
|
|
190
189
|
|
|
190
|
+
// Send user: "main" to route to the main session (agent:main:main).
|
|
191
|
+
// This ensures Parker sees CC's messages in the same stream as iMessage.
|
|
192
|
+
// The OpenClaw gateway treats user: "main" as "use the default session."
|
|
191
193
|
const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
|
192
194
|
method: "POST",
|
|
193
195
|
headers: {
|
|
@@ -196,7 +198,7 @@ export async function sendMessage(
|
|
|
196
198
|
},
|
|
197
199
|
body: JSON.stringify({
|
|
198
200
|
model: agentId,
|
|
199
|
-
user,
|
|
201
|
+
user: "main",
|
|
200
202
|
messages: [
|
|
201
203
|
{
|
|
202
204
|
role: "user",
|