omnispace 0.16.0 → 0.17.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.
@@ -0,0 +1,99 @@
1
+ import { callApi } from "./api.js";
2
+ import { RETRY_DELAYS_MS } from "./retry.js";
3
+ import { buildManifest } from "./include.js";
4
+ import { unresolvedConflicts } from "./conflicts.js";
5
+ import { readJournal } from "./journal.js";
6
+ import { pullSpace, remoteMap } from "./pull.js";
7
+ import { pushSpace } from "./sync.js";
8
+ import { LockedError, withLock } from "./lock.js";
9
+ import { loadToken, readSpaceConfig } from "./config.js";
10
+ import { statusSpace, summarizeStatus } from "./status.js";
11
+ /** What to do given the three-way summary. A file changed on both sides stops everything. */
12
+ export function planAuto(summary, head, baseVersion, pullOnly) {
13
+ if (summary.both.length > 0)
14
+ return { kind: "diverged", files: summary.both };
15
+ const pull = head > baseVersion;
16
+ const push = !pullOnly && summary.local.length > 0;
17
+ return pull || push ? { kind: "sync", pull, push } : { kind: "nothing" };
18
+ }
19
+ /** Runs `fn` with console output captured: pull and push talk a lot, a hook must say one line. */
20
+ async function captured(fn) {
21
+ const lines = [];
22
+ const saved = { log: console.log, error: console.error };
23
+ const grab = (...args) => void lines.push(args.join(" "));
24
+ console.log = grab;
25
+ console.error = grab;
26
+ try {
27
+ const code = await fn();
28
+ const last = [...lines].reverse().map((l) => l.trim()).find((l) => l.length > 0) ?? "";
29
+ return { code, last };
30
+ }
31
+ finally {
32
+ console.log = saved.log;
33
+ console.error = saved.error;
34
+ }
35
+ }
36
+ async function autoOnce(root, opts, deps) {
37
+ const config = await readSpaceConfig(root);
38
+ if (!config?.spaceId)
39
+ return null;
40
+ const token = await loadToken(config.apiUrl);
41
+ if (!token)
42
+ return "CLI sem sessão nesta máquina; rode omnispace login (sync automático parado).";
43
+ const [journal, markers] = await Promise.all([readJournal(root), unresolvedConflicts(root, { readOnly: true })]);
44
+ if (journal)
45
+ return "um pull parou no meio; rode omnispace sync --pull.";
46
+ if (markers.length > 0)
47
+ return `${markers.length} arquivo(s) com marcador de conflito; resolva antes do sync automático.`;
48
+ const manifest = await buildManifest(root);
49
+ if (manifest.truncated)
50
+ return "pasta grande demais para o sync automático.";
51
+ const res = await callApi(config.apiUrl, token, "/api/cli/sync/status", { spaceId: config.spaceId, baseVersion: config.baseVersion }, { retryDelaysMs: RETRY_DELAYS_MS });
52
+ if (!res.ok)
53
+ return `servidor não consultado (${res.error}); sync automático adiado.`;
54
+ const head = res.head;
55
+ const headMap = remoteMap(res.files);
56
+ const baseMap = res.base ? remoteMap(res.base) : headMap;
57
+ const local = new Map(manifest.files.map((f) => [f.path, f.hash]));
58
+ const skip = (path) => manifest.unreadable.some((u) => path === u || path.startsWith(`${u}/`));
59
+ const readable = (map) => new Map([...map].filter(([path]) => !skip(path)));
60
+ const summary = summarizeStatus(local, readable(baseMap), readable(headMap));
61
+ const plan = planAuto(summary, head, config.baseVersion, Boolean(opts.pullOnly));
62
+ if (plan.kind === "nothing")
63
+ return null;
64
+ if (plan.kind === "diverged") {
65
+ return `${plan.files.length} arquivo(s) mudado(s) nesta pasta e no servidor (${plan.files.slice(0, 3).join(", ")}); sync automático parado, rode omnispace sync --pull para decidir.`;
66
+ }
67
+ const parts = [];
68
+ if (plan.pull) {
69
+ // conflito: "backup" only matters if a race creates a conflict after the check: it keeps both.
70
+ const r = await captured(() => deps.pull(root, { conflito: "backup" }));
71
+ if (r.code !== 0)
72
+ return `pull automático não terminou: ${r.last || "erro sem mensagem"}`;
73
+ if (summary.remote > 0)
74
+ parts.push(`trouxe ${summary.remote} arquivo(s) do servidor`);
75
+ }
76
+ if (plan.push) {
77
+ const r = await captured(() => deps.push(root, { yes: true }));
78
+ if (r.code !== 0)
79
+ return `${parts.length ? `${parts.join(", ")}; ` : ""}envio automático não feito: ${r.last || "erro sem mensagem"}`;
80
+ const after = await readSpaceConfig(root);
81
+ parts.push(`enviei ${summary.local.length} arquivo(s)${after ? ` (versão ${after.baseVersion})` : ""}`);
82
+ }
83
+ return parts.length > 0 ? parts.join(", ") : null;
84
+ }
85
+ export async function syncAuto(root, opts = {}, deps = { pull: pullSpace, push: pushSpace }) {
86
+ try {
87
+ const line = await withLock(root, () => autoOnce(root, opts, deps));
88
+ if (line)
89
+ console.log(`Omni: sync automático: ${line}`);
90
+ }
91
+ catch (err) {
92
+ // Another sync (another agent in the same folder) holds the lock: it does the work.
93
+ if (!(err instanceof LockedError))
94
+ console.log(`Omni: sync automático falhou (${err.message.slice(0, 160)}).`);
95
+ }
96
+ if (opts.resumo)
97
+ await statusSpace(root, { resumo: true });
98
+ return 0;
99
+ }
package/dist/hooks.js ADDED
@@ -0,0 +1,90 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ // Hooks do Claude Code que mantêm a pasta em dia sem a pessoa pedir (checklist 2 de
4
+ // docs/plan/visao-jarvis-automacao-2026-09-28.md). Vão em `.claude/settings.json`, que sincroniza
5
+ // entre máquinas: os comandos são iguais em qualquer sistema, ao contrário dos arquivos de MCP.
6
+ // SessionStart: `omnispace sync --auto --pull-only --resumo` (traz o que chegou e diz o estado;
7
+ // troca o `omnispace status --resumo` antigo, que rodaria em paralelo e leria o estado velho)
8
+ // SessionEnd: `omnispace sync --auto` (traz e envia o que mudou, sem conflito)
9
+ export const START_COMMAND = "omnispace sync --auto --pull-only --resumo";
10
+ export const END_COMMAND = "omnispace sync --auto";
11
+ const OLD_START = /^\s*(omnispace|omni)\s+status\s+--resumo\s*$/;
12
+ function hasCommand(groups, test) {
13
+ return groups.some((g) => (g.hooks ?? []).some((h) => typeof h.command === "string" && test(h.command)));
14
+ }
15
+ /** Pure merge: returns the new settings and whether anything changed. Never removes another hook. */
16
+ export function mergeHooks(settings) {
17
+ const next = { ...settings, hooks: { ...(settings.hooks ?? {}) } };
18
+ const hooks = next.hooks;
19
+ let changed = false;
20
+ const start = (hooks.SessionStart ?? []).map((g) => ({ ...g, hooks: (g.hooks ?? []).map((h) => ({ ...h })) }));
21
+ if (!hasCommand(start, (c) => c.includes("sync --auto"))) {
22
+ let replaced = false;
23
+ for (const group of start) {
24
+ for (const h of group.hooks ?? []) {
25
+ if (typeof h.command === "string" && OLD_START.test(h.command)) {
26
+ h.command = START_COMMAND;
27
+ replaced = true;
28
+ }
29
+ }
30
+ }
31
+ if (!replaced)
32
+ start.push({ matcher: "startup|resume", hooks: [{ type: "command", command: START_COMMAND, timeout: 90 }] });
33
+ hooks.SessionStart = start;
34
+ changed = true;
35
+ }
36
+ const end = (hooks.SessionEnd ?? []).map((g) => ({ ...g, hooks: (g.hooks ?? []).map((h) => ({ ...h })) }));
37
+ if (!hasCommand(end, (c) => c.includes("sync --auto"))) {
38
+ end.push({ hooks: [{ type: "command", command: END_COMMAND, timeout: 90 }] });
39
+ hooks.SessionEnd = end;
40
+ changed = true;
41
+ }
42
+ return { next, changed };
43
+ }
44
+ /** `.claude/settings.json` of the folder. Unreadable (comments, broken JSON): untouched, reported. */
45
+ export async function installHooks(folder) {
46
+ const path = join(folder, ".claude", "settings.json");
47
+ let settings = {};
48
+ let raw;
49
+ try {
50
+ raw = await readFile(path, "utf8");
51
+ }
52
+ catch {
53
+ raw = undefined;
54
+ }
55
+ if (raw !== undefined) {
56
+ try {
57
+ settings = JSON.parse(raw);
58
+ }
59
+ catch {
60
+ return { path, changed: false, skipped: true };
61
+ }
62
+ }
63
+ const { next, changed } = mergeHooks(settings);
64
+ if (!changed)
65
+ return { path, changed: false, skipped: false };
66
+ await mkdir(dirname(path), { recursive: true });
67
+ const tmp = `${path}.${process.pid}.tmp`;
68
+ await writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, "utf8");
69
+ await rename(tmp, path);
70
+ return { path, changed: true, skipped: false };
71
+ }
72
+ // Depois de sync, clone, vincular e init (junto do MCP): liga os hooks sozinho na pasta de espaço.
73
+ // Só fala quando mudou algo; `OMNI_NO_HOOKS=1` desliga.
74
+ export async function autoWireHooks(folder) {
75
+ if (process.env.OMNI_NO_HOOKS)
76
+ return;
77
+ try {
78
+ if (!(await readFile(join(folder, ".omni", "space.json"), "utf8").then(() => true, () => false)))
79
+ return;
80
+ const r = await installHooks(folder);
81
+ if (r.changed) {
82
+ console.log(`\nSync automático ligado nos hooks do Claude Code (${r.path}): traz o que chegou ao abrir a sessão e envia o que mudou ao fechar. Conflito nunca é forçado.`);
83
+ }
84
+ if (r.skipped)
85
+ console.log(`\nNão mexi em ${r.path} (comentários ou JSON quebrado). Para ligar o sync automático, adicione os hooks à mão: rode omnispace hooks instalar depois de corrigir o arquivo.`);
86
+ }
87
+ catch {
88
+ // ligar os hooks nunca derruba o comando que a pessoa rodou
89
+ }
90
+ }
package/dist/index.js CHANGED
@@ -22,6 +22,8 @@ import { runPermissoes } from "./permissoes.js";
22
22
  import { runUso } from "./uso.js";
23
23
  import { runLlm } from "./llm.js";
24
24
  import { confirmarSpace } from "./confirmar.js";
25
+ import { syncAuto } from "./auto-sync.js";
26
+ import { autoWireHooks, installHooks } from "./hooks.js";
25
27
  import { autoWireMcp, detectEditors, installMcpConfig, mcpNextSteps, mcpSkippedNote, runMcpServer } from "./mcp.js";
26
28
  const USAGE = `
27
29
  Omni CLI
@@ -49,6 +51,8 @@ Uso:
49
51
  omni llm [set anthropic|rm] usar a sua chave da Anthropic no lugar do modelo do Omni, com teto diário
50
52
  omni mcp servidor MCP local (stdio) pro Claude Code e pro Cursor lerem este espaço
51
53
  omni mcp instalar [pasta] acrescenta o servidor omni ao MCP do Claude Code, Cursor (e VS Code, Trae se a pasta usa) (mescla, nunca apaga outro)
54
+ omni sync [pasta] --auto [--pull-only] [--resumo] o que os hooks rodam: traz e envia sem perguntar, para se houver conflito
55
+ omni hooks instalar [pasta] liga o sync automático nos hooks do Claude Code da pasta (mescla, nunca apaga outro)
52
56
  omni confirmar [pedido] [pasta] decide s/n um pedido pendente de escrita, visto por inteiro no terminal
53
57
 
54
58
  A CLI não reorganiza a sua pasta. Com --link, o que fazer com o que foi
@@ -74,6 +78,12 @@ async function main(argv) {
74
78
  return printWhoami();
75
79
  if (command === "sync") {
76
80
  const rest = args.slice(1);
81
+ if (rest.includes("--auto")) {
82
+ return syncAuto(resolve(rest.find((a) => !a.startsWith("-")) ?? process.cwd()), {
83
+ pullOnly: rest.includes("--pull-only"),
84
+ resumo: rest.includes("--resumo"),
85
+ });
86
+ }
77
87
  return withMcp(await runSync(rest), resolve(rest.find((a) => !a.startsWith("-")) ?? process.cwd()));
78
88
  }
79
89
  if (command === "clone") {
@@ -103,6 +113,23 @@ async function main(argv) {
103
113
  }
104
114
  return runMcpServer(process.cwd());
105
115
  }
116
+ if (command === "hooks") {
117
+ const rest = args.slice(1);
118
+ if (rest[0] !== "instalar") {
119
+ console.error("Use omnispace hooks instalar [pasta].");
120
+ return 1;
121
+ }
122
+ const folder = resolve(rest.slice(1).find((a) => !a.startsWith("-")) ?? process.cwd());
123
+ const r = await installHooks(folder);
124
+ console.log(r.skipped ? `
125
+ Não mexi em ${r.path}: comentários ou JSON quebrado. Corrija e rode de novo.
126
+ ` : r.changed ? `
127
+ Hooks de sync automático ligados em ${r.path}.
128
+ ` : `
129
+ Os hooks já estavam em ${r.path}.
130
+ `);
131
+ return r.skipped ? 1 : 0;
132
+ }
106
133
  if (command === "confirmar") {
107
134
  const rest = args.slice(1);
108
135
  const pedido = rest.find((a) => !a.startsWith("-"));
@@ -211,8 +238,10 @@ Só quer ver o que o Omni leria nesta pasta, sem enviar nada?
211
238
  return 0;
212
239
  }
213
240
  async function withMcp(code, folder) {
214
- if (code === 0)
241
+ if (code === 0) {
215
242
  await autoWireMcp(folder);
243
+ await autoWireHooks(folder);
244
+ }
216
245
  return code;
217
246
  }
218
247
  async function runSync(rest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnispace",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
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": {