@h1veframework/cli 0.5.1 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +48 -62
  2. package/dist/index.js +78 -22
  3. package/package.json +15 -5
package/README.md CHANGED
@@ -1,93 +1,79 @@
1
- # @h1veframework/cli
1
+ # @h1veframework/cli · `nf`
2
2
 
3
- CLI `nf` do **H1VE Flow** opera o fluxo da feature da sua branch direto do terminal
4
- (status, start, move, spec, done, blocker, health, connect), autenticado por um token pessoal.
3
+ **The command-line companion for [H1VE](https://h1ve.org)the governance & memory layer for teams building with AI.**
5
4
 
6
- Parte do tooling do H1VE junto com o **[servidor MCP](https://www.npmjs.com/package/@h1veframework/mcp)**
7
- (para o Claude Code).
5
+ Your team ships with AI (Claude Code, Cursor, Copilot). As the codebase grows, the AI and your teammates start
6
+ **losing context and drifting from decisions you already made**. H1VE keeps every feature aligned — a shared,
7
+ **eternal project memory** plus an **anti-drift engine** that checks each change against what's been decided.
8
+ `nf` drives that workflow straight from your terminal.
8
9
 
9
- ## Pré-requisitos
10
+ Part of the H1VE toolchain, alongside the **[MCP server](https://www.npmjs.com/package/@h1veframework/mcp)** for
11
+ Claude Code / Cursor.
10
12
 
11
- - **Node.js 18.18+** (`node --version`).
12
- - Uma conta no H1VE Flow. **Não precisa criar/colar token** — o `nf login` cuida disso.
13
+ **[h1ve.org](https://h1ve.org)** · **[app.h1ve.org](https://app.h1ve.org)**
13
14
 
14
- ## Instalação
15
+ ## Quickstart (zero paste)
15
16
 
16
17
  ```bash
17
18
  npm i -g @h1veframework/cli
18
- nf --version
19
+ nf login # opens your browser → you click Authorize → done
20
+ nf status # state of the current branch's feature
19
21
  ```
20
22
 
21
- ## Login (recomendadosem colar nada)
23
+ `nf login` stores the credential locally (`~/.config/h1ve/credentials.json`, `600`) the MCP server reads the
24
+ same one, so **one login sets up both**. The project **auto-resolves from your git remote**, so there's nothing
25
+ to paste and no `H1VE_PROJECT_ID` to set. `nf logout` removes it.
22
26
 
23
- ```bash
24
- nf login # abre o navegador → você autoriza → pronto
25
- nf health # já funciona
26
- ```
27
+ > `nf health` saying "no snapshots yet" is **success** (you're connected; the project just has no metrics yet).
27
28
 
28
- O `nf login` abre o navegador (onde você já está logado no H1VE), você clica **Autorizar**, e o CLI **guarda
29
- a credencial sozinho** em `~/.config/h1ve/credentials.json` (permissão 600). O **[MCP](https://www.npmjs.com/package/@h1veframework/mcp)**
30
- lê a mesma credencial — um `nf login` configura os dois. `nf logout` remove. **100% automático** (SPEC-078):
31
- o projeto também é resolvido sozinho pelo repo (SPEC-077), então nada de `H1VE_PROJECT_ID`.
29
+ ## Commands
32
30
 
33
- > "Nenhum snapshot registrado" no `nf health` é **sucesso** (conectou; projeto sem métricas ainda).
31
+ | Command | What it does |
32
+ |---|---|
33
+ | `nf login` / `nf logout` | Authorize in the browser and store the credential locally / remove it |
34
+ | `nf status` | State of the current branch's feature (stage, days active, blockers, sign-offs) |
35
+ | `nf start [<#\|id>] [--slug <s>]` | Start an assigned feature: create the `feat/{you}/{slug}` branch and record the slug |
36
+ | `nf spec` | Print the feature's spec (markdown) |
37
+ | `nf move <stage> [--note]` | Move the feature to another stage |
38
+ | `nf done [--from <file>] [--no-move]` | Submit the AI declaration (JSON) and move `dev → pr` |
39
+ | `nf blocker "<desc>"` | Open a blocker on the feature (you become its owner) |
40
+ | `nf health` | Latest technical-health snapshots for the project |
41
+ | `nf connect --kind <k> --label <l> --env KEY=VAL` | Write a credential to your **local** `.env.local` and register the inventory (never sent to the server) |
42
+ | `nf serve [--port 7391]` | Run the local agent (`127.0.0.1`) so the dashboard can apply credentials from the browser |
34
43
 
35
- ## Alternativa: variáveis de ambiente (CI / headless)
44
+ Flags: `--json` (raw output for scripts) · `--project <name\|id>` (if you belong to more than one project) · `-h`.
36
45
 
37
- Sem navegador (ex.: CI), configure por env — tem **precedência** sobre o `nf login`:
46
+ ## Example
38
47
 
39
48
  ```bash
40
- export H1VE_API_URL="https://app.h1ve.org" # opcional (default é este)
41
- export H1VE_API_KEY="nf_pat_..." # PAT criado em app.h1ve.org/api-tokens
42
- # export H1VE_PROJECT_ID="..." # OPCIONAL o CLI resolve o projeto pelo repo; use só p/ forçar
49
+ nf start # start your assigned feature (creates the branch)
50
+ # ... work: git, code, commits ...
51
+ nf status # check state anytime
52
+ nf blocker "waiting on the Neon credential" # blocked? open a blocker
53
+ nf done --from ai-declaration.json # submit the AI declaration + move dev → pr
43
54
  ```
44
55
 
45
- > **`H1VE_PROJECT_ID`** (SPEC-076/077): **opcional**. Rodando dentro de um repo conectado a um projeto, o
46
- > CLI **resolve o projeto sozinho** pelo `git remote` — zero config, mesmo com 2+ projetos. Use só pra
47
- > **forçar** um projeto (override).
56
+ ## CI / headless (env vars)
48
57
 
49
- ## Comandos
50
-
51
- | Comando | O que faz |
52
- |---|---|
53
- | `nf login` | Autoriza no navegador e salva a credencial local (sem colar nada) |
54
- | `nf logout` | Remove a credencial local |
55
- | `nf health` | Últimos snapshots de saúde técnica do projeto |
56
- | `nf status` | Estado da feature da branch atual (stage, dias ativo, blockers, sign-offs) |
57
- | `nf start [<nº\|id>] [--slug <s>]` | Inicia uma feature atribuída: cria a branch `feat/{você}/{slug}` e grava o slug |
58
- | `nf spec` | Imprime a spec (markdown) da feature da branch |
59
- | `nf move <stage> [--note]` | Move a feature para outro stage |
60
- | `nf done [--from <arq>] [--no-move]` | Envia a AI declaration (JSON) e move `dev → pr` |
61
- | `nf blocker "<desc>"` | Abre um blocker na feature (você vira o dono) |
62
- | `nf connect --kind <k> --label <l> --env KEY=VAL` | Aplica uma credencial no `.env.local` **local** (nunca ao servidor) e registra o inventário (Jeito B) |
63
- | `nf serve [--port 7391]` | Sobe o agente local (`127.0.0.1`) p/ o menu visual do painel aplicar credenciais pelo navegador |
64
-
65
- Flags: `--json` (saída crua p/ scripts) · `--project <nome\|id>` (se você é membro de +1 projeto) · `-h` (ajuda completa).
66
-
67
- ## Exemplo de uso
58
+ No browser? Configure by env (takes **precedence** over `nf login`):
68
59
 
69
60
  ```bash
70
- nf start # inicia a feature atribuída (cria a branch)
71
- # ... trabalha (git, código, commits) ...
72
- nf status # estado a qualquer momento
73
- nf blocker "aguardando credencial do Neon" # trava? abre blocker
74
- nf done --from ai-declaration.json # envia a AI declaration + move dev → pr
61
+ export H1VE_API_URL="https://app.h1ve.org" # optional (this is the default)
62
+ export H1VE_API_KEY="nf_pat_..." # a PAT from app.h1ve.org/api-tokens
63
+ # export H1VE_PROJECT_ID="..." # OPTIONAL the CLI resolves the project from the repo
75
64
  ```
76
65
 
77
66
  ## Troubleshooting
78
67
 
79
- | Sintoma | Solução |
68
+ | Symptom | Fix |
80
69
  |---|---|
81
- | `nf: command not found` | Node ausente ou terminal errado. Confira `node --version`. No Windows, o PowerShell pode não ter o bin do npm no PATH — use o terminal do VS Code ou reabra o shell. |
82
- | `404` ao instalar | Propagação do npm logo após publicação. Espere alguns minutos. |
83
- | `NO_PROJECT` | Você é membro de +1 projetopasse `--project <nome\|id>`. |
84
- | `401` / `403` | Token errado/ausente/sem permissão. Confira que `H1VE_API_KEY` é um PAT (`nf_pat_…`). `403 SERVICE_CANNOT_WRITE` = chave de serviço (só leitura) numa escrita → use um PAT. |
85
-
86
- ## Compatibilidade de nomes
70
+ | `nf: command not found` | Node missing or wrong terminal. Check `node --version`. On Windows, reopen the shell or use the VS Code terminal. |
71
+ | `404` on install | npm propagation right after a publish wait a few minutes. |
72
+ | `NO_PROJECT` | You belong to more than one project pass `--project <name\|id>`. |
73
+ | `401` / `403` | Wrong/missing token. Make sure `H1VE_API_KEY` is a PAT (`nf_pat_…`). `403 SERVICE_CANNOT_WRITE` = a read-only service key used on a write → use a PAT. |
87
74
 
88
- Use **`H1VE_API_URL`** e **`H1VE_API_KEY`**. Os nomes legados `NEXUS_FLOW_API_URL` / `NEXUS_FLOW_API_KEY`
89
- **ainda são aceitos** por compatibilidade — se você já configurou com eles, não precisa mudar nada.
75
+ Requires **Node.js 18.18+**. Legacy env names `NEXUS_FLOW_API_URL` / `NEXUS_FLOW_API_KEY` are still accepted.
90
76
 
91
77
  ---
92
78
 
93
- MIT · [H1VE Flow](https://app.h1ve.org)
79
+ MIT · built with H1VE, by [H1VE](https://h1ve.org) 🐝
package/dist/index.js CHANGED
@@ -50,6 +50,24 @@ async function createBranch(name, runner = defaultGitRunner) {
50
50
  throw new Error("GIT_BRANCH_FAILED");
51
51
  }
52
52
  }
53
+ async function switchBranch(name, runner = defaultGitRunner) {
54
+ try {
55
+ await runner(["switch", name]);
56
+ } catch (err) {
57
+ const text = gitErrText(err);
58
+ if (/not a git repository/i.test(text)) throw new Error("NOT_A_GIT_REPO");
59
+ throw new Error("GIT_SWITCH_FAILED");
60
+ }
61
+ }
62
+ async function listLocalBranches(runner = defaultGitRunner) {
63
+ let out;
64
+ try {
65
+ out = await runner(["for-each-ref", "--format=%(refname:short)", "refs/heads"]);
66
+ } catch {
67
+ return [];
68
+ }
69
+ return out.split("\n").map((l) => l.trim()).filter(Boolean);
70
+ }
53
71
 
54
72
  // ../core/src/client.ts
55
73
  var NexusApiError = class extends Error {
@@ -128,16 +146,17 @@ function createClient(config) {
128
146
  // do id e ignora o header onde não escopa — então mandar sempre é simples e seguro.
129
147
  ...config.projectId ? { "x-project-id": config.projectId } : {}
130
148
  };
131
- async function postAction(featureId, action, body) {
149
+ async function writeAction(method, featureId, action, body) {
132
150
  const url = `${rootUrl(config.baseUrl)}/api/features/${encodeURIComponent(featureId)}/${action}`;
133
151
  const res = await doFetch(url, {
134
- method: "POST",
152
+ method,
135
153
  headers: { ...authHeaders, "content-type": "application/json" },
136
154
  body: JSON.stringify(body)
137
155
  });
138
156
  if (!res.ok) throw mapWriteError(res.status, await readErrorCode(res));
139
157
  return await res.json();
140
158
  }
159
+ const postAction = (featureId, action, body) => writeAction("POST", featureId, action, body);
141
160
  return {
142
161
  async getContext(branch) {
143
162
  const url = `${rootUrl(config.baseUrl)}/api/context?branch=${encodeURIComponent(branch)}`;
@@ -157,9 +176,38 @@ function createClient(config) {
157
176
  if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
158
177
  return await res.json();
159
178
  },
179
+ async recallMemory(query) {
180
+ const params = new URLSearchParams();
181
+ if (query.entities?.length) params.set("entities", query.entities.join(","));
182
+ if (query.text) params.set("q", query.text);
183
+ if (query.limit) params.set("limit", String(query.limit));
184
+ const url = `${rootUrl(config.baseUrl)}/api/memory/recall?${params.toString()}`;
185
+ const res = await doFetch(url, { headers: authHeaders });
186
+ if (!res.ok) throw errorForStatus(res.status, await readErrorCode(res));
187
+ return await res.json();
188
+ },
160
189
  startFeature(featureId, slug) {
161
190
  return postAction(featureId, "start", slug ? { slug } : {});
162
191
  },
192
+ setSpec(featureId, specContent) {
193
+ return writeAction("PATCH", featureId, "spec", { spec_content: specContent });
194
+ },
195
+ // SPEC-100 (B2): best-effort — o juiz de drift é ADVISORY; qualquer falha → {checked:false}
196
+ // (nunca lança, pra não estragar o fluxo de gravar a SPEC no `set_spec` do MCP).
197
+ async checkSpecAlignment(featureId) {
198
+ try {
199
+ const url = `${rootUrl(config.baseUrl)}/api/features/${encodeURIComponent(featureId)}/spec/alignment`;
200
+ const res = await doFetch(url, {
201
+ method: "POST",
202
+ headers: { ...authHeaders, "content-type": "application/json" },
203
+ body: "{}"
204
+ });
205
+ if (!res.ok) return { checked: false };
206
+ return await res.json();
207
+ } catch {
208
+ return { checked: false };
209
+ }
210
+ },
163
211
  moveStage(featureId, toStage, note) {
164
212
  return postAction(featureId, "move", { to_stage: toStage, note });
165
213
  },
@@ -442,16 +490,17 @@ function formatBlocker(r) {
442
490
  return `\u2713 Blocker aberto: ${r.description}`;
443
491
  }
444
492
  function formatStartList(list) {
445
- const header = "Features atribu\xEDdas a iniciar \u2014 escolha: nf start <n\xBA>";
446
- const rows = list.map(
447
- (f, i) => ` ${String(i + 1).padStart(2)}. ${f.name} [${stageLabel(f.stage)}]`
448
- );
493
+ const header = "Features para iniciar ou retomar \u2014 escolha: nf start <n\xBA>";
494
+ const rows = list.map((f, i) => {
495
+ const tag = f.branch_slug !== null ? " (retomar)" : "";
496
+ return ` ${String(i + 1).padStart(2)}. ${f.name} [${stageLabel(f.stage)}]${tag}`;
497
+ });
449
498
  return [header, ...rows].join("\n");
450
499
  }
451
- function formatStart(r) {
500
+ function formatStart(r, opts) {
452
501
  return [
453
- `\u2713 Iniciada: ${r.feature.name}`,
454
- `Branch: ${r.branch_name} (criada e ativa)`,
502
+ `\u2713 ${opts?.resumed ? "Retomada" : "Iniciada"}: ${r.feature.name}`,
503
+ `Branch: ${r.branch_name} (${opts?.resumed ? "pronta e ativa" : "criada e ativa"})`,
455
504
  "Pr\xF3ximo: codar \u2192 'nf status' \u2192 'nf done' (move dev \u2192 pr)"
456
505
  ].join("\n");
457
506
  }
@@ -573,8 +622,15 @@ async function health(io) {
573
622
  }
574
623
 
575
624
  // src/commands/start.ts
576
- function startable(features) {
577
- return features.filter((f) => f.branch_slug === null && f.stage !== "main");
625
+ function branchSlugOf(branch) {
626
+ return branch.match(/^feat\/[^/]+\/(.+)$/)?.[1] ?? null;
627
+ }
628
+ function startable(features, localBranches) {
629
+ const localSlugs = new Set(
630
+ localBranches.map(branchSlugOf).filter((s) => s !== null)
631
+ );
632
+ const hasLocalBranch = (f) => f.branch_slug !== null && localSlugs.has(f.branch_slug);
633
+ return features.filter((f) => f.stage !== "main" && !hasLocalBranch(f));
578
634
  }
579
635
  function pick(list, arg) {
580
636
  if (!arg) return list.length === 1 ? list[0] ?? null : null;
@@ -585,27 +641,25 @@ function pick(list, arg) {
585
641
  throw new CliError(`Feature n\xE3o encontrada: ${arg}. Rode 'nf start' para ver a lista.`);
586
642
  }
587
643
  async function start(io, args) {
588
- const list = startable(await io.client.listFeatures());
644
+ const [features, localBranches] = await Promise.all([io.client.listFeatures(), io.listBranches()]);
645
+ const list = startable(features, localBranches);
589
646
  if (list.length === 0) {
590
- throw new CliError("Nenhuma feature atribu\xEDda a iniciar (sem branch e fora de main).");
647
+ throw new CliError("Nenhuma feature para iniciar ou retomar (sem branch local e fora de main).");
591
648
  }
592
649
  const selected = pick(list, args.positional);
593
650
  if (!selected) {
594
651
  return { human: formatStartList(list), data: { features: list } };
595
652
  }
653
+ const resuming = selected.branch_slug !== null;
596
654
  const result = await io.client.startFeature(selected.id, args.slug);
597
655
  try {
598
656
  await io.createBranch(result.branch_name);
599
657
  } catch (err) {
600
658
  const code = err instanceof Error ? err.message : "";
601
- if (code === "BRANCH_EXISTS") {
602
- throw new CliError(
603
- `branch_slug gravado, mas a branch '${result.branch_name}' j\xE1 existe. Rode: git switch ${result.branch_name}`
604
- );
605
- }
606
- throw err;
659
+ if (code === "BRANCH_EXISTS") await io.switchBranch(result.branch_name);
660
+ else throw err;
607
661
  }
608
- return { human: formatStart(result), data: result };
662
+ return { human: formatStart(result, { resumed: resuming }), data: result };
609
663
  }
610
664
 
611
665
  // src/commands/connect.ts
@@ -1232,14 +1286,14 @@ async function logout(io, _args) {
1232
1286
  }
1233
1287
 
1234
1288
  // src/index.ts
1235
- var VERSION = "0.5.0";
1289
+ var VERSION = "0.6.0";
1236
1290
  var HELP = `nf \u2014 CLI do Nexus Flow
1237
1291
 
1238
1292
  Uso:
1239
1293
  nf login autoriza no navegador e salva a credencial local (sem colar nada)
1240
1294
  nf logout remove a credencial local
1241
1295
  nf start [<n\xBA|id>] [--slug <s>]
1242
- inicia uma feature atribu\xEDda: cria a branch e grava o slug
1296
+ inicia (ou retoma) uma feature: cria/entra na branch e grava o slug
1243
1297
  nf status estado da feature da branch atual
1244
1298
  nf spec imprime a spec da feature
1245
1299
  nf move <stage> [--note] move a feature de stage
@@ -1333,6 +1387,8 @@ ${HELP}`);
1333
1387
  client,
1334
1388
  resolveBranch: () => currentBranch(defaultGitRunner),
1335
1389
  createBranch: (name) => createBranch(name, defaultGitRunner),
1390
+ switchBranch: (name) => switchBranch(name, defaultGitRunner),
1391
+ listBranches: () => listLocalBranches(defaultGitRunner),
1336
1392
  readFile: (path) => readFile(path, "utf8"),
1337
1393
  readStdin,
1338
1394
  writeEnvVars: (pairs, opts) => writeEnvVars(pairs, { file: opts?.file }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h1veframework/cli",
3
- "version": "0.5.1",
4
- "description": "CLI `nf` do H1VE Flow: opera o fluxo da feature (status/move/blocker/spec/done/health/start) da própria branch, via PAT.",
3
+ "version": "0.6.0",
4
+ "description": "The CLI for H1VE the governance & memory layer for teams building with AI. Drive your feature flow (start, status, move, spec, done) from the terminal. Works with Claude Code, Cursor and Copilot.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -19,17 +19,27 @@
19
19
  "url": "git+https://github.com/GusHartz/nexus-flow.git",
20
20
  "directory": "packages/cli"
21
21
  },
22
- "homepage": "https://github.com/GusHartz/nexus-flow/tree/main/packages/cli#readme",
22
+ "homepage": "https://h1ve.org",
23
23
  "bugs": {
24
24
  "url": "https://github.com/GusHartz/nexus-flow/issues"
25
25
  },
26
26
  "keywords": [
27
27
  "h1ve",
28
- "h1ve-flow",
29
28
  "cli",
30
29
  "nf",
30
+ "ai",
31
+ "claude-code",
32
+ "cursor",
33
+ "copilot",
34
+ "ai-coding",
35
+ "ai-agents",
36
+ "memory",
37
+ "anti-drift",
38
+ "context",
39
+ "governance",
40
+ "developer-tools",
31
41
  "dev-workflow",
32
- "governance"
42
+ "mcp"
33
43
  ],
34
44
  "publishConfig": {
35
45
  "access": "public"