@vectorplane/ctrl-cli 0.1.13 → 0.1.15

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI oficial do VectorPlane.
4
4
 
5
- Use o comando `vp` para autenticar, verificar a sessão local e sincronizar o workspace com o produto.
5
+ Use o comando `vp` para autenticar, verificar a sessão local, sincronizar o workspace e operar o control plane com uma sessão local endurecida.
6
6
 
7
7
  ## Instalação
8
8
 
@@ -15,123 +15,73 @@ npm install -g @vectorplane/ctrl-cli
15
15
  ```bash
16
16
  vp init
17
17
  vp login
18
+ vp login --manual
19
+ vp login --device
18
20
  vp logout
19
21
  vp status
22
+ vp doctor --policy
20
23
  vp sync
21
24
  vp task templates
22
25
  vp task run --title "Entrega X" --intention "Implementar fluxo Y"
23
- vp task run --template code-review --title "Review auth" --intention "Revisar risco e cobertura do fluxo de auth"
24
26
  vp task watch <task-id>
25
- vp task claim --capability qa.test
26
- vp task execute <task-id> --step <step-id>
27
- vp task daemon --capability qa.test
28
27
  vp context --search "decisões sobre autenticação"
29
- vp context --delivery --search "fluxo de login" --type decisions
30
- vp context --diff --from latest-1 --to latest
31
28
  vp workspace webhook list
32
- vp workspace webhook create --name approvals --url https://example.com/hooks/vectorplane --secret supersecret123 --events task.approval_required,task.blocked
33
- vp context --workspace <workspace> --delivery
34
- vp session check-in --workspace <workspace> --agent codex --type codex --client openai-codex --feature feature-key --task task-key --owning-path "src/core,src/ui" --need "api-contract" --provide "implementation" --status "starting implementation"
35
- vp draft create --type progress --title "Entrega concluída" --content "Resumo da mudança"
29
+ vp session check-in --workspace <workspace> --agent codex --type codex --client openai-codex
36
30
  ```
37
31
 
38
- ## Comandos
39
-
40
- ### `vp init`
41
-
42
- - autentica localmente se ainda não houver sessão
43
- - resolve e associa o workspace atual a partir do `git remote`, somente quando houver correspondência com um workspace que o usuário pode acessar
44
- - detecta o agente local automaticamente, pede confirmação se houver ambiguidade e aceita `--agent`
45
- - baixa o template oficial do backend
46
- - grava o arquivo local e adiciona a entrada no `.gitignore`
47
- - aceita `--force` para sobrescrever template existente
48
- - durante execução, se outro agente passar a operar no workspace e a detecção for confiável, o setup local é reconciliado automaticamente
32
+ ## Comandos principais
49
33
 
50
34
  ### `vp login`
51
35
 
52
- - abre o navegador para autenticação
53
- - conclui o login com callback local seguro
54
- - salva a sessão localmente
55
- - suporta `--no-browser` e `--manual`
56
- - em ambientes remotos ou isolados, o loopback local pode exigir um fluxo alternativo
57
- - a URL web do login é emitida pelo control plane; o domínio do app não deve ser hardcoded em automações externas
36
+ - suporta `browser`, `manual` e `device`
37
+ - `--manual` evita abrir o navegador automaticamente
38
+ - `--device` evita dependência de callback loopback e fecha melhor com SSH, container, CI e WSL
39
+ - a URL web do login é emitida pelo control plane
58
40
 
59
41
  ### `vp logout`
60
42
 
61
- - revoga a sessão do CLI quando possível
62
- - remove a sessão local do dispositivo
43
+ - tenta revogar a sessão remotamente
44
+ - limpa a sessão local do dispositivo
45
+ - registra auditoria local mínima da operação
46
+
47
+ ### `vp doctor`
63
48
 
64
- ### `vp sync`
49
+ - valida storage de sessão, política ativa, conectividade e ambiente local
50
+ - suporta `--policy` para checar só conformidade local
51
+ - recomenda o modo de login mais seguro para o ambiente atual
65
52
 
66
- - coleta contexto local do workspace
67
- - envia a sincronização para o VectorPlane
68
- - usa fila local quando a API estiver indisponível
53
+ ### `vp config`
54
+
55
+ - gerencia perfis, política local e envelope de privacidade
56
+ - `vp config privacy set <standard|minimal|enterprise>`
57
+ - `vp config policy set <chave> <true|false>`
69
58
 
70
59
  ### `vp status`
71
60
 
72
- - mostra o estado da sessão local
73
- - exibe workspace ativo, diretório atual e última sincronização
74
-
75
- ### `vp task`
76
-
77
- - cria tasks no orchestrator do workspace
78
- - lista templates disponíveis com `vp task templates`
79
- - aceita `--template <slug>` para herdar capabilities e paths padrão do backend
80
- - lista e inspeciona tasks existentes
81
- - acompanha uma task em tempo real com `vp task watch <taskId>`
82
- - faz claim explícito de steps `pending_claim` atribuídos a agentes CLI
83
- - executa steps locais via `metadata.execution.command`
84
- - suporta loop controlado com `vp task daemon --capability ...`
85
- - registra delegação de step
86
- - envia callback de execução por step para concluir a pipeline
87
- - lê handoff operacional por task
88
- - consulta observabilidade por task ou workspace
89
- - consulta health do workspace ligado às tasks
90
- - para automação local real, o agente de registry deve ter `metadata.execution` com:
91
- - `type: "command"`
92
- - `command: "npm"` ou equivalente
93
- - `args`, `cwd` e `env` opcionais
94
-
95
- ### `vp draft`
96
-
97
- - cria drafts editoriais ligados ao workspace atual
98
- - lista drafts existentes para conferência rápida
99
- - suporta `ui`, `ux`, `project_skeleton`, `template_engineering`, `patterns`, `progress`, `decisions` e `architecture`
100
- - aceita `--no-impact` para registrar explicitamente quando uma lane não foi afetada
101
-
102
- ### `vp session`
103
-
104
- - suporta `check-in`, `heartbeat` e `check-out`
105
- - permite declarar `feature`, `task`, `component`, `role`, `owning-path`, `need`, `provide` e `status`
106
- - deve ser usado em conjunto com `vp context --delivery` para reduzir conflito entre agentes ativos
107
-
108
- ### Comandos adicionais já implementados
109
-
110
- - `vp whoami`
111
- - mostra a identidade autenticada
112
- - `vp doctor`
113
- - executa verificações locais e de conectividade
114
- - `vp config`
115
- - gerencia perfis e configuração local
116
- - inclui `vp config privacy` e `vp config policy`
117
- - `vp workspace`
118
- - gerencia o workspace atual
119
- - suporta `vp workspace webhook list|create|delete` para integrações externas por workspace
120
- - `vp session`
121
- - gerencia sessões de agente
122
- - `vp context`
123
- - carrega contexto remoto do workspace
124
- - suporta `--search "QUERY"` para busca semântica na memória
125
- - com `--delivery --search`, retorna delivery context com fragmentos semânticos relevantes
126
- - suporta `--diff --from latest-1 --to latest` para comparar snapshots do workspace
127
- - `vp bootstrap`
128
- - obtém instruções de setup do workspace
129
- - `vp event send`
130
- - envia eventos operacionais
131
- - `vp draft`
132
- - envia e consulta drafts editoriais do workspace
133
- - `vp task`
134
- - opera tasks do autonomous control plane
61
+ - mostra estado da sessão local
62
+ - exibe storage usado, último login, último refresh, último logout e falhas recentes de auth
63
+
64
+ ## Privacidade de máquina
65
+
66
+ Perfis:
67
+
68
+ - `standard`: envia o contexto operacional mínimo usual
69
+ - `minimal`: reduz ao máximo dados locais e desabilita lookup de IP/interface por padrão
70
+ - `enterprise`: permite envelope mais rico, mas continua respeitando policies locais
71
+
72
+ Policies disponíveis:
73
+
74
+ - `requireSecureStorage`
75
+ - `disableFileSessionFallback`
76
+ - `requireManualLogin`
77
+ - `requireDeviceLogin`
78
+ - `blockPublicIpLookup`
79
+ - `blockMacAddressCollection`
80
+ - `blockUsernameCollection`
81
+ - `blockNetworkInterfaceCollection`
82
+ - `blockCurrentDirectoryCollection`
83
+ - `blockHomeDirectoryCollection`
84
+ - `blockShellCollection`
135
85
 
136
86
  ## Persistência local
137
87
 
@@ -146,8 +96,14 @@ vp draft create --type progress --title "Entrega concluída" --content "Resumo d
146
96
  ## Segurança
147
97
 
148
98
  - tokens nunca são impressos no terminal
149
- - tokens nunca são enviados por query string
150
- - o callback valida o `state`
99
+ - logs mascaram chaves sensíveis e ids de sessão
151
100
  - a sessão usa secure storage do sistema quando disponível
152
- - existe fallback para arquivo local apenas quando permitido
101
+ - existe fallback para arquivo local apenas quando permitido pela policy
153
102
  - `vp logout` encerra a sessão local e tenta revogação remota
103
+ - o CLI continua sendo thin client: autorização e regra crítica permanecem no backend
104
+
105
+ ## Tutoriais relacionados
106
+
107
+ - setup completo do ecossistema: [/home/developer/Documentos/Projetos/conductor-edge-ia/docs/tutorials/setup-ecossistema.md](/home/developer/Documentos/Projetos/conductor-edge-ia/docs/tutorials/setup-ecossistema.md)
108
+ - operação de workspace: [/home/developer/Documentos/Projetos/conductor-edge-ia/docs/tutorials/operacao-workspace.md](/home/developer/Documentos/Projetos/conductor-edge-ia/docs/tutorials/operacao-workspace.md)
109
+ - memória e agentes: [/home/developer/Documentos/Projetos/conductor-edge-ia/docs/tutorials/memoria-e-agentes.md](/home/developer/Documentos/Projetos/conductor-edge-ia/docs/tutorials/memoria-e-agentes.md)
@@ -1 +1 @@
1
- export declare function runDoctorCommand(cliVersion: string): Promise<number>;
1
+ export declare function runDoctorCommand(cliVersion: string, args?: string[]): Promise<number>;
@@ -2,10 +2,11 @@ import { access } from "node:fs/promises";
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import { configDirectoryExists, ensureSessionAvailable, getConfigDirectoryPath, getSessionStorageInfo } from "../core/config.js";
4
4
  import { collectGitContext } from "../core/git.js";
5
- import { collectMachineContext, collectRuntimeContext } from "../core/machine.js";
5
+ import { assessExecutionEnvironment, collectMachineContext, collectRuntimeContext, describePrivacyEnvelope } from "../core/machine.js";
6
6
  import { loadRuntimeStatus } from "../core/runtime.js";
7
7
  import { ensureFreshSession } from "../core/session.js";
8
8
  import { VectorPlaneApiClient } from "../core/api.js";
9
+ import { getBooleanOption, parseArgs } from "../core/cli.js";
9
10
  async function writable(filePath) {
10
11
  try {
11
12
  await access(filePath, fsConstants.W_OK);
@@ -18,23 +19,9 @@ async function writable(filePath) {
18
19
  function printCheck(label, ok, details) {
19
20
  process.stdout.write(`${ok ? "OK" : "FAIL"} ${label}: ${details}\n`);
20
21
  }
21
- function detectExecutionRisks() {
22
- const risks = [];
23
- if (process.env.WSL_DISTRO_NAME) {
24
- risks.push("wsl");
25
- }
26
- if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT) {
27
- risks.push("ssh");
28
- }
29
- if (process.env.CI === "true") {
30
- risks.push("ci");
31
- }
32
- if (process.env.CONTAINER || process.env.DOCKER_CONTAINER) {
33
- risks.push("container");
34
- }
35
- return risks;
36
- }
37
- export async function runDoctorCommand(cliVersion) {
22
+ export async function runDoctorCommand(cliVersion, args = []) {
23
+ const parsed = parseArgs(args);
24
+ const policyOnly = getBooleanOption(parsed, "policy");
38
25
  const runtime = await loadRuntimeStatus();
39
26
  const configDirectory = await getConfigDirectoryPath();
40
27
  const [hasConfigDir, git, canWriteConfig] = await Promise.all([
@@ -43,15 +30,21 @@ export async function runDoctorCommand(cliVersion) {
43
30
  writable(configDirectory),
44
31
  ]);
45
32
  const storage = await getSessionStorageInfo(runtime.profile.name);
46
- const risks = detectExecutionRisks();
33
+ const environment = assessExecutionEnvironment();
34
+ const privacyEnvelope = describePrivacyEnvelope(runtime.profile.machinePrivacyProfile, runtime.config.policy);
47
35
  printCheck("config_dir", hasConfigDir, configDirectory);
48
36
  printCheck("config_dir_writable", canWriteConfig, canWriteConfig ? "gravável" : "sem permissão de escrita");
49
37
  printCheck("git", git.isRepository, git.isRepository ? (git.rootPath ?? process.cwd()) : "repositório não detectado");
50
38
  printCheck("node", true, process.version);
51
39
  printCheck("privacy_profile", true, runtime.profile.machinePrivacyProfile);
40
+ printCheck("privacy_envelope", true, JSON.stringify(privacyEnvelope));
52
41
  printCheck("session_storage", (storage?.protected ?? false) || runtime.config.preferredSessionStorage === "system", storage ? `${storage.backend}:${storage.protected ? "protected" : "file"}` : runtime.config.preferredSessionStorage);
53
42
  printCheck("policy_secure_storage", !runtime.config.policy.requireSecureStorage || storage?.protected === true, JSON.stringify(runtime.config.policy));
54
- printCheck("execution_environment", risks.length === 0, risks.length === 0 ? "local" : risks.join(","));
43
+ printCheck("execution_environment", environment.loopbackLikelySafe, environment.risks.length === 0 ? "local" : environment.risks.join(","));
44
+ printCheck("login_recommendation", true, environment.recommendedLoginMode);
45
+ if (policyOnly) {
46
+ return 0;
47
+ }
55
48
  const apiClient = new VectorPlaneApiClient(runtime.profile.apiBaseUrl, runtime.config.requestTimeoutMs, runtime.logger);
56
49
  try {
57
50
  const health = await apiClient.getHealth();
@@ -12,9 +12,9 @@ const VALID_DRAFT_TYPES = new Set([
12
12
  "decisions",
13
13
  "progress",
14
14
  "patterns",
15
- "ui",
16
- "ux",
17
- "template_engineering",
15
+ "ui_ux",
16
+ "software_engineering",
17
+ "system_design",
18
18
  "project_skeleton",
19
19
  ]);
20
20
  const VALID_STATUSES = new Set([
@@ -43,8 +43,8 @@ function inferMemoryKey(type, title) {
43
43
  ? "decision"
44
44
  : type === "patterns"
45
45
  ? "pattern"
46
- : type === "template_engineering"
47
- ? "template"
46
+ : type === "software_engineering"
47
+ ? "software-engineering"
48
48
  : type === "project_skeleton"
49
49
  ? "skeleton"
50
50
  : type;
@@ -1,18 +1,37 @@
1
1
  import { getBooleanOption, getStringOption, parseArgs } from "../core/cli.js";
2
2
  import { runLoginFlow } from "../core/auth.js";
3
3
  import { getSessionStorageInfo, saveSession, setActiveProfile, updateProfileState, upsertProfile } from "../core/config.js";
4
- import { collectMachineContext, collectRuntimeContext } from "../core/machine.js";
4
+ import { assessExecutionEnvironment, collectMachineContext, collectRuntimeContext } from "../core/machine.js";
5
5
  import { loadRuntimeStatus } from "../core/runtime.js";
6
6
  import { VectorPlaneApiClient } from "../core/api.js";
7
+ function resolveLoginMode(args) {
8
+ if (args.device || args.requireDeviceLogin) {
9
+ return "device";
10
+ }
11
+ if (args.manual || args.noBrowser || args.requireManualLogin) {
12
+ return "manual";
13
+ }
14
+ return "browser";
15
+ }
7
16
  export async function runLoginCommand(cliVersion, args) {
8
17
  const parsed = parseArgs(args);
9
18
  const requestedProfile = getStringOption(parsed, "profile");
10
- const noBrowser = getBooleanOption(parsed, "no-browser") || getBooleanOption(parsed, "manual");
11
19
  if (requestedProfile) {
12
20
  await upsertProfile(requestedProfile, { name: requestedProfile });
13
21
  await setActiveProfile(requestedProfile);
14
22
  }
15
23
  const runtime = await loadRuntimeStatus();
24
+ const loginMode = resolveLoginMode({
25
+ noBrowser: getBooleanOption(parsed, "no-browser"),
26
+ manual: getBooleanOption(parsed, "manual"),
27
+ device: getBooleanOption(parsed, "device"),
28
+ requireManualLogin: runtime.config.policy.requireManualLogin,
29
+ requireDeviceLogin: runtime.config.policy.requireDeviceLogin,
30
+ });
31
+ const environment = assessExecutionEnvironment();
32
+ if (environment.recommendedLoginMode === "device" && loginMode === "browser") {
33
+ runtime.logger.warn("o ambiente atual parece pouco confiável para loopback. considere `vp login --device`.");
34
+ }
16
35
  const machine = await collectMachineContext(runtime.device, runtime.config);
17
36
  const runtimeContext = await collectRuntimeContext(cliVersion, "login", process.argv.slice(2));
18
37
  const apiClient = new VectorPlaneApiClient(runtime.profile.apiBaseUrl, runtime.config.requestTimeoutMs, runtime.logger);
@@ -24,7 +43,7 @@ export async function runLoginCommand(cliVersion, args) {
24
43
  device: runtime.device,
25
44
  apiClient,
26
45
  logger: runtime.logger,
27
- noBrowser: noBrowser || runtime.config.policy.requireManualLogin,
46
+ loginMode,
28
47
  });
29
48
  await saveSession(session, runtime.profile.name);
30
49
  const storage = await getSessionStorageInfo(runtime.profile.name);
@@ -34,6 +53,7 @@ export async function runLoginCommand(cliVersion, args) {
34
53
  lastWorkspace: session.workspace,
35
54
  lastError: null,
36
55
  lastLoginAt: session.obtainedAt,
56
+ lastLoginMethod: loginMode,
37
57
  lastSessionId: session.sessionId,
38
58
  storageBackend: storage?.backend ?? null,
39
59
  storageProtected: storage?.protected ?? null,
@@ -42,6 +62,7 @@ export async function runLoginCommand(cliVersion, args) {
42
62
  });
43
63
  runtime.logger.success("login realizado com sucesso.");
44
64
  process.stdout.write(`Perfil: ${runtime.profile.name}\n`);
65
+ process.stdout.write(`Modo de login: ${loginMode}\n`);
45
66
  process.stdout.write(`Workspace ativo: ${session.workspace}\n`);
46
67
  return 0;
47
68
  }
@@ -15,8 +15,10 @@ export async function runLogoutCommand() {
15
15
  return 0;
16
16
  }
17
17
  const apiClient = new VectorPlaneApiClient(runtime.profile.apiBaseUrl, runtime.config.requestTimeoutMs, runtime.logger);
18
+ let remoteRevokedAt = null;
18
19
  try {
19
20
  await revokeSession({ session, apiClient });
21
+ remoteRevokedAt = new Date().toISOString();
20
22
  }
21
23
  catch (error) {
22
24
  runtime.logger.warn("não foi possível revogar a sessão remotamente. removendo a sessão local mesmo assim.");
@@ -29,6 +31,7 @@ export async function runLogoutCommand() {
29
31
  await updateProfileState(runtime.profile.name, {
30
32
  lastCommand: "logout",
31
33
  lastLogoutAt: loggedOutAt,
34
+ lastRemoteRevokeAt: remoteRevokedAt,
32
35
  lastSessionId: null,
33
36
  });
34
37
  runtime.logger.success("sessão local encerrada.");
@@ -35,16 +35,22 @@ export async function runStatusCommand() {
35
35
  process.stdout.write(`Sessão CLI: ${session.sessionId}\n`);
36
36
  process.stdout.write(`Expira em: ${session.expiresAt}\n`);
37
37
  process.stdout.write(`Último refresh: ${profileState.lastRefreshAt ?? session.lastRefreshAt ?? "nunca"}\n`);
38
+ process.stdout.write(`Último login: ${profileState.lastLoginAt ?? "desconhecido"}\n`);
39
+ process.stdout.write(`Último método de login: ${profileState.lastLoginMethod ?? "desconhecido"}\n`);
40
+ process.stdout.write(`Último logout: ${profileState.lastLogoutAt ?? "nunca"}\n`);
41
+ process.stdout.write(`Última revogação remota: ${profileState.lastRemoteRevokeAt ?? "nunca"}\n`);
38
42
  if (git.branch) {
39
43
  process.stdout.write(`Branch: ${git.branch}\n`);
40
44
  }
41
45
  process.stdout.write(`Última sincronização: ${profileState.lastSyncAt ?? "nunca"}\n`);
42
46
  process.stdout.write(`Status do último sync: ${profileState.lastSyncStatus ?? "desconhecido"}\n`);
43
- process.stdout.write(`Último login: ${profileState.lastLoginAt ?? "desconhecido"}\n`);
44
- process.stdout.write(`Último logout: ${profileState.lastLogoutAt ?? "nunca"}\n`);
45
47
  if (profileState.lastSnapshotPath) {
46
48
  process.stdout.write(`Último snapshot local: ${profileState.lastSnapshotPath}\n`);
47
49
  }
50
+ if (profileState.lastAuthFailure) {
51
+ process.stdout.write(`Última falha de auth: ${profileState.lastAuthFailure}\n`);
52
+ process.stdout.write(`Falha registrada em: ${profileState.lastAuthFailureAt ?? "desconhecido"}\n`);
53
+ }
48
54
  if (profileState.lastError) {
49
55
  process.stdout.write(`Último erro: ${profileState.lastError}\n`);
50
56
  }
@@ -1,4 +1,4 @@
1
- import type { CliConfig, CliProfileConfig } from "../types/config.js";
1
+ import type { CliConfig, CliLoginMode, CliProfileConfig } from "../types/config.js";
2
2
  import type { AuthSession } from "../types/auth.js";
3
3
  import type { DeviceIdentity, MachineContext, RuntimeContext } from "../types/machine.js";
4
4
  import { VectorPlaneApiClient } from "./api.js";
@@ -13,5 +13,5 @@ export declare function runLoginFlow(params: {
13
13
  device: DeviceIdentity;
14
14
  apiClient: VectorPlaneApiClient;
15
15
  logger: Logger;
16
- noBrowser?: boolean;
16
+ loginMode: CliLoginMode;
17
17
  }): Promise<AuthSession>;
package/dist/core/auth.js CHANGED
@@ -25,48 +25,118 @@ export async function openBrowser(url) {
25
25
  });
26
26
  });
27
27
  }
28
+ function printLoginInstructions(loginMode, loginUrl, logger) {
29
+ if (loginMode === "browser") {
30
+ logger.info("abrindo navegador para autenticação...");
31
+ return;
32
+ }
33
+ if (loginMode === "manual") {
34
+ logger.info("login manual habilitado. abra a URL abaixo no navegador:");
35
+ process.stdout.write(`${loginUrl}\n`);
36
+ return;
37
+ }
38
+ logger.info("login device-like habilitado. use a URL abaixo em qualquer navegador confiável:");
39
+ process.stdout.write(`${loginUrl}\n`);
40
+ }
41
+ async function waitForCallbackCode(callbackServer, timeoutMs) {
42
+ try {
43
+ const callback = await callbackServer.waitForCallback(timeoutMs);
44
+ return callback.code;
45
+ }
46
+ catch (error) {
47
+ throw new AuthError("Falha no callback local do login do CLI.", error);
48
+ }
49
+ }
50
+ async function waitForAuthorizedAttempt(params) {
51
+ const startedAt = Date.now();
52
+ let announcedAuthorization = false;
53
+ while (Date.now() - startedAt < params.timeoutMs) {
54
+ let status;
55
+ try {
56
+ status = await params.apiClient.getLoginAttemptStatus(params.attemptId, params.pollToken);
57
+ }
58
+ catch (error) {
59
+ throw new AuthError("Falha ao acompanhar a autorização do login do CLI por polling.", error);
60
+ }
61
+ const code = pickAuthorizedCode(status);
62
+ if (code) {
63
+ return code;
64
+ }
65
+ if (status.status === "authorized" && !announcedAuthorization) {
66
+ announcedAuthorization = true;
67
+ params.logger.info("autorização recebida pela API. finalizando sessão...");
68
+ }
69
+ await delay(LOGIN_ATTEMPT_POLL_INTERVAL_MS);
70
+ }
71
+ throw new AuthError("Tempo esgotado aguardando a confirmação do login do CLI.");
72
+ }
73
+ function pickAuthorizedCode(status) {
74
+ if ((status.status === "authorized" || status.status === "completed") && typeof status.code === "string" && status.code.length > 0) {
75
+ return status.code;
76
+ }
77
+ return null;
78
+ }
79
+ function delay(ms) {
80
+ return new Promise((resolve) => {
81
+ setTimeout(resolve, ms);
82
+ });
83
+ }
28
84
  export async function runLoginFlow(params) {
29
85
  const state = generateLoginState();
30
- const callbackServer = await createCallbackServer(params.config.callbackHost, params.config.callbackPort, params.config.callbackPath, state);
86
+ const useLoopback = params.loginMode !== "device";
87
+ const callbackServer = useLoopback
88
+ ? await createCallbackServer(params.config.callbackHost, params.config.callbackPort, params.config.callbackPath, state)
89
+ : null;
31
90
  try {
91
+ const redirectUri = callbackServer?.callbackUrl ?? `http://127.0.0.1/disabled-device-flow/${state}`;
32
92
  const loginAttempt = await params.apiClient.createLoginAttempt({
33
- redirectUri: callbackServer.callbackUrl,
93
+ redirectUri,
34
94
  state,
35
95
  client: CLI_CLIENT_ID,
36
96
  });
37
97
  const loginUrl = loginAttempt.loginUrl;
38
- if (params.noBrowser) {
39
- params.logger.info("login manual habilitado. abra a URL abaixo no navegador:");
40
- process.stdout.write(`${loginUrl}\n`);
41
- }
42
- else {
43
- params.logger.info("abrindo navegador para autenticação...");
98
+ printLoginInstructions(params.loginMode, loginUrl, params.logger);
99
+ if (params.loginMode === "browser") {
44
100
  const opened = await openBrowser(loginUrl);
45
101
  if (!opened) {
46
- params.logger.warn("não foi possível abrir o navegador automaticamente.");
102
+ params.logger.warn("não foi possível abrir o navegador automaticamente. continue pela URL abaixo.");
47
103
  process.stdout.write(`${loginUrl}\n`);
48
104
  }
49
105
  }
50
106
  params.logger.info("aguardando confirmação...");
51
- const code = await Promise.any([
52
- callbackServer.waitForCallback(CALLBACK_TIMEOUT_MS).then((callback) => callback.code),
53
- waitForAuthorizedAttempt({
107
+ const code = params.loginMode === "device"
108
+ ? await waitForAuthorizedAttempt({
54
109
  apiClient: params.apiClient,
55
110
  attemptId: loginAttempt.attemptId,
56
111
  pollToken: loginAttempt.pollToken,
57
112
  timeoutMs: CALLBACK_TIMEOUT_MS,
58
113
  logger: params.logger,
59
- }),
60
- ]).catch((error) => {
61
- throw new AuthError("Não foi possível concluir o login do CLI.", error);
62
- });
114
+ })
115
+ : await Promise.any([
116
+ waitForCallbackCode(callbackServer, CALLBACK_TIMEOUT_MS),
117
+ waitForAuthorizedAttempt({
118
+ apiClient: params.apiClient,
119
+ attemptId: loginAttempt.attemptId,
120
+ pollToken: loginAttempt.pollToken,
121
+ timeoutMs: CALLBACK_TIMEOUT_MS,
122
+ logger: params.logger,
123
+ }),
124
+ ]).catch((error) => {
125
+ throw new AuthError("Não foi possível concluir o login do CLI.", error);
126
+ });
63
127
  const payload = {
64
128
  code,
65
129
  client: CLI_CLIENT_ID,
66
130
  device: params.machine,
67
131
  runtime: params.runtime,
68
132
  };
69
- const tokenResponse = await params.apiClient.exchangeToken(payload);
133
+ let tokenResponse;
134
+ try {
135
+ tokenResponse = await params.apiClient.exchangeToken(payload);
136
+ }
137
+ catch (error) {
138
+ throw new AuthError("Falha na troca do código do CLI por sessão autenticada.", error);
139
+ }
70
140
  if (!tokenResponse.access_token || !tokenResponse.refresh_token) {
71
141
  throw new AuthError("A API retornou uma sessão inválida.");
72
142
  }
@@ -80,38 +150,11 @@ export async function runLoginFlow(params) {
80
150
  obtainedAt: new Date().toISOString(),
81
151
  expiresAt: new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString(),
82
152
  lastRefreshAt: null,
153
+ deviceId: params.device.machineId,
83
154
  };
84
155
  }
85
156
  finally {
86
- await callbackServer.close();
157
+ await callbackServer?.close();
87
158
  }
88
159
  }
89
- async function waitForAuthorizedAttempt(params) {
90
- const startedAt = Date.now();
91
- let announcedAuthorization = false;
92
- while (Date.now() - startedAt < params.timeoutMs) {
93
- const status = await params.apiClient.getLoginAttemptStatus(params.attemptId, params.pollToken);
94
- const code = pickAuthorizedCode(status);
95
- if (code) {
96
- return code;
97
- }
98
- if (status.status === "authorized" && !announcedAuthorization) {
99
- announcedAuthorization = true;
100
- params.logger.info("autorização recebida pela API. finalizando sessão...");
101
- }
102
- await delay(LOGIN_ATTEMPT_POLL_INTERVAL_MS);
103
- }
104
- throw new AuthError("Tempo esgotado aguardando a confirmação do login do CLI.");
105
- }
106
- function pickAuthorizedCode(status) {
107
- if ((status.status === "authorized" || status.status === "completed") && typeof status.code === "string" && status.code.length > 0) {
108
- return status.code;
109
- }
110
- return null;
111
- }
112
- function delay(ms) {
113
- return new Promise((resolve) => {
114
- setTimeout(resolve, ms);
115
- });
116
- }
117
160
  //# sourceMappingURL=auth.js.map
@@ -38,9 +38,14 @@ export const DEFAULT_CONFIG = {
38
38
  requireSecureStorage: false,
39
39
  disableFileSessionFallback: false,
40
40
  requireManualLogin: false,
41
+ requireDeviceLogin: false,
41
42
  blockPublicIpLookup: false,
42
43
  blockMacAddressCollection: false,
43
44
  blockUsernameCollection: false,
45
+ blockNetworkInterfaceCollection: false,
46
+ blockCurrentDirectoryCollection: false,
47
+ blockHomeDirectoryCollection: false,
48
+ blockShellCollection: false,
44
49
  },
45
50
  };
46
51
  export const DEFAULT_PROFILE_STATE = {
@@ -53,8 +58,10 @@ export const DEFAULT_PROFILE_STATE = {
53
58
  lastSnapshotPath: null,
54
59
  lastSessionId: null,
55
60
  lastLoginAt: null,
61
+ lastLoginMethod: null,
56
62
  lastRefreshAt: null,
57
63
  lastLogoutAt: null,
64
+ lastRemoteRevokeAt: null,
58
65
  storageBackend: null,
59
66
  storageProtected: null,
60
67
  lastAuthFailure: null,
@@ -74,7 +81,20 @@ export const DEFAULT_WORKSPACE_BINDINGS = {
74
81
  };
75
82
  export const CALLBACK_TIMEOUT_MS = 120_000;
76
83
  export const LOGIN_ATTEMPT_POLL_INTERVAL_MS = 1_000;
77
- export const SENSITIVE_KEYS = ["token", "authorization", "refresh", "secret", "password", "cookie"];
84
+ export const SENSITIVE_KEYS = [
85
+ "token",
86
+ "authorization",
87
+ "refresh",
88
+ "secret",
89
+ "password",
90
+ "cookie",
91
+ "set-cookie",
92
+ "session",
93
+ "machineid",
94
+ "deviceid",
95
+ "polltoken",
96
+ "code",
97
+ ];
78
98
  export const SAFE_ENV_KEYS = [
79
99
  "SHELL",
80
100
  "TERM",
package/dist/core/git.js CHANGED
@@ -9,17 +9,14 @@ function sanitizeRemoteUrl(remote) {
9
9
  if (remote.startsWith("git@")) {
10
10
  const [left, right] = remote.split(":", 2);
11
11
  const host = left.split("@")[1];
12
- return `ssh://${host}/${right.replace(/\.git$/i, "")}`;
12
+ return `https://${host}/${right.replace(/\.git$/i, "")}`;
13
13
  }
14
14
  try {
15
15
  const url = new URL(remote);
16
- url.username = "";
17
- url.password = "";
18
- url.pathname = url.pathname.replace(/\.git$/i, "");
19
- return url.toString();
16
+ return `https://${url.host}${url.pathname.replace(/\.git$/i, "")}`;
20
17
  }
21
18
  catch {
22
- return remote.replace(/:\/\/[^@]+@/, "://");
19
+ return remote.replace(/:\/\/[^@]+@/, "://").replace(/^ssh:\/\//i, "https://").replace(/\.git$/i, "");
23
20
  }
24
21
  }
25
22
  async function runGit(args, cwd) {
@@ -1,4 +1,11 @@
1
- import type { CliConfig } from "../types/config.js";
1
+ import type { CliConfig, CliPolicyConfig, MachinePrivacyProfile } from "../types/config.js";
2
2
  import type { DeviceIdentity, MachineContext, RuntimeContext } from "../types/machine.js";
3
+ export interface ExecutionEnvironmentAssessment {
4
+ risks: string[];
5
+ recommendedLoginMode: "browser" | "manual" | "device";
6
+ loopbackLikelySafe: boolean;
7
+ }
8
+ export declare function assessExecutionEnvironment(): ExecutionEnvironmentAssessment;
9
+ export declare function describePrivacyEnvelope(profile: MachinePrivacyProfile, policy: CliPolicyConfig): Record<string, boolean>;
3
10
  export declare function collectRuntimeContext(cliVersion: string, command: string, args: string[]): Promise<RuntimeContext>;
4
11
  export declare function collectMachineContext(identity: DeviceIdentity, config: CliConfig): Promise<MachineContext>;
@@ -30,7 +30,7 @@ async function detectPublicIp(timeoutMs) {
30
30
  clearTimeout(timer);
31
31
  }
32
32
  }
33
- function collectNetworkInterfaces() {
33
+ function collectNetworkInterfaces(includeMacAddress, includeAddresses) {
34
34
  const interfaces = os.networkInterfaces();
35
35
  const results = [];
36
36
  for (const [name, entries] of Object.entries(interfaces)) {
@@ -38,14 +38,48 @@ function collectNetworkInterfaces() {
38
38
  results.push({
39
39
  name,
40
40
  family: entry.family,
41
- mac: entry.mac,
42
- address: entry.address,
41
+ mac: includeMacAddress ? entry.mac : null,
42
+ address: includeAddresses ? entry.address : null,
43
43
  internal: entry.internal,
44
- cidr: entry.cidr ?? null,
44
+ cidr: includeAddresses ? (entry.cidr ?? null) : null,
45
45
  });
46
46
  }
47
47
  }
48
- return results.sort((left, right) => left.name.localeCompare(right.name) || left.address.localeCompare(right.address));
48
+ return results.sort((left, right) => left.name.localeCompare(right.name) || String(left.address).localeCompare(String(right.address)));
49
+ }
50
+ export function assessExecutionEnvironment() {
51
+ const risks = [];
52
+ if (process.env.WSL_DISTRO_NAME) {
53
+ risks.push("wsl");
54
+ }
55
+ if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT) {
56
+ risks.push("ssh");
57
+ }
58
+ if (process.env.CI === "true") {
59
+ risks.push("ci");
60
+ }
61
+ if (process.env.CONTAINER || process.env.DOCKER_CONTAINER || process.env.KUBERNETES_SERVICE_HOST) {
62
+ risks.push("container");
63
+ }
64
+ const loopbackLikelySafe = risks.length === 0;
65
+ const recommendedLoginMode = loopbackLikelySafe
66
+ ? "browser"
67
+ : risks.includes("ssh") || risks.includes("container") || risks.includes("ci")
68
+ ? "device"
69
+ : "manual";
70
+ return { risks, recommendedLoginMode, loopbackLikelySafe };
71
+ }
72
+ export function describePrivacyEnvelope(profile, policy) {
73
+ return {
74
+ username: !policy.blockUsernameCollection && profile !== "minimal",
75
+ publicIp: !policy.blockPublicIpLookup && profile !== "minimal",
76
+ networkInterfaces: !policy.blockNetworkInterfaceCollection && profile !== "minimal",
77
+ networkAddresses: profile === "enterprise" && !policy.blockNetworkInterfaceCollection,
78
+ macAddress: profile === "enterprise" && !policy.blockMacAddressCollection,
79
+ currentDirectory: !policy.blockCurrentDirectoryCollection && profile !== "minimal",
80
+ homeDirectory: !policy.blockHomeDirectoryCollection && profile === "enterprise",
81
+ shell: !policy.blockShellCollection && profile !== "minimal",
82
+ };
49
83
  }
50
84
  export async function collectRuntimeContext(cliVersion, command, args) {
51
85
  return {
@@ -62,14 +96,11 @@ export async function collectRuntimeContext(cliVersion, command, args) {
62
96
  export async function collectMachineContext(identity, config) {
63
97
  const cpus = os.cpus();
64
98
  const privacyProfile = config.profiles[config.activeProfile]?.machinePrivacyProfile ?? "standard";
65
- const collectPublicIp = !config.policy.blockPublicIpLookup && privacyProfile !== "minimal";
66
- const includeMacAddress = !config.policy.blockMacAddressCollection && privacyProfile === "enterprise";
67
- const includeUsername = !config.policy.blockUsernameCollection && privacyProfile !== "minimal";
68
- const networkInterfaces = collectNetworkInterfaces().map((entry) => ({
69
- ...entry,
70
- mac: includeMacAddress ? entry.mac : null,
71
- }));
72
- const publicIp = collectPublicIp ? await detectPublicIp(config.publicIpLookupTimeoutMs) : null;
99
+ const envelope = describePrivacyEnvelope(privacyProfile, config.policy);
100
+ const publicIp = envelope.publicIp ? await detectPublicIp(config.publicIpLookupTimeoutMs) : null;
101
+ const networkInterfaces = envelope.networkInterfaces
102
+ ? collectNetworkInterfaces(envelope.macAddress, envelope.networkAddresses)
103
+ : [];
73
104
  return {
74
105
  machineId: identity.machineId,
75
106
  hostname: os.hostname(),
@@ -77,19 +108,19 @@ export async function collectMachineContext(identity, config) {
77
108
  osRelease: os.release(),
78
109
  architecture: os.arch(),
79
110
  kernelVersion: os.version(),
80
- username: includeUsername ? os.userInfo().username : null,
111
+ username: envelope.username ? os.userInfo().username : null,
81
112
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
82
113
  locale: Intl.DateTimeFormat().resolvedOptions().locale,
83
- shell: process.env.SHELL ?? null,
84
- currentDirectory: process.cwd(),
85
- homeDirectory: os.homedir(),
114
+ shell: envelope.shell ? (process.env.SHELL ?? null) : null,
115
+ currentDirectory: envelope.currentDirectory ? process.cwd() : null,
116
+ homeDirectory: envelope.homeDirectory ? os.homedir() : null,
86
117
  cpuModel: cpus[0]?.model ?? null,
87
118
  cpuCount: cpus.length,
88
119
  loadAverage: os.loadavg(),
89
120
  memoryTotal: os.totalmem(),
90
121
  memoryFree: os.freemem(),
91
122
  uptimeSeconds: os.uptime(),
92
- networkInterfaces: privacyProfile === "minimal" ? [] : networkInterfaces,
123
+ networkInterfaces,
93
124
  publicIp,
94
125
  privacyProfile,
95
126
  };
package/dist/index.js CHANGED
@@ -31,14 +31,14 @@ function printHelp() {
31
31
  process.stdout.write("\n");
32
32
  process.stdout.write("Comandos principais:\n");
33
33
  process.stdout.write(" init [--agent <tipo>] [--workspace <workspace>] [--force]\n");
34
- process.stdout.write(" login [--no-browser|--manual]\n");
34
+ process.stdout.write(" login [--no-browser|--manual|--device]\n");
35
35
  process.stdout.write(" logout\n");
36
36
  process.stdout.write(" sync [--force]\n");
37
37
  process.stdout.write(" status\n");
38
38
  process.stdout.write(" task <run|templates|list|inspect|watch|claim|execute|daemon|approve|reject|delegate|step-update|handoff|observability|health>\n");
39
39
  process.stdout.write(" registry <list|register|update|deactivate>\n");
40
40
  process.stdout.write(" whoami\n");
41
- process.stdout.write(" doctor\n");
41
+ process.stdout.write(" doctor [--policy]\n");
42
42
  process.stdout.write(" draft <create|list>\n");
43
43
  process.stdout.write(" config <profile|get|set>\n");
44
44
  process.stdout.write(" workspace <current|use|resolve|clear|policy|webhook>\n");
@@ -73,7 +73,7 @@ export async function runCli(args) {
73
73
  case "whoami":
74
74
  return runWhoAmICommand(cliVersion);
75
75
  case "doctor":
76
- return runDoctorCommand(cliVersion);
76
+ return runDoctorCommand(cliVersion, rest);
77
77
  case "draft":
78
78
  return runDraftCommand(cliVersion, rest);
79
79
  case "config":
@@ -164,7 +164,7 @@ export interface AgentSessionResponse {
164
164
  workspaceId?: string;
165
165
  [key: string]: unknown;
166
166
  }
167
- export type MemoryDraftType = "architecture" | "decisions" | "progress" | "patterns" | "ui" | "ux" | "template_engineering" | "project_skeleton";
167
+ export type MemoryDraftType = "architecture" | "decisions" | "progress" | "patterns" | "ui_ux" | "software_engineering" | "system_design" | "project_skeleton";
168
168
  export type MemoryDraftStatus = "draft" | "in_review" | "approved" | "merged" | "rejected" | "archived";
169
169
  export interface MemoryDraftCreateRequest {
170
170
  docType: MemoryDraftType;
@@ -56,4 +56,5 @@ export interface AuthSession {
56
56
  obtainedAt: string;
57
57
  expiresAt: string;
58
58
  lastRefreshAt: string | null;
59
+ deviceId?: string | null;
59
60
  }
@@ -1,13 +1,19 @@
1
1
  export type VectorPlaneEnvironment = "production" | "preview" | "self-hosted";
2
2
  export type MachinePrivacyProfile = "standard" | "minimal" | "enterprise";
3
3
  export type SessionStorageBackend = "system" | "file";
4
+ export type CliLoginMode = "browser" | "manual" | "device";
4
5
  export interface CliPolicyConfig {
5
6
  requireSecureStorage: boolean;
6
7
  disableFileSessionFallback: boolean;
7
8
  requireManualLogin: boolean;
9
+ requireDeviceLogin: boolean;
8
10
  blockPublicIpLookup: boolean;
9
11
  blockMacAddressCollection: boolean;
10
12
  blockUsernameCollection: boolean;
13
+ blockNetworkInterfaceCollection: boolean;
14
+ blockCurrentDirectoryCollection: boolean;
15
+ blockHomeDirectoryCollection: boolean;
16
+ blockShellCollection: boolean;
11
17
  }
12
18
  export interface CliProfileConfig {
13
19
  name: string;
@@ -43,8 +49,10 @@ export interface CliProfileState {
43
49
  lastSnapshotPath: string | null;
44
50
  lastSessionId: string | null;
45
51
  lastLoginAt: string | null;
52
+ lastLoginMethod: CliLoginMode | null;
46
53
  lastRefreshAt: string | null;
47
54
  lastLogoutAt: string | null;
55
+ lastRemoteRevokeAt: string | null;
48
56
  storageBackend: SessionStorageBackend | null;
49
57
  storageProtected: boolean | null;
50
58
  lastAuthFailure: string | null;
@@ -6,7 +6,7 @@ export interface NetworkInterfaceContext {
6
6
  name: string;
7
7
  family: string;
8
8
  mac: string | null;
9
- address: string;
9
+ address: string | null;
10
10
  internal: boolean;
11
11
  cidr: string | null;
12
12
  }
@@ -36,8 +36,8 @@ export interface MachineContext {
36
36
  timezone: string;
37
37
  locale: string;
38
38
  shell: string | null;
39
- currentDirectory: string;
40
- homeDirectory: string;
39
+ currentDirectory: string | null;
40
+ homeDirectory: string | null;
41
41
  cpuModel: string | null;
42
42
  cpuCount: number;
43
43
  loadAverage: number[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorplane/ctrl-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Official VectorPlane CLI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,12 +14,17 @@
14
14
  "files": [
15
15
  "bin/vp.js",
16
16
  "dist/**/*.js",
17
+ "!dist/**/__tests__/**",
18
+ "!dist/**/*.test.js",
17
19
  "dist/**/*.d.ts",
20
+ "!dist/**/__tests__/**",
21
+ "!dist/**/*.test.d.ts",
18
22
  "README.md",
19
23
  "LICENSE"
20
24
  ],
21
25
  "publishConfig": {
22
- "access": "public"
26
+ "access": "public",
27
+ "provenance": true
23
28
  },
24
29
  "repository": {
25
30
  "type": "git",
@@ -43,7 +48,8 @@
43
48
  "pack:dry-run": "npm pack --dry-run",
44
49
  "release:check": "npm run check && npm run build && npm run pack:dry-run",
45
50
  "prepublishOnly": "npm run release:check",
46
- "start": "node ./dist/index.js"
51
+ "start": "node ./dist/index.js",
52
+ "test": "vitest run"
47
53
  },
48
54
  "keywords": [
49
55
  "vectorplane",
@@ -56,6 +62,7 @@
56
62
  "license": "MIT",
57
63
  "devDependencies": {
58
64
  "@types/node": "^24.5.2",
59
- "typescript": "^5.9.2"
65
+ "typescript": "^5.9.2",
66
+ "vitest": "^4.1.3"
60
67
  }
61
68
  }