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 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
- "claude-code": (session) => ({ cmd: "claude", args: ["-p", "--output-format", "json", ...(session ? ["--resume", session] : [])] }),
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
- return { file, replaced: Boolean(files.setEntry(file, "mcpServers", NAME, { type: "http", url })) };
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.4.1",
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",