likho-cli 0.4.0 → 0.4.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/bin/likho.js +26 -4
- package/lib/files.js +21 -0
- package/lib/tools.js +39 -1
- package/package.json +1 -1
package/bin/likho.js
CHANGED
|
@@ -88,10 +88,13 @@ function orgOfUrl(url) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** Any folder but the home folder can be a project (a new one has no files yet). */
|
|
92
|
+
const inHome = (ctx) => path.resolve(ctx.cwd) === path.resolve(ctx.home);
|
|
93
|
+
|
|
91
94
|
async function chooseScope(opts, ctx, verb) {
|
|
92
95
|
if (opts.scope) return opts.scope;
|
|
93
|
-
if (
|
|
94
|
-
if (!interactive(opts)) return "project";
|
|
96
|
+
if (inHome(ctx)) return "global";
|
|
97
|
+
if (!interactive(opts)) return isProjectFolder(ctx) ? "project" : "global";
|
|
95
98
|
const folder = path.basename(ctx.cwd);
|
|
96
99
|
const setup = verb === "Set up";
|
|
97
100
|
return answer(ui.select({
|
|
@@ -277,6 +280,25 @@ async function connect(opts) {
|
|
|
277
280
|
}
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
// Old or extra Likho entries in the same tools (e.g. "likho-hub" from
|
|
284
|
+
// before the rename) are a second connection: offer to remove them.
|
|
285
|
+
const leftovers = done.flatMap((t) => (t.leftovers ? t.leftovers(ctx, scope).map((name) => ({ t, name })) : []));
|
|
286
|
+
if (leftovers.length) {
|
|
287
|
+
const list = leftovers.map(({ t, name }) => `"${name}" in ${t.name}`).join(", ");
|
|
288
|
+
const clean = interactive(opts)
|
|
289
|
+
? await answer(ui.confirm({ message: `Also found older Likho connections: ${list}. Remove them? (recommended)`, initialValue: true }))
|
|
290
|
+
: true;
|
|
291
|
+
if (clean) {
|
|
292
|
+
for (const { t, name } of leftovers) {
|
|
293
|
+
try {
|
|
294
|
+
if (t.removeOld(ctx, scope, name)) ui.log.success(`Removed the old "${name}" from ${t.name}`);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
ui.log.error(`${t.name}: couldn't remove "${name}": ${err.message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
280
302
|
let helperOn = false;
|
|
281
303
|
if (pair && Object.keys(helperCodes).length) {
|
|
282
304
|
try {
|
|
@@ -285,7 +307,7 @@ async function connect(opts) {
|
|
|
285
307
|
const { key, org } = await helper.redeem(opts.server, code, pair.verifier);
|
|
286
308
|
orgs.push({ slug: org.slug, name: org.name, key });
|
|
287
309
|
}
|
|
288
|
-
helper.install(opts.server, orgs,
|
|
310
|
+
helper.install(opts.server, orgs, inHome(ctx) ? "" : ctx.cwd);
|
|
289
311
|
helperOn = true;
|
|
290
312
|
ui.log.success(`Wake-ups on for ${orgs.map((o) => o.name).join(", ")}: this computer wakes your agents when they're tagged, while it's on.`);
|
|
291
313
|
} catch (err) {
|
|
@@ -299,7 +321,7 @@ async function connect(opts) {
|
|
|
299
321
|
ui.note(lines.join("\n"), "Restart these tools, then sign in once in each");
|
|
300
322
|
}
|
|
301
323
|
if (helperOn) {
|
|
302
|
-
const where =
|
|
324
|
+
const where = !inHome(ctx)
|
|
303
325
|
? `Agents are only woken in folders set up like this one (${path.basename(ctx.cwd)}): run npx likho-cli in each other project folder too.`
|
|
304
326
|
: "Agents are only woken in project folders you set up: run npx likho-cli inside each project folder whose agents should wake.";
|
|
305
327
|
ui.note(
|
package/lib/files.js
CHANGED
|
@@ -64,6 +64,16 @@ function getEntry(file, section, name) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Every entry in data[section], as {name: entry}. */
|
|
68
|
+
function listEntries(file, section) {
|
|
69
|
+
try {
|
|
70
|
+
const block = readJson(file)[section];
|
|
71
|
+
return block && typeof block === "object" ? block : {};
|
|
72
|
+
} catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
// ---- Codex's TOML: only the [mcp_servers.<name>] table is ever touched.
|
|
68
78
|
function tomlTable(name) {
|
|
69
79
|
return new RegExp(`^\\[mcp_servers\\.${name}\\][\\s\\S]*?(?=^\\[|(?![\\s\\S]))`, "m");
|
|
@@ -85,6 +95,15 @@ function getTomlServerUrl(file, name) {
|
|
|
85
95
|
return url ? url[1] : null;
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
/** {name: url} for every [mcp_servers.<name>] table. */
|
|
99
|
+
function listTomlServers(file) {
|
|
100
|
+
if (!fs.existsSync(file)) return {};
|
|
101
|
+
const out = {};
|
|
102
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
103
|
+
for (const m of raw.matchAll(/^\[mcp_servers\.([\w-]+)\]/gm)) out[m[1]] = getTomlServerUrl(file, m[1]) || "";
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
function removeTomlServer(file, name) {
|
|
89
108
|
if (!fs.existsSync(file)) return false;
|
|
90
109
|
const raw = fs.readFileSync(file, "utf8");
|
|
@@ -103,4 +122,6 @@ module.exports = {
|
|
|
103
122
|
setTomlServer,
|
|
104
123
|
getTomlServerUrl,
|
|
105
124
|
removeTomlServer,
|
|
125
|
+
listEntries,
|
|
126
|
+
listTomlServers,
|
|
106
127
|
};
|
package/lib/tools.js
CHANGED
|
@@ -13,6 +13,24 @@ const files = require("./files");
|
|
|
13
13
|
|
|
14
14
|
const NAME = "likho";
|
|
15
15
|
|
|
16
|
+
// Likho's hosted MCP addresses, old and new. An entry under another name that
|
|
17
|
+
// points here (or the old name "likho-hub") is a leftover second connection.
|
|
18
|
+
const LIKHO_HOSTS = ["likho.me", "www.likho.me", "mcp.likho.me", "likho-mcp.onrender.com"];
|
|
19
|
+
|
|
20
|
+
function isOldLikho(name, entry) {
|
|
21
|
+
if (name === NAME) return false;
|
|
22
|
+
if (name === "likho-hub") return true;
|
|
23
|
+
const url = typeof entry === "string" ? entry : entry && (entry.url || entry.httpUrl || entry.serverUrl);
|
|
24
|
+
try {
|
|
25
|
+
return LIKHO_HOSTS.includes(new URL(url).host);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Names of old or extra Likho entries in a {name: entry} block. */
|
|
32
|
+
const oldNames = (block) => Object.entries(block || {}).filter(([n, e]) => isOldLikho(n, e)).map(([n]) => n);
|
|
33
|
+
|
|
16
34
|
function onPath(cmd) {
|
|
17
35
|
const probe = process.platform === "win32" ? spawnSync("where", [cmd], { encoding: "utf8" }) : spawnSync("which", [cmd], { encoding: "utf8" });
|
|
18
36
|
return probe.status === 0 && Boolean((probe.stdout || "").trim());
|
|
@@ -56,6 +74,12 @@ function jsonTool({ id, name, section, entry, globalFile, projectFile, detect, s
|
|
|
56
74
|
const found = files.getEntry(this.file(ctx, scope), section, NAME);
|
|
57
75
|
return found ? found.url || found.httpUrl || found.serverUrl || "(set)" : null;
|
|
58
76
|
},
|
|
77
|
+
leftovers(ctx, scope) {
|
|
78
|
+
return oldNames(files.listEntries(this.file(ctx, scope), section));
|
|
79
|
+
},
|
|
80
|
+
removeOld(ctx, scope, name) {
|
|
81
|
+
return files.removeEntry(this.file(ctx, scope), section, name);
|
|
82
|
+
},
|
|
59
83
|
};
|
|
60
84
|
}
|
|
61
85
|
|
|
@@ -86,6 +110,13 @@ function tools(ctx) {
|
|
|
86
110
|
const found = files.getEntry(this.file(c, scope), "mcpServers", NAME);
|
|
87
111
|
return found ? found.url || "(set)" : null;
|
|
88
112
|
},
|
|
113
|
+
leftovers(c, scope) {
|
|
114
|
+
return oldNames(files.listEntries(this.file(c, scope), "mcpServers"));
|
|
115
|
+
},
|
|
116
|
+
removeOld(c, scope, name) {
|
|
117
|
+
if (scope === "global" && c.which("claude") && run("claude", ["mcp", "remove", name, "--scope", "user"])) return true;
|
|
118
|
+
return files.removeEntry(this.file(c, scope), "mcpServers", name);
|
|
119
|
+
},
|
|
89
120
|
},
|
|
90
121
|
{
|
|
91
122
|
id: "codex",
|
|
@@ -112,6 +143,13 @@ function tools(ctx) {
|
|
|
112
143
|
current(c) {
|
|
113
144
|
return files.getTomlServerUrl(this.file(c), NAME);
|
|
114
145
|
},
|
|
146
|
+
leftovers(c) {
|
|
147
|
+
return oldNames(files.listTomlServers(this.file(c)));
|
|
148
|
+
},
|
|
149
|
+
removeOld(c, scope, name) {
|
|
150
|
+
if (c.which("codex") && run("codex", ["mcp", "remove", name])) return true;
|
|
151
|
+
return files.removeTomlServer(this.file(c), name);
|
|
152
|
+
},
|
|
115
153
|
},
|
|
116
154
|
jsonTool({
|
|
117
155
|
id: "cursor",
|
|
@@ -189,4 +227,4 @@ function context(overrides = {}) {
|
|
|
189
227
|
};
|
|
190
228
|
}
|
|
191
229
|
|
|
192
|
-
module.exports = { tools, context, NAME };
|
|
230
|
+
module.exports = { tools, context, NAME, isOldLikho };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "likho-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Connect every AI tool on your computer (Claude Code, Codex, Cursor, VS Code, Gemini CLI, opencode, Windsurf) to Likho in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://likho.me",
|