likho-cli 0.4.1 → 0.5.0
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 +1 -1
- package/lib/helper.js +12 -5
- package/lib/tools.js +26 -1
- package/package.json +1 -1
package/bin/likho.js
CHANGED
|
@@ -316,7 +316,7 @@ async function connect(opts) {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
if (done.length) {
|
|
319
|
-
const lines = done.map((t) => `${t.name}: ${t.signIn}`);
|
|
319
|
+
const lines = done.map((t) => `${t.name}: ${(scope === "project" && t.projectSignIn) || t.signIn}`);
|
|
320
320
|
if (scope === "project") lines.push("", "The files written here hold only Likho's address, no keys: safe to commit.");
|
|
321
321
|
ui.note(lines.join("\n"), "Restart these tools, then sign in once in each");
|
|
322
322
|
}
|
package/lib/helper.js
CHANGED
|
@@ -27,7 +27,8 @@ const MAX_REPLY = 20_000;
|
|
|
27
27
|
// How each kind of agent runs without a window. Arguments are fixed: the
|
|
28
28
|
// message only ever travels on standard input, never on a command line.
|
|
29
29
|
const RUNNERS = {
|
|
30
|
-
|
|
30
|
+
// --max-turns: one wake-up can't run away; the reply reports its token usage.
|
|
31
|
+
"claude-code": (session) => ({ cmd: "claude", args: ["-p", "--output-format", "json", "--max-turns", "30", ...(session ? ["--resume", session] : [])] }),
|
|
31
32
|
codex: () => ({ cmd: "codex", args: ["exec", "--skip-git-repo-check", "-"] }),
|
|
32
33
|
gemini: () => ({ cmd: "gemini", args: [] }),
|
|
33
34
|
};
|
|
@@ -164,14 +165,20 @@ async function wake(cfg, org, job) {
|
|
|
164
165
|
const result = await run(cmd, args, { cwd: job.folder, input: job.prompt, env });
|
|
165
166
|
|
|
166
167
|
let reply = result.out.trim();
|
|
168
|
+
let tokens;
|
|
167
169
|
if (assistant === "claude-code") {
|
|
168
170
|
try {
|
|
169
171
|
const parsed = JSON.parse(reply);
|
|
172
|
+
// Every token the run processed (Likho's per-day limit counts these).
|
|
173
|
+
const u = parsed.usage || {};
|
|
174
|
+
const sum = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"]
|
|
175
|
+
.reduce((n, k) => n + (Number(u[k]) || 0), 0);
|
|
176
|
+
if (sum > 0) tokens = sum;
|
|
170
177
|
if (parsed.session_id) {
|
|
171
178
|
sessions[job.agent.number] = parsed.session_id;
|
|
172
179
|
fs.writeFileSync(SESSIONS, JSON.stringify(sessions, null, 2));
|
|
173
180
|
}
|
|
174
|
-
if (parsed.is_error && LIMIT_TEXT.test(String(parsed.result))) return finish({ status: "limited", retry_after: 3600 });
|
|
181
|
+
if (parsed.is_error && LIMIT_TEXT.test(String(parsed.result))) return finish({ status: "limited", retry_after: 3600, tokens });
|
|
175
182
|
reply = String(parsed.result || "").trim();
|
|
176
183
|
} catch {
|
|
177
184
|
// not JSON: an error message, handled below
|
|
@@ -183,9 +190,9 @@ async function wake(cfg, org, job) {
|
|
|
183
190
|
if (/not recognized|ENOENT|not found/i.test(result.err)) return finish({ status: "failed", error: `${cmd} isn't installed or isn't on this computer's PATH` });
|
|
184
191
|
return finish({ status: "failed", error: (result.err.trim().split("\n").pop() || `exited with ${result.code}`).slice(0, 250) });
|
|
185
192
|
}
|
|
186
|
-
if (!reply) return finish({ status: "failed", error: "it finished without an answer" });
|
|
187
|
-
log(`answered for ${job.agent.name}`);
|
|
188
|
-
return finish({ status: "done", reply: reply.slice(0, MAX_REPLY) });
|
|
193
|
+
if (!reply) return finish({ status: "failed", error: "it finished without an answer", tokens });
|
|
194
|
+
log(`answered for ${job.agent.name}${tokens ? ` (${tokens} tokens)` : ""}`);
|
|
195
|
+
return finish({ status: "done", reply: reply.slice(0, MAX_REPLY), tokens });
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
package/lib/tools.js
CHANGED
|
@@ -83,6 +83,26 @@ function jsonTool({ id, name, section, entry, globalFile, projectFile, detect, s
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Claude Code keeps servers from a project's .mcp.json "pending approval"
|
|
88
|
+
* until someone approves them, and the VS Code panel never asks: the server
|
|
89
|
+
* just doesn't appear. The person running likho-cli has chosen to add it, so
|
|
90
|
+
* record that approval in the project's own, uncommitted local settings.
|
|
91
|
+
*/
|
|
92
|
+
function approveProjectServer(ctx) {
|
|
93
|
+
const file = path.join(ctx.cwd, ".claude", "settings.local.json");
|
|
94
|
+
try {
|
|
95
|
+
const data = files.readJson(file);
|
|
96
|
+
const list = Array.isArray(data.enabledMcpjsonServers) ? data.enabledMcpjsonServers : [];
|
|
97
|
+
if (list.includes(NAME)) return;
|
|
98
|
+
data.enabledMcpjsonServers = [...list, NAME];
|
|
99
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
100
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
|
|
101
|
+
} catch {
|
|
102
|
+
// unreadable local settings: Claude Code will ask instead
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
function tools(ctx) {
|
|
87
107
|
const list = [
|
|
88
108
|
{
|
|
@@ -90,6 +110,9 @@ function tools(ctx) {
|
|
|
90
110
|
name: "Claude Code",
|
|
91
111
|
detect: () => ctx.which("claude") || fs.existsSync(path.join(ctx.home, ".claude.json")) || fs.existsSync(path.join(ctx.home, ".claude")),
|
|
92
112
|
signIn: "In Claude Code run /mcp, pick likho, choose Authenticate.",
|
|
113
|
+
// A new folder's .mcp.json stays "pending approval" until Claude Code is
|
|
114
|
+
// run there once in a terminal (the VS Code panel doesn't ask).
|
|
115
|
+
projectSignIn: "If likho isn't in /mcp: run `claude` once in a terminal in this folder, trust it and approve likho. Then /mcp, pick likho, Authenticate.",
|
|
93
116
|
project: true,
|
|
94
117
|
global: true,
|
|
95
118
|
file: (c, scope) => (scope === "project" ? path.join(c.cwd, ".mcp.json") : path.join(c.home, ".claude.json")),
|
|
@@ -100,7 +123,9 @@ function tools(ctx) {
|
|
|
100
123
|
run("claude", ["mcp", "remove", NAME, "--scope", "user"]);
|
|
101
124
|
if (run("claude", ["mcp", "add", "--transport", "http", "--scope", "user", NAME, url])) return { file, replaced: false };
|
|
102
125
|
}
|
|
103
|
-
|
|
126
|
+
const replaced = Boolean(files.setEntry(file, "mcpServers", NAME, { type: "http", url }));
|
|
127
|
+
if (scope === "project") approveProjectServer(c);
|
|
128
|
+
return { file, replaced };
|
|
104
129
|
},
|
|
105
130
|
remove(c, scope) {
|
|
106
131
|
if (scope === "global" && c.which("claude") && run("claude", ["mcp", "remove", NAME, "--scope", "user"])) return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "likho-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|