omnispace 0.9.0 → 0.12.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/README.md +20 -2
- package/dist/ask.js +115 -0
- package/dist/dotenv.js +100 -0
- package/dist/env.js +243 -0
- package/dist/include.js +12 -2
- package/dist/index.js +20 -0
- package/dist/login.js +1 -1
- package/dist/permissoes.js +92 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,13 +84,31 @@ Tirar esta máquina da conta e o `.omni/` das pastas dela (pede duas confirmaç
|
|
|
84
84
|
npx -y omnispace@latest desvincular
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
Ver e mudar o que o agente pode fazer sozinho com cada ação dos conectores do espaço. Por padrão, leitura é automática, escrita pede confirmação e ação destrutiva fica bloqueada. Restringir vale na hora; liberar pede confirmação no navegador:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
npx -y omnispace@latest permissoes
|
|
91
|
+
npx -y omnispace@latest permissoes set slack send_message bloqueado
|
|
92
|
+
```
|
|
93
|
+
|
|
87
94
|
No Windows, se o caminho tiver espaço ou apóstrofo, entre na pasta com aspas duplas: `cd "C:\caminho\da\pasta"`.
|
|
88
95
|
|
|
89
96
|
## O que o Omni lê e envia
|
|
90
97
|
|
|
91
98
|
- A pasta é lida para montar a lista de arquivos. Sobem só arquivos de texto (Markdown, código, JSON e afins), até 25 MB por arquivo e 100 MB por espaço. Nome de arquivo que o Windows não aceita (como `CON`, `a:b.md` ou terminado em ponto) fica de fora, para o espaço abrir em qualquer sistema.
|
|
92
|
-
- Nunca sobem: `.env` e variações, chaves (`.pem`, `.key`, `id_rsa`), pastas `.git`, `node_modules` e de build, imagens e mídia, e o `.claude/settings.local.json` do Claude Code (configuração de cada máquina; o `.claude/settings.json` sincroniza).
|
|
93
|
-
- Arquivos `.env` são lidos só para listar os nomes das chaves; nenhum valor é guardado ou enviado.
|
|
99
|
+
- Nunca sobem: `.env` e variações, chaves (`.pem`, `.key`, `id_rsa`), pastas `.git`, `node_modules` e de build, imagens e mídia, e o `.claude/settings.local.json` do Claude Code (configuração de cada máquina; o `.claude/settings.json` sincroniza) e o `.claude/worktrees/` (cópias do repositório das sessões paralelas do Claude Code).
|
|
100
|
+
- Arquivos `.env` são lidos só para listar os nomes das chaves; nenhum valor é guardado ou enviado. A exceção é `omnispace env push` (abaixo), quando você escolhe chave por chave.
|
|
94
101
|
- Antes de enviar, a CLI mostra o que sobe e o que fica de fora e pede confirmação.
|
|
95
102
|
- Na pasta, o Omni grava só `.omni/space.json` (e, só enquanto um sync roda, `.omni/lock`, `.omni/tmp/` e `.omni/pull-pending.json`, apagados no fim; `.omni/backups/` e `.omni/conflicts.json` quando há conflito). Se um sync for interrompido, rodar o mesmo comando de novo continua de onde parou. Nada disso sincroniza. O token de acesso fica em `~/.omni/credentials.json`, fora de qualquer projeto, legível só pelo seu usuário.
|
|
96
103
|
- A CLI só fala com o servidor por HTTPS (`http` só vale para um servidor na própria máquina, em desenvolvimento).
|
|
104
|
+
|
|
105
|
+
## Credenciais do `.env` no cofre do espaço
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
npx -y omnispace@latest env push
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Com a CLI logada, dentro da pasta do espaço: a CLI lê os arquivos `.env` da raiz (menos `.env.example`, `.env.sample` e `.env.template`) e pergunta chave por chave se ela vai para o cofre do espaço. Mostra o nome, o arquivo e no máximo os 4 últimos caracteres do valor. Antes de enviar, pede que você confirme no navegador, logado na sua conta, digitando o código do terminal. O valor vai cifrado para o cofre (Vault) e não aparece em tela, log nem resposta. Sem terminal, nada é enviado.
|
|
112
|
+
|
|
113
|
+
- `env list` mostra as chaves guardadas (só nomes).
|
|
114
|
+
- `env rm <CHAVE> [--arquivo .env.local]` apaga do cofre de vez, também com confirmação no navegador. Se um conector usava a chave, ele fica desligado.
|
package/dist/ask.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { callApi } from "./api.js";
|
|
3
|
+
import { loadToken, readSpaceConfig } from "./config.js";
|
|
4
|
+
export const ASK_USAGE = 'Uso: omnispace ask "<pergunta>" [pasta] [--fontes] [--detalhes]';
|
|
5
|
+
/** `ask "<pergunta>" [pasta] [--fontes] [--detalhes]`. The question must come quoted: one argument. */
|
|
6
|
+
export function parseAskArgs(rest, cwd = process.cwd()) {
|
|
7
|
+
const positional = rest.filter((a) => !a.startsWith("--"));
|
|
8
|
+
const unknown = rest.filter((a) => a.startsWith("--") && a !== "--fontes" && a !== "--detalhes");
|
|
9
|
+
if (unknown.length > 0)
|
|
10
|
+
return { error: `Opção desconhecida: ${unknown[0]}\n${ASK_USAGE}` };
|
|
11
|
+
if (positional.length === 0 || !positional[0].trim())
|
|
12
|
+
return { error: ASK_USAGE };
|
|
13
|
+
if (positional.length > 2)
|
|
14
|
+
return { error: `Coloque a pergunta entre aspas.\n${ASK_USAGE}` };
|
|
15
|
+
return {
|
|
16
|
+
question: positional[0].trim(),
|
|
17
|
+
folder: resolve(cwd, positional[1] ?? "."),
|
|
18
|
+
sourcesOnly: rest.includes("--fontes"),
|
|
19
|
+
details: rest.includes("--detalhes"),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const REASON = { index: "index.md", log: "log.md", link: "apontado no index", termo: "busca por termo" };
|
|
23
|
+
/** What `omnispace ask` prints, as lines. `baseVersion`: the version this folder is on. */
|
|
24
|
+
export function formatAsk(res, opts) {
|
|
25
|
+
const out = [];
|
|
26
|
+
const consultedList = () => {
|
|
27
|
+
for (const c of res.consulted) {
|
|
28
|
+
out.push(` [${c.id}] ${c.path} (${REASON[c.reason] ?? c.reason}, ${c.chars.toLocaleString("pt-BR")} caracteres${c.truncated ? ", em parte" : ""})`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
if (opts.sourcesOnly) {
|
|
32
|
+
out.push(`Arquivos que entrariam na resposta (versão ${res.version}, sem chamar o modelo):`);
|
|
33
|
+
if (res.consulted.length === 0)
|
|
34
|
+
out.push(" nenhum");
|
|
35
|
+
consultedList();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
out.push("Resposta:", res.answer ?? "", "");
|
|
39
|
+
const sources = res.sources ?? [];
|
|
40
|
+
if (sources.length > 0) {
|
|
41
|
+
out.push("Fontes:");
|
|
42
|
+
for (const s of sources)
|
|
43
|
+
out.push(` [${s.id}] ${s.path} (versão ${s.version})`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
out.push("Fontes: nenhuma citada. Esta resposta não se apoia em arquivo do espaço.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
out.push("", "Limites:");
|
|
50
|
+
for (const limit of res.limits)
|
|
51
|
+
out.push(` - ${limit}`);
|
|
52
|
+
if (res.version > 0 && opts.baseVersion < res.version) {
|
|
53
|
+
out.push(` - Esta pasta está na versão ${opts.baseVersion}; a resposta usa a versão ${res.version} do servidor.`);
|
|
54
|
+
}
|
|
55
|
+
if (opts.details) {
|
|
56
|
+
const s = res.stats;
|
|
57
|
+
if (!opts.sourcesOnly) {
|
|
58
|
+
out.push("", "Consultados:");
|
|
59
|
+
consultedList();
|
|
60
|
+
}
|
|
61
|
+
out.push("", "Custo desta pergunta:");
|
|
62
|
+
out.push(` arquivos: ${s.filesUsed ?? 0} usados, ${s.filesScanned ?? 0} lidos na busca, ${s.filesInVersion ?? 0} na versão`);
|
|
63
|
+
out.push(` contexto: ${(s.contextChars ?? 0).toLocaleString("pt-BR")} caracteres`);
|
|
64
|
+
if (s.model) {
|
|
65
|
+
out.push(` modelo: ${s.model}, ${s.inputTokens ?? 0} tokens de entrada, ${s.outputTokens ?? 0} de saída`);
|
|
66
|
+
out.push(` custo estimado: US$ ${(s.costUsd ?? 0).toFixed(5)}`);
|
|
67
|
+
}
|
|
68
|
+
if (s.ms !== undefined)
|
|
69
|
+
out.push(` tempo: ${(s.ms / 1000).toFixed(1)} s`);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `omnispace ask`: a question about this folder's space, answered by the server from the latest
|
|
75
|
+
* published version. Reads nothing from the folder besides .omni/space.json, and writes nothing.
|
|
76
|
+
* Never retried: an answer costs tokens.
|
|
77
|
+
*/
|
|
78
|
+
export async function askSpace(args) {
|
|
79
|
+
const config = await readSpaceConfig(args.folder);
|
|
80
|
+
if (!config) {
|
|
81
|
+
console.log(`\n${basename(args.folder)} não é um espaço Omni (não tem .omni/space.json).`);
|
|
82
|
+
console.log("Rode o ask de dentro da pasta de um espaço, ou passe o caminho dela.\n");
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
if (!config.spaceId) {
|
|
86
|
+
console.log("\nEsta pasta foi vinculada pelo navegador, mas o espaço ainda não foi criado.\n");
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const token = await loadToken(config.apiUrl);
|
|
90
|
+
if (!token) {
|
|
91
|
+
console.log(`\nNenhuma sessão da CLI nesta máquina para ${config.apiUrl}.`);
|
|
92
|
+
console.log("Para entrar: omnispace login\n");
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
if (!args.sourcesOnly)
|
|
96
|
+
console.log("\n→ Consultando a versão publicada...");
|
|
97
|
+
const res = await callApi(config.apiUrl, token, "/api/cli/ask", {
|
|
98
|
+
spaceId: config.spaceId,
|
|
99
|
+
question: args.question,
|
|
100
|
+
sourcesOnly: args.sourcesOnly,
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
console.log(`\nSem resposta: ${res.error}`);
|
|
104
|
+
if (res.expired || res.status === 401)
|
|
105
|
+
console.log("Para entrar de novo: omnispace login");
|
|
106
|
+
console.log("");
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
console.log("");
|
|
110
|
+
const opts = { baseVersion: config.baseVersion, sourcesOnly: args.sourcesOnly, details: args.details };
|
|
111
|
+
for (const line of formatAsk(res, opts))
|
|
112
|
+
console.log(line);
|
|
113
|
+
console.log("");
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
package/dist/dotenv.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// Leitura dos `.env` da pasta para `omnispace env push` (pós-marco C1; ADR 004). Diferente da
|
|
4
|
+
// varredura (scan.ts), aqui o valor é lido de propósito, porque a pessoa vai decidir chave por chave
|
|
5
|
+
// o que sobe para o cofre. O valor fica só na memória deste processo: nunca é impresso (só os 4
|
|
6
|
+
// últimos caracteres, e só de valor longo), nunca vai para arquivo nem para log.
|
|
7
|
+
const MAX_ENV_BYTES = 256 * 1024;
|
|
8
|
+
/** Same rule as the server (lib/cli-verify.ts): `.env`, `.env.local`, `.env.production.local`. */
|
|
9
|
+
export const ENV_FILE_RE = /^\.env(\.[A-Za-z0-9_-]{1,32}){0,3}$/;
|
|
10
|
+
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
11
|
+
/** Files that hold placeholders, not the real values. */
|
|
12
|
+
const TEMPLATE_RE = /(^|\.)(example|sample|template|dist|defaults?)(\.|$)/i;
|
|
13
|
+
/**
|
|
14
|
+
* Parses a `.env` the way dotenv does for the common cases: `export KEY=v`, spaces around `=`,
|
|
15
|
+
* unquoted values with a trailing ` # comment`, single quotes (literal), double quotes (with \n and
|
|
16
|
+
* multi-line). A later line with the same key wins. Lines that are not assignments are skipped.
|
|
17
|
+
*/
|
|
18
|
+
export function parseDotenv(text, file = ".env") {
|
|
19
|
+
const lines = text.replace(/^/, "").split(/\r?\n/);
|
|
20
|
+
const byKey = new Map();
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i].trim();
|
|
23
|
+
if (!line || line.startsWith("#"))
|
|
24
|
+
continue;
|
|
25
|
+
const eq = line.indexOf("=");
|
|
26
|
+
if (eq <= 0)
|
|
27
|
+
continue;
|
|
28
|
+
const key = line.slice(0, eq).replace(/^export\s+/, "").trim();
|
|
29
|
+
if (!ENV_KEY_RE.test(key))
|
|
30
|
+
continue;
|
|
31
|
+
let rest = line.slice(eq + 1).trim();
|
|
32
|
+
let value;
|
|
33
|
+
const quote = rest[0];
|
|
34
|
+
if (quote === '"' || quote === "'" || quote === "`") {
|
|
35
|
+
// Multi-line quoted value: keep reading lines until the closing quote.
|
|
36
|
+
let body = rest.slice(1);
|
|
37
|
+
let close = findClosing(body, quote);
|
|
38
|
+
while (close < 0 && i + 1 < lines.length) {
|
|
39
|
+
i++;
|
|
40
|
+
body += `\n${lines[i]}`;
|
|
41
|
+
close = findClosing(body, quote);
|
|
42
|
+
}
|
|
43
|
+
if (close < 0)
|
|
44
|
+
continue; // never closed: not a value we can trust
|
|
45
|
+
value = body.slice(0, close);
|
|
46
|
+
if (quote === '"')
|
|
47
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const hash = rest.search(/\s#/);
|
|
51
|
+
if (hash >= 0)
|
|
52
|
+
rest = rest.slice(0, hash);
|
|
53
|
+
value = rest.trim();
|
|
54
|
+
}
|
|
55
|
+
byKey.set(key, { file, key, value });
|
|
56
|
+
}
|
|
57
|
+
return [...byKey.values()];
|
|
58
|
+
}
|
|
59
|
+
function findClosing(body, quote) {
|
|
60
|
+
for (let i = 0; i < body.length; i++) {
|
|
61
|
+
if (body[i] === "\\" && quote === '"') {
|
|
62
|
+
i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (body[i] === quote)
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
return -1;
|
|
69
|
+
}
|
|
70
|
+
/** The `.env` files at the root of the folder, without templates, in a stable order. */
|
|
71
|
+
export async function listEnvFiles(root) {
|
|
72
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
73
|
+
return entries
|
|
74
|
+
.filter((e) => e.isFile() && ENV_FILE_RE.test(e.name) && !TEMPLATE_RE.test(e.name.slice(4)))
|
|
75
|
+
.map((e) => e.name)
|
|
76
|
+
.sort((a, b) => (a === ".env" ? -1 : b === ".env" ? 1 : a.localeCompare(b)));
|
|
77
|
+
}
|
|
78
|
+
/** Every variable of every `.env` at the root. Empty values are skipped (nothing to keep). */
|
|
79
|
+
export async function readEnvVars(root) {
|
|
80
|
+
const vars = [];
|
|
81
|
+
const skipped = [];
|
|
82
|
+
for (const file of await listEnvFiles(root)) {
|
|
83
|
+
const path = join(root, file);
|
|
84
|
+
if ((await stat(path)).size > MAX_ENV_BYTES) {
|
|
85
|
+
skipped.push(file);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
vars.push(...parseDotenv(await readFile(path, "utf8"), file).filter((v) => v.value !== ""));
|
|
89
|
+
}
|
|
90
|
+
return { vars, skipped };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* What the terminal shows of a value: the last 4 characters, and only when the value is long enough
|
|
94
|
+
* for 4 characters to say nothing useful about it (ADR 004 and barrier 4).
|
|
95
|
+
*/
|
|
96
|
+
export function maskValue(value) {
|
|
97
|
+
if (value.length < 12 || /\s/.test(value.slice(-4)))
|
|
98
|
+
return "(valor não mostrado)";
|
|
99
|
+
return `••••${value.slice(-4)}`;
|
|
100
|
+
}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { callApi } from "./api.js";
|
|
4
|
+
import { insecureApiUrl, loadToken, readSpaceConfig } from "./config.js";
|
|
5
|
+
import { maskValue, readEnvVars } from "./dotenv.js";
|
|
6
|
+
import { openBrowser } from "./login.js";
|
|
7
|
+
import { RETRY_DELAYS_MS } from "./retry.js";
|
|
8
|
+
const STATUS_TEXT = {
|
|
9
|
+
active: "conectado",
|
|
10
|
+
active_limited: "conectado com limitação",
|
|
11
|
+
error: "com erro",
|
|
12
|
+
disabled: "desligado",
|
|
13
|
+
};
|
|
14
|
+
const USAGE = `Uso:
|
|
15
|
+
omnispace env push [pasta] pergunta chave por chave o que vai para o cofre do espaço
|
|
16
|
+
omnispace env list [pasta] mostra as chaves guardadas (só nomes)
|
|
17
|
+
omnispace env rm <CHAVE>... [--arquivo .env] [--pasta <pasta>] apaga do cofre, de vez`;
|
|
18
|
+
async function askTerminal(question) {
|
|
19
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
try {
|
|
21
|
+
return (await rl.question(question)).trim();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
rl.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The space of this folder and this machine's session, or a printed reason why not. */
|
|
28
|
+
export async function context(root) {
|
|
29
|
+
const config = await readSpaceConfig(root);
|
|
30
|
+
if (!config?.spaceId) {
|
|
31
|
+
console.error(config ? "O espaço desta pasta ainda não foi criado. Termine a conta no navegador." : "Esta pasta não é um espaço Omni. Rode omnispace init primeiro.");
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Barrier 1: credentials only travel over https, whatever the folder's config says.
|
|
35
|
+
const insecure = insecureApiUrl(config.apiUrl);
|
|
36
|
+
if (insecure) {
|
|
37
|
+
console.error(insecure);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const token = await loadToken(config.apiUrl);
|
|
41
|
+
if (!token) {
|
|
42
|
+
console.error("Nenhuma sessão da CLI nesta máquina. Para entrar: omnispace login");
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return { apiUrl: config.apiUrl, token, spaceId: config.spaceId };
|
|
46
|
+
}
|
|
47
|
+
export function refused(res, what) {
|
|
48
|
+
console.error(`${what}: ${res.error}`);
|
|
49
|
+
if (res.expired || res.status === 401)
|
|
50
|
+
console.error("Para entrar de novo: omnispace login");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Asks for the on-the-spot verification and waits for the browser. Returns the device code to send
|
|
54
|
+
* with the action, or null (refused, expired, failed).
|
|
55
|
+
*/
|
|
56
|
+
export async function verify(ctx, action, keys, opts) {
|
|
57
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
58
|
+
const start = await callApi(ctx.apiUrl, ctx.token, "/api/cli/verify/start", { spaceId: ctx.spaceId, action, keys }, { retryDelaysMs: RETRY_DELAYS_MS });
|
|
59
|
+
if (!start.ok) {
|
|
60
|
+
refused(start, "Falha ao pedir a verificação");
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const deviceCode = start.deviceCode;
|
|
64
|
+
const expiresAt = Date.now() + start.expiresIn * 1000;
|
|
65
|
+
const waitMs = opts.pollMs ?? start.interval * 1000;
|
|
66
|
+
log("");
|
|
67
|
+
log("Confirme no navegador, logado na sua conta:");
|
|
68
|
+
log(` ${start.verifyUrl}`);
|
|
69
|
+
log("e digite este código:");
|
|
70
|
+
log(` ${start.userCode}`);
|
|
71
|
+
log("");
|
|
72
|
+
if (opts.openBrowser)
|
|
73
|
+
openBrowser(start.verifyUrl);
|
|
74
|
+
log("→ Esperando a confirmação (Ctrl+C cancela, nada é enviado)...");
|
|
75
|
+
while (Date.now() < expiresAt) {
|
|
76
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
77
|
+
const poll = await callApi(ctx.apiUrl, ctx.token, "/api/cli/verify/poll", { deviceCode }, { retryDelaysMs: RETRY_DELAYS_MS });
|
|
78
|
+
if (poll.ok && poll.state === "pending")
|
|
79
|
+
continue;
|
|
80
|
+
if (poll.ok && poll.state === "approved")
|
|
81
|
+
return deviceCode;
|
|
82
|
+
console.error(poll.denied ? "Recusado no navegador. Nada foi enviado." : `Verificação não concluída: ${poll.error}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
console.error("O código expirou antes da confirmação. Nada foi enviado.");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
async function readVault(ctx) {
|
|
89
|
+
const res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/env/list", { spaceId: ctx.spaceId }, { retryDelaysMs: RETRY_DELAYS_MS });
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
refused(res, "Falha ao ler o cofre do espaço");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return { keys: res.keys, connectors: (res.connectors ?? []) };
|
|
95
|
+
}
|
|
96
|
+
async function push(root, opts) {
|
|
97
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
98
|
+
const ask = opts.ask ?? askTerminal;
|
|
99
|
+
const interactive = opts.interactive ?? Boolean(process.stdin.isTTY);
|
|
100
|
+
if (!interactive) {
|
|
101
|
+
console.error("omnispace env push só roda num terminal, porque cada chave é decidida por você. Nada foi enviado.");
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
const ctx = await context(root);
|
|
105
|
+
if (!ctx)
|
|
106
|
+
return 1;
|
|
107
|
+
const { vars, skipped } = await readEnvVars(root);
|
|
108
|
+
for (const file of skipped)
|
|
109
|
+
console.error(`${file} ficou de fora: maior que 256 KB.`);
|
|
110
|
+
if (vars.length === 0) {
|
|
111
|
+
log("Nenhuma variável com valor nos arquivos .env da raiz desta pasta. Nada a enviar.");
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const vault = await readVault(ctx);
|
|
115
|
+
if (!vault)
|
|
116
|
+
return 1;
|
|
117
|
+
const already = new Set(vault.keys.map((s) => `${s.file}\n${s.key}`));
|
|
118
|
+
log("");
|
|
119
|
+
log(`${vars.length} variáveis nos arquivos .env desta pasta. Para cada uma, responda:`);
|
|
120
|
+
log(" s = guardar no cofre do espaço n = pular q = parar de perguntar");
|
|
121
|
+
log("O valor vai cifrado para o cofre (Vault) e nunca aparece na tela nem em log.");
|
|
122
|
+
log("");
|
|
123
|
+
const accepted = [];
|
|
124
|
+
for (const v of vars) {
|
|
125
|
+
const note = already.has(`${v.file}\n${v.key}`) ? " [já está no cofre, será substituída]" : "";
|
|
126
|
+
const answer = (await ask(` ${v.key} (${v.file}) ${maskValue(v.value)}${note} Guardar? [s/n/q] `)).toLowerCase();
|
|
127
|
+
if (answer === "q")
|
|
128
|
+
break;
|
|
129
|
+
if (answer === "s" || answer === "sim" || answer === "y")
|
|
130
|
+
accepted.push(v);
|
|
131
|
+
}
|
|
132
|
+
if (accepted.length === 0) {
|
|
133
|
+
log("\nNada escolhido. Nada foi enviado.");
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
const keys = accepted.map(({ file, key }) => ({ file, key }));
|
|
137
|
+
const deviceCode = await verify(ctx, "env_push", keys, opts);
|
|
138
|
+
if (!deviceCode)
|
|
139
|
+
return 1;
|
|
140
|
+
const res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/env/store", {
|
|
141
|
+
spaceId: ctx.spaceId,
|
|
142
|
+
verification: deviceCode,
|
|
143
|
+
entries: accepted.map(({ file, key, value }) => ({ file, key, value })),
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
refused(res, "O cofre recusou o envio");
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
log("");
|
|
150
|
+
for (const s of res.stored) {
|
|
151
|
+
log(`✓ ${s.key} (${s.file}) ${s.replaced ? "substituída" : "guardada"} no cofre`);
|
|
152
|
+
}
|
|
153
|
+
for (const c of (res.connectors ?? [])) {
|
|
154
|
+
log(`${c.state === "linked" ? "✓" : "!"} ${c.message}`);
|
|
155
|
+
if (c.test?.ok)
|
|
156
|
+
log(" ✓ Teste de conexão passou: o conector está ligado para o Omni neste espaço.");
|
|
157
|
+
else if (c.test)
|
|
158
|
+
log(` ✗ Teste de conexão falhou: ${c.test.message ?? "sem detalhe"}. O conector fica desligado até passar.`);
|
|
159
|
+
}
|
|
160
|
+
log("");
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
async function list(root, opts) {
|
|
164
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
165
|
+
const ctx = await context(root);
|
|
166
|
+
if (!ctx)
|
|
167
|
+
return 1;
|
|
168
|
+
const vault = await readVault(ctx);
|
|
169
|
+
if (!vault)
|
|
170
|
+
return 1;
|
|
171
|
+
if (vault.keys.length === 0) {
|
|
172
|
+
log("O cofre deste espaço está vazio. Para guardar chaves do .env: omnispace env push");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
log(`${vault.keys.length} chave(s) no cofre deste espaço (só nomes, nenhum valor sai do cofre):`);
|
|
176
|
+
for (const s of vault.keys)
|
|
177
|
+
log(` ${s.key} (${s.file}) atualizada em ${new Date(s.updatedAt).toLocaleString("pt-BR")}`);
|
|
178
|
+
}
|
|
179
|
+
if (vault.connectors.length > 0) {
|
|
180
|
+
log("");
|
|
181
|
+
log("Conectores do espaço:");
|
|
182
|
+
for (const c of vault.connectors) {
|
|
183
|
+
const state = !c.hasCredential ? "sem credencial" : (STATUS_TEXT[c.status] ?? c.status);
|
|
184
|
+
log(` ${c.name}: ${state}${c.status === "error" && c.message ? ` (${c.message})` : ""}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
async function remove(root, names, file, opts) {
|
|
190
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
191
|
+
if (names.length === 0) {
|
|
192
|
+
console.error(USAGE);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
const ctx = await context(root);
|
|
196
|
+
if (!ctx)
|
|
197
|
+
return 1;
|
|
198
|
+
const vault = await readVault(ctx);
|
|
199
|
+
if (!vault)
|
|
200
|
+
return 1;
|
|
201
|
+
const targets = vault.keys.filter((s) => names.includes(s.key) && (!file || s.file === file)).map(({ file: f, key }) => ({ file: f, key }));
|
|
202
|
+
const unknown = names.filter((n) => !targets.some((t) => t.key === n));
|
|
203
|
+
for (const n of unknown)
|
|
204
|
+
console.error(`${n}${file ? ` (${file})` : ""} não está no cofre deste espaço.`);
|
|
205
|
+
if (targets.length === 0)
|
|
206
|
+
return 1;
|
|
207
|
+
log(`Vai apagar do cofre, de vez: ${targets.map((t) => `${t.key} (${t.file})`).join(", ")}`);
|
|
208
|
+
const deviceCode = await verify(ctx, "env_rm", targets, opts);
|
|
209
|
+
if (!deviceCode)
|
|
210
|
+
return 1;
|
|
211
|
+
const res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/env/remove", { spaceId: ctx.spaceId, verification: deviceCode, keys: targets });
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
refused(res, "O cofre recusou");
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
for (const r of res.removed)
|
|
217
|
+
log(`✓ ${r.key} (${r.file}) apagada do cofre`);
|
|
218
|
+
for (const c of (res.disconnected ?? []))
|
|
219
|
+
log(`! O conector ${c} usava essa credencial: a cópia dele foi apagada e ele ficou desligado.`);
|
|
220
|
+
return unknown.length > 0 ? 1 : 0;
|
|
221
|
+
}
|
|
222
|
+
/** `omnispace env <push|list|rm> ...`, the one entry in the dispatcher (index.ts). */
|
|
223
|
+
export async function runEnv(args, opts = {}) {
|
|
224
|
+
const [sub, ...rest] = args;
|
|
225
|
+
const valueOf = (flag) => {
|
|
226
|
+
const eq = rest.find((a) => a.startsWith(`${flag}=`));
|
|
227
|
+
if (eq)
|
|
228
|
+
return eq.slice(flag.length + 1);
|
|
229
|
+
const i = rest.indexOf(flag);
|
|
230
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
231
|
+
};
|
|
232
|
+
// The word after `--arquivo` or `--pasta` is that flag's value, not a key name.
|
|
233
|
+
const flagValues = new Set(["--arquivo", "--pasta"].map((f) => rest.indexOf(f)).filter((i) => i >= 0).map((i) => rest[i + 1]));
|
|
234
|
+
const positional = rest.filter((a) => !a.startsWith("-") && !flagValues.has(a));
|
|
235
|
+
if (sub === "push")
|
|
236
|
+
return push(resolve(positional[0] ?? process.cwd()), opts);
|
|
237
|
+
if (sub === "list")
|
|
238
|
+
return list(resolve(positional[0] ?? process.cwd()), opts);
|
|
239
|
+
if (sub === "rm")
|
|
240
|
+
return remove(resolve(valueOf("--pasta") ?? process.cwd()), positional, valueOf("--arquivo"), opts);
|
|
241
|
+
console.error(USAGE);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
package/dist/include.js
CHANGED
|
@@ -9,6 +9,15 @@ export const SKIP_DIRS = new Set([
|
|
|
9
9
|
"node_modules", ".git", ".next", ".vercel", "dist", "build", "out", "coverage",
|
|
10
10
|
".turbo", ".cache", "venv", ".venv", "__pycache__", ".idea", ".vscode", "target",
|
|
11
11
|
]);
|
|
12
|
+
/**
|
|
13
|
+
* Folders skipped by where they are, not by their name alone. `.claude/worktrees/` holds full copies
|
|
14
|
+
* of a repo that Claude Code sessions work on in parallel (Lucas, 2026-09-28): synced, every copy
|
|
15
|
+
* would go up as text. Matches at any depth (a repo inside the space can have its own).
|
|
16
|
+
*/
|
|
17
|
+
const SKIP_DIR_PATHS_RE = /(^|\/)\.claude\/worktrees$/i;
|
|
18
|
+
export function isSkippedDirPath(path) {
|
|
19
|
+
return SKIP_DIR_PATHS_RE.test(path);
|
|
20
|
+
}
|
|
12
21
|
// Local state of the Omni link itself: machine specific, never part of the space. `.omni/tmp/`
|
|
13
22
|
// holds downloads of an unfinished pull (fatia 3); `.omni/conflicts.json` and `.omni/backups/`
|
|
14
23
|
// are the conflict resolution of this machine (fatia 4); `.omni/pull-pending.json` and
|
|
@@ -74,7 +83,8 @@ export function isMachineConfig(path) {
|
|
|
74
83
|
*/
|
|
75
84
|
export function neverSyncs(path) {
|
|
76
85
|
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
77
|
-
|
|
86
|
+
const inSkippedDir = /(^|\/)\.claude\/worktrees\//i.test(path);
|
|
87
|
+
return isSecret(name) || isMachineConfig(path) || isLocalOnly(path) || inSkippedDir;
|
|
78
88
|
}
|
|
79
89
|
export function isText(name) {
|
|
80
90
|
const lower = name.toLowerCase();
|
|
@@ -143,7 +153,7 @@ export async function buildManifest(root) {
|
|
|
143
153
|
const abs = join(dir, entry.name);
|
|
144
154
|
const relParts = [...parts, entry.name];
|
|
145
155
|
if (entry.isDirectory()) {
|
|
146
|
-
if (SKIP_DIRS.has(entry.name))
|
|
156
|
+
if (SKIP_DIRS.has(entry.name) || isSkippedDirPath(normalizePath(relParts)))
|
|
147
157
|
continue;
|
|
148
158
|
// A subfolder with its own .git (repo, submodule or worktree) DOES sync, text only: the
|
|
149
159
|
// agent needs the code to act on it (find a bug, fix it, report back). Lucas, 2026-09-23,
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,9 @@ import { initWithAccount } from "./init-account.js";
|
|
|
16
16
|
import { statusSpace } from "./status.js";
|
|
17
17
|
import { terminalAsk, unlinkMachine } from "./desvincular.js";
|
|
18
18
|
import { linkExistingSpace } from "./vincular.js";
|
|
19
|
+
import { askSpace, parseAskArgs } from "./ask.js";
|
|
20
|
+
import { runEnv } from "./env.js";
|
|
21
|
+
import { runPermissoes } from "./permissoes.js";
|
|
19
22
|
const USAGE = `
|
|
20
23
|
Omni CLI
|
|
21
24
|
|
|
@@ -27,12 +30,17 @@ Uso:
|
|
|
27
30
|
omni init [pasta] --scan-only varre a pasta e mostra o que encontrou
|
|
28
31
|
omni init [pasta] --link <código> varre a pasta e envia pro código mostrado no navegador
|
|
29
32
|
omni status [pasta] [--resumo] o que mudou aqui e no servidor, e o próximo comando (só lê)
|
|
33
|
+
omni ask "<pergunta>" [pasta] responde com base na versão publicada do espaço, com as fontes
|
|
34
|
+
[--fontes] só mostra quais arquivos seriam lidos, sem chamar o modelo
|
|
35
|
+
[--detalhes] mostra também custo, contexto e arquivos consultados
|
|
30
36
|
omni sync [pasta] --push [--yes] publica uma nova versão do espaço no servidor
|
|
31
37
|
omni sync [pasta] --pull traz para a pasta a versão mais nova do servidor
|
|
32
38
|
[--conflito=local|mesclar|backup] mesma resposta para todos os conflitos, sem perguntar
|
|
33
39
|
omni clone <código> <pasta> baixa um espaço da sua conta numa pasta nova desta máquina
|
|
34
40
|
omni vincular [pasta] [--espaco <nome>] liga uma pasta que já tem os arquivos a um espaço da conta
|
|
35
41
|
omni desvincular [pasta] tira esta máquina da conta e o .omni/ das pastas (pede 2 confirmações)
|
|
42
|
+
omni env push|list|rm [pasta] guarda chaves do .env no cofre do espaço, uma a uma (confirma no navegador)
|
|
43
|
+
omni permissoes [set <conector> <ação> <estado>] [pasta] o que o agente pode fazer sozinho em cada ação
|
|
36
44
|
|
|
37
45
|
A CLI não reorganiza a sua pasta. Com --link, o que fazer com o que foi
|
|
38
46
|
varrido é decidido na tela de revisão, no navegador.
|
|
@@ -59,6 +67,10 @@ async function main(argv) {
|
|
|
59
67
|
return runSync(args.slice(1));
|
|
60
68
|
if (command === "clone")
|
|
61
69
|
return runClone(args.slice(1));
|
|
70
|
+
if (command === "env")
|
|
71
|
+
return runEnv(args.slice(1), { openBrowser: process.stdout.isTTY });
|
|
72
|
+
if (command === "permissoes")
|
|
73
|
+
return runPermissoes(args.slice(1), { openBrowser: process.stdout.isTTY });
|
|
62
74
|
if (command === "vincular") {
|
|
63
75
|
const rest = args.slice(1);
|
|
64
76
|
const espacoIndex = rest.indexOf("--espaco");
|
|
@@ -72,6 +84,14 @@ async function main(argv) {
|
|
|
72
84
|
const rest = args.slice(1);
|
|
73
85
|
return statusSpace(resolve(rest.find((a) => !a.startsWith("-")) ?? process.cwd()), { resumo: rest.includes("--resumo") });
|
|
74
86
|
}
|
|
87
|
+
if (command === "ask") {
|
|
88
|
+
const ask = parseAskArgs(args.slice(1));
|
|
89
|
+
if ("error" in ask) {
|
|
90
|
+
console.error(ask.error);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
return askSpace(ask);
|
|
94
|
+
}
|
|
75
95
|
if (command !== "init") {
|
|
76
96
|
console.error(`Comando desconhecido: ${command}`);
|
|
77
97
|
console.log(USAGE.trim());
|
package/dist/login.js
CHANGED
|
@@ -10,7 +10,7 @@ import { RETRY_DELAYS_MS } from "./retry.js";
|
|
|
10
10
|
* Opens the page in the default browser. Best effort: the URL is always printed too. Only http(s)
|
|
11
11
|
* URLs, and never through a shell (on Windows `cmd /c start` would parse `&` and `|` in the URL).
|
|
12
12
|
*/
|
|
13
|
-
function openBrowser(url) {
|
|
13
|
+
export function openBrowser(url) {
|
|
14
14
|
if (!/^https?:\/\//.test(url))
|
|
15
15
|
return;
|
|
16
16
|
const os = platform();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { callApi } from "./api.js";
|
|
3
|
+
import { RETRY_DELAYS_MS } from "./retry.js";
|
|
4
|
+
import { context, refused, verify } from "./env.js";
|
|
5
|
+
// `omnispace permissoes` (docs/plan/permissoes-por-acao.md; Lucas, 2026-09-28): what the agent may
|
|
6
|
+
// do on its own with each connector action of the space. Tightening applies right away; loosening
|
|
7
|
+
// asks for the confirmation in the browser, like a credential.
|
|
8
|
+
const STATES = ["automatico", "confirmar", "bloqueado"];
|
|
9
|
+
const STATE_LABEL = { automatico: "automático", confirmar: "pedir confirmação", bloqueado: "bloqueado" };
|
|
10
|
+
const ORIGIN_LABEL = { padrao: "padrão do Omni", usuario: "definida por você", importacao: "importada" };
|
|
11
|
+
const USAGE = `Uso:
|
|
12
|
+
omnispace permissoes [pasta] lista as ações dos conectores do espaço e o que o agente pode fazer
|
|
13
|
+
omnispace permissoes set <conector> <ação> <estado> [pasta] muda uma ação: automatico, confirmar ou bloqueado
|
|
14
|
+
Liberar (bloqueado para confirmar, ou para automático) pede confirmação no navegador; restringir vale na hora.`;
|
|
15
|
+
export function parseStateArg(value) {
|
|
16
|
+
const plain = (value ?? "").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
17
|
+
return STATES.find((s) => s === plain) ?? null;
|
|
18
|
+
}
|
|
19
|
+
async function list(root, opts) {
|
|
20
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
21
|
+
const ctx = await context(root);
|
|
22
|
+
if (!ctx)
|
|
23
|
+
return 1;
|
|
24
|
+
const res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/permissions/list", { spaceId: ctx.spaceId }, { retryDelaysMs: RETRY_DELAYS_MS });
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
refused(res, "Falha ao ler as permissões");
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const rows = res.permissions;
|
|
30
|
+
if (rows.length === 0) {
|
|
31
|
+
log("Este espaço ainda não tem conector. As permissões aparecem aqui quando houver um (omnispace env push).");
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
let current = "";
|
|
35
|
+
for (const r of rows) {
|
|
36
|
+
if (r.connectorId !== current) {
|
|
37
|
+
current = r.connectorId;
|
|
38
|
+
log(`\n${r.connectorName} (${r.connectorId})`);
|
|
39
|
+
}
|
|
40
|
+
log(` ${r.actionKey.padEnd(22)} ${r.actionName.padEnd(26)} ${r.kind.padEnd(10)} ${STATE_LABEL[r.state].padEnd(18)} ${ORIGIN_LABEL[r.origin] ?? r.origin}`);
|
|
41
|
+
}
|
|
42
|
+
log("\nPara mudar: omnispace permissoes set <conector> <ação> <automatico|confirmar|bloqueado>");
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
async function set(root, connectorId, actionKey, state, opts) {
|
|
46
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
47
|
+
const ctx = await context(root);
|
|
48
|
+
if (!ctx)
|
|
49
|
+
return 1;
|
|
50
|
+
const changes = [{ connectorId, actionKey, state }];
|
|
51
|
+
// Not retried: a repeat after a cut connection is harmless (same state), but it is reported.
|
|
52
|
+
let res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/permissions/set", { spaceId: ctx.spaceId, changes });
|
|
53
|
+
if (!res.ok && res.needsVerification) {
|
|
54
|
+
log(`Liberar ${connectorId}:${actionKey} (${STATE_LABEL[state]}) deixa o agente fazer mais sozinho.`);
|
|
55
|
+
const interactive = opts.interactive ?? Boolean(process.stdin.isTTY);
|
|
56
|
+
if (!interactive) {
|
|
57
|
+
console.error("Sem terminal para acompanhar a confirmação no navegador. Nada mudou.");
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const code = await verify(ctx, "perm_set", [{ file: `${connectorId}:${actionKey}`, key: state }], opts);
|
|
61
|
+
if (!code)
|
|
62
|
+
return 1;
|
|
63
|
+
res = await callApi(ctx.apiUrl, ctx.token, "/api/cli/permissions/set", { spaceId: ctx.spaceId, changes, verification: code });
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
refused(res, "A permissão não mudou");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const applied = res.applied;
|
|
70
|
+
if (applied.length === 0)
|
|
71
|
+
log(`${connectorId}:${actionKey} já estava como ${STATE_LABEL[state]}. Nada mudou.`);
|
|
72
|
+
else
|
|
73
|
+
log(`✓ ${connectorId}:${actionKey}: ${STATE_LABEL[applied[0].from]} → ${STATE_LABEL[applied[0].to]}.`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
export async function runPermissoes(args, opts = {}) {
|
|
77
|
+
const [first, ...rest] = args;
|
|
78
|
+
if (first === "set") {
|
|
79
|
+
const [connectorId, actionKey, stateArg, folder] = rest;
|
|
80
|
+
const state = parseStateArg(stateArg);
|
|
81
|
+
if (!connectorId || !actionKey || !state) {
|
|
82
|
+
console.error(USAGE);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
return set(resolve(folder ?? process.cwd()), connectorId, actionKey, state, opts);
|
|
86
|
+
}
|
|
87
|
+
if (first === "--help" || first === "-h" || first === "help") {
|
|
88
|
+
console.log(USAGE);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
return list(resolve(first && first !== "list" ? first : (rest[0] ?? process.cwd())), opts);
|
|
92
|
+
}
|