omnispace 0.13.1 → 0.15.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/dist/ask.js CHANGED
@@ -102,6 +102,9 @@ export function formatAsk(res, opts) {
102
102
  }
103
103
  if (s.ms !== undefined)
104
104
  out.push(` tempo: ${(s.ms / 1000).toFixed(1)} s`);
105
+ if (s.steps?.length) {
106
+ out.push(` passos do modelo: ${s.steps.map((st, i) => `${i + 1}) ${st.finish}, ${st.toolCalls} ferramenta(s), ${st.textChars} caracteres de texto`).join("; ")}`);
107
+ }
105
108
  }
106
109
  return out;
107
110
  }
@@ -167,11 +170,25 @@ export async function askSpace(args, opts = {}) {
167
170
  }
168
171
  if (!args.sourcesOnly)
169
172
  console.log("\n→ Consultando a versão publicada...");
170
- const res = await callApi(config.apiUrl, token, "/api/cli/ask", {
171
- spaceId: config.spaceId,
172
- question: args.question,
173
- sourcesOnly: args.sourcesOnly,
174
- });
173
+ const body = { spaceId: config.spaceId, question: args.question, sourcesOnly: args.sourcesOnly };
174
+ let res = await callApi(config.apiUrl, token, "/api/cli/ask", body);
175
+ // The person's own LLM key failed (docs/plan/byok-e-medicao.md, decision 1): the Omni's model
176
+ // answers only if they say yes now. Without a terminal, nothing else is tried.
177
+ if (!res.ok && res.byokFailed === true) {
178
+ console.log(`\n${terminalSafe(String(res.error))}`);
179
+ const interactive = opts.interactive ?? Boolean(process.stdin.isTTY);
180
+ if (!interactive) {
181
+ console.log("Sem terminal para perguntar; nada foi respondido. Rode de novo com um terminal para usar o modelo do Omni.\n");
182
+ return 1;
183
+ }
184
+ const answer = (await (opts.ask ?? askTerminal)("Responder com o modelo gratuito do Omni desta vez? [s/n] ")).toLowerCase();
185
+ if (answer !== "s" && answer !== "sim") {
186
+ console.log("Nada foi respondido.\n");
187
+ return 1;
188
+ }
189
+ console.log("\n→ Consultando com o modelo do Omni...");
190
+ res = await callApi(config.apiUrl, token, "/api/cli/ask", { ...body, engine: "omni" });
191
+ }
175
192
  if (!res.ok) {
176
193
  console.log(`\nSem resposta: ${res.error}`);
177
194
  if (res.expired || res.status === 401)
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ import { linkExistingSpace } from "./vincular.js";
19
19
  import { askSpace, parseAskArgs } from "./ask.js";
20
20
  import { runEnv } from "./env.js";
21
21
  import { runPermissoes } from "./permissoes.js";
22
+ import { runUso } from "./uso.js";
23
+ import { runLlm } from "./llm.js";
22
24
  const USAGE = `
23
25
  Omni CLI
24
26
 
@@ -41,6 +43,8 @@ Uso:
41
43
  omni desvincular [pasta] tira esta máquina da conta e o .omni/ das pastas (pede 2 confirmações)
42
44
  omni env push|list|rm [pasta] guarda chaves do .env no cofre do espaço, uma a uma (confirma no navegador)
43
45
  omni permissoes [set <conector> <ação> <estado>] [pasta] o que o agente pode fazer sozinho em cada ação
46
+ omni uso tokens e custo de modelo da sua conta hoje e no mês, por rota e por chave
47
+ omni llm [set anthropic|rm] usar a sua chave da Anthropic no lugar do modelo do Omni, com teto diário
44
48
 
45
49
  A CLI não reorganiza a sua pasta. Com --link, o que fazer com o que foi
46
50
  varrido é decidido na tela de revisão, no navegador.
@@ -71,6 +75,10 @@ async function main(argv) {
71
75
  return runEnv(args.slice(1), { openBrowser: process.stdout.isTTY });
72
76
  if (command === "permissoes")
73
77
  return runPermissoes(args.slice(1), { openBrowser: process.stdout.isTTY });
78
+ if (command === "uso")
79
+ return runUso(args.slice(1));
80
+ if (command === "llm")
81
+ return runLlm(args.slice(1), { openBrowser: process.stdout.isTTY });
74
82
  if (command === "vincular") {
75
83
  const rest = args.slice(1);
76
84
  const espacoIndex = rest.indexOf("--espaco");
package/dist/llm.js ADDED
@@ -0,0 +1,150 @@
1
+ import { resolve } from "node:path";
2
+ import { createInterface } from "node:readline";
3
+ import { callApi } from "./api.js";
4
+ import { defaultApiUrl, loadToken } from "./config.js";
5
+ import { context, refused, verify } from "./env.js";
6
+ import { RETRY_DELAYS_MS } from "./retry.js";
7
+ // `omnispace llm` (docs/plan/byok-e-medicao.md): the person's own LLM key. With it on, questions
8
+ // run on their model and the cost comes out of their provider account, up to a daily ceiling in
9
+ // US$ checked before each call. The key is typed hidden (never an argument: it would stay in the
10
+ // shell history), goes to the Vault once the browser approves, and is never shown again.
11
+ const MODELS = { haiku: "claude-haiku-4-5", sonnet: "claude-sonnet-5-5" };
12
+ export const LLM_USAGE = `Uso:
13
+ omnispace llm qual modelo responde as suas perguntas, teto e gasto de hoje
14
+ omnispace llm set anthropic [--modelo haiku|sonnet] [--teto <US$ por dia>] [pasta]
15
+ liga a sua chave da Anthropic (pedida escondida no terminal)
16
+ omnispace llm rm [pasta] apaga a sua chave; as perguntas voltam para o modelo do Omni
17
+ Gravar e apagar a chave pedem confirmação no navegador. Padrão: haiku, teto de US$ 2 por dia.`;
18
+ export function formatLlmStatus(s) {
19
+ if (!s.provider) {
20
+ return [
21
+ "As suas perguntas rodam no modelo gratuito do Omni (gpt-oss:20b), dentro da cota diária do seu plano.",
22
+ "Para usar a sua chave da Anthropic: omnispace llm set anthropic",
23
+ ];
24
+ }
25
+ const out = [
26
+ `Chave: ${s.provider}, terminando em ${s.keyLast4}, modelo ${s.model}${s.enabled ? "" : " (desligada)"}`,
27
+ `Teto: US$ ${(s.dailyCapUsd ?? 0).toFixed(2)} por dia; gasto hoje: US$ ${(s.spentTodayUsd ?? 0).toFixed(6)}`,
28
+ ];
29
+ out.push(s.engine === "pessoa" ? "As suas perguntas rodam na sua chave." : "As suas perguntas rodam no modelo do Omni.");
30
+ return out;
31
+ }
32
+ /** Reads a line without echoing it: the key never appears on screen. */
33
+ function askHidden(question) {
34
+ return new Promise((done) => {
35
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
36
+ const write = rl._writeToOutput;
37
+ process.stdout.write(question);
38
+ rl._writeToOutput = () => { };
39
+ rl.question("", (answer) => {
40
+ if (write)
41
+ rl._writeToOutput = write;
42
+ rl.close();
43
+ process.stdout.write("\n");
44
+ done(answer.trim());
45
+ });
46
+ });
47
+ }
48
+ export function parseSetArgs(args) {
49
+ const rest = [...args];
50
+ const provider = rest.shift();
51
+ if (provider !== "anthropic")
52
+ return { error: "Por enquanto só a Anthropic: omnispace llm set anthropic" };
53
+ let model = MODELS.haiku;
54
+ let cap = "2";
55
+ let folder = ".";
56
+ while (rest.length > 0) {
57
+ const a = rest.shift();
58
+ if (a === "--modelo") {
59
+ const m = MODELS[(rest.shift() ?? "").toLowerCase()];
60
+ if (!m)
61
+ return { error: "Modelo: haiku ou sonnet." };
62
+ model = m;
63
+ }
64
+ else if (a === "--teto") {
65
+ const v = (rest.shift() ?? "").replace(",", ".");
66
+ if (!/^\d{1,4}(\.\d{1,2})?$/.test(v) || Number(v) <= 0 || Number(v) > 1000)
67
+ return { error: "Teto: um valor em US$ por dia, de 0.01 a 1000." };
68
+ cap = String(Number(v));
69
+ }
70
+ else if (a.startsWith("--")) {
71
+ return { error: `Opção desconhecida: ${a}` };
72
+ }
73
+ else {
74
+ folder = a;
75
+ }
76
+ }
77
+ return { provider, model, cap, folder };
78
+ }
79
+ export async function runLlm(args, opts = {}) {
80
+ const log = opts.log ?? ((line) => console.log(line));
81
+ const sub = args[0];
82
+ if (!sub || sub === "status") {
83
+ const apiUrl = defaultApiUrl();
84
+ const token = await loadToken(apiUrl);
85
+ if (!token) {
86
+ console.error("Nenhuma sessão da CLI nesta máquina. Para entrar: omnispace login");
87
+ return 1;
88
+ }
89
+ const res = await callApi(apiUrl, token, "/api/cli/llm/status", {}, { retryDelaysMs: RETRY_DELAYS_MS });
90
+ if (!res.ok) {
91
+ refused(res, "Falha ao ler a chave de LLM");
92
+ return 1;
93
+ }
94
+ for (const line of formatLlmStatus(res))
95
+ log(line);
96
+ return 0;
97
+ }
98
+ if (sub === "set") {
99
+ const parsed = parseSetArgs(args.slice(1));
100
+ if ("error" in parsed) {
101
+ console.error(`${parsed.error}\n\n${LLM_USAGE}`);
102
+ return 1;
103
+ }
104
+ const interactive = opts.interactive ?? Boolean(process.stdin.isTTY);
105
+ if (!interactive) {
106
+ console.error("A chave é pedida escondida no terminal; rode num terminal.");
107
+ return 1;
108
+ }
109
+ const ctx = await context(resolve(parsed.folder));
110
+ if (!ctx)
111
+ return 1;
112
+ log(`Modelo ${parsed.model}, teto de US$ ${Number(parsed.cap).toFixed(2)} por dia. Cada pergunta passa a ser cobrada na sua conta da Anthropic.`);
113
+ const key = opts.ask ? await opts.ask("Chave da API da Anthropic (sk-ant-...): ") : await askHidden("Chave da API da Anthropic (sk-ant-..., não aparece na tela): ");
114
+ if (!/^sk-ant-[A-Za-z0-9_-]{20,200}$/.test(key)) {
115
+ console.error("Isso não parece uma chave da API da Anthropic (sk-ant-...). Nada foi enviado.");
116
+ return 1;
117
+ }
118
+ const refs = [{ file: "llm:anthropic", key: `${parsed.model}|${parsed.cap}` }];
119
+ const verification = await verify(ctx, "llm_set", refs, opts);
120
+ if (!verification)
121
+ return 1;
122
+ // Not retried: it carries the key, and a failure is better said than repeated.
123
+ const res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/llm/set", { spaceId: ctx.spaceId, verification, keys: refs, key });
124
+ if (!res.ok) {
125
+ refused(res, "A chave não foi ligada");
126
+ return 1;
127
+ }
128
+ log(`✓ Chave terminando em ${String(res.keyLast4)} ligada. As suas perguntas rodam agora em ${String(res.model)}, até US$ ${Number(res.dailyCapUsd).toFixed(2)} por dia.`);
129
+ log(" Para ver o gasto: omnispace uso Para desligar: omnispace llm rm");
130
+ return 0;
131
+ }
132
+ if (sub === "rm") {
133
+ const ctx = await context(resolve(args[1] ?? "."));
134
+ if (!ctx)
135
+ return 1;
136
+ const refs = [{ file: "llm:anthropic", key: "remover" }];
137
+ const verification = await verify(ctx, "llm_rm", refs, opts);
138
+ if (!verification)
139
+ return 1;
140
+ const res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/llm/remove", { spaceId: ctx.spaceId, verification, keys: refs });
141
+ if (!res.ok) {
142
+ refused(res, "A chave não foi apagada");
143
+ return 1;
144
+ }
145
+ log(res.removed ? "✓ Chave apagada do Omni. As suas perguntas voltam para o modelo do Omni." : "Não havia chave ligada.");
146
+ return 0;
147
+ }
148
+ console.error(LLM_USAGE);
149
+ return 1;
150
+ }
package/dist/uso.js ADDED
@@ -0,0 +1,44 @@
1
+ import { callApi } from "./api.js";
2
+ import { defaultApiUrl, loadToken } from "./config.js";
3
+ import { RETRY_DELAYS_MS } from "./retry.js";
4
+ const KEY_LABEL = { omni: "chave do Omni", pessoa: "sua chave" };
5
+ const int = (n) => n.toLocaleString("pt-BR");
6
+ const usd = (n) => `US$ ${n.toFixed(6).replace(/0{1,4}$/, "")}`;
7
+ export function formatUsage(res) {
8
+ const out = [];
9
+ const block = (title, s) => {
10
+ out.push(`${title}: ${s.calls} chamada(s), ${usd(s.costUsd)}`);
11
+ for (const g of s.groups) {
12
+ const cache = g.cacheReadTokens || g.cacheWriteTokens ? ` (cache: ${int(g.cacheReadTokens)} lidos, ${int(g.cacheWriteTokens)} gravados)` : "";
13
+ out.push(` ${g.route.padEnd(7)} ${`${g.provider}/${g.model}`.padEnd(28)} ${(KEY_LABEL[g.keyOwner] ?? g.keyOwner).padEnd(14)} ${String(g.calls).padStart(4)}x ${int(g.inputTokens)} entrada${cache}, ${int(g.outputTokens)} saída ${usd(g.costUsd)}`);
14
+ }
15
+ };
16
+ block(`Hoje (${res.day}, UTC)`, res.today);
17
+ out.push("");
18
+ block(`Este mês (${res.month})`, res.thisMonth);
19
+ out.push("");
20
+ out.push(res.quota.limit === null
21
+ ? `Cota do Omni hoje: ${int(res.quota.tokensUsed)} tokens, sem limite no seu plano.`
22
+ : `Cota do Omni hoje: ${int(res.quota.tokensUsed)} de ${int(res.quota.limit)} tokens.`);
23
+ return out;
24
+ }
25
+ export async function runUso(args) {
26
+ if (args.length > 0) {
27
+ console.error("Uso: omnispace uso (o uso de modelo da sua conta hoje e no mês)");
28
+ return 1;
29
+ }
30
+ const apiUrl = defaultApiUrl();
31
+ const token = await loadToken(apiUrl);
32
+ if (!token) {
33
+ console.error("Nenhuma sessão da CLI nesta máquina. Para entrar: omnispace login");
34
+ return 1;
35
+ }
36
+ const res = await callApi(apiUrl, token, "/api/cli/usage", {}, { retryDelaysMs: RETRY_DELAYS_MS });
37
+ if (!res.ok) {
38
+ console.error(`Falha ao ler o uso: ${res.error}`);
39
+ return 1;
40
+ }
41
+ for (const line of formatUsage(res))
42
+ console.log(line);
43
+ return 0;
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnispace",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Omni CLI (alpha): transforma uma pasta da sua máquina em um espaço Omni e sincroniza entre máquinas.",
5
5
  "type": "module",
6
6
  "bin": {