omnispace 0.14.0 → 0.15.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/dist/ask.js +22 -5
- package/dist/index.js +8 -0
- package/dist/llm.js +150 -0
- package/dist/uso.js +44 -0
- package/package.json +1 -1
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|