@vecteur/cli 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Vecteur CLI
2
+
3
+ One command line for Vecteur's public API. Sign in through your browser, then use the same
4
+ Projects and run records as the web app. The client stores your credential locally; it never
5
+ calls the OPE, engine or database directly.
6
+
7
+ ```bash
8
+ npm install -g @vecteur/cli
9
+ vecteur login
10
+ vecteur whoami
11
+ vecteur project list
12
+ vecteur project create 'Transfer study'
13
+ vecteur project use 'Transfer study'
14
+ vecteur project get PROJECT_ID --json
15
+ vecteur run submit --ask 'GTO to GEO transfer'
16
+ vecteur run get RUN_ID --json
17
+ vecteur run events RUN_ID --json
18
+ vecteur artifact get sha256:ARTIFACT_DIGEST --output report.docx
19
+ vecteur logout
20
+ ```
21
+
22
+ Use `vecteur login --origin URL` for another deployment. For automation, supply a Project PAT
23
+ through `VECTEUR_TOKEN` and its origin through `VECTEUR_BASE_URL`; never pass a token on argv.
24
+ `--help` and `--version` work without signing in.
25
+
26
+ `project list` shows authorized Projects and the server's Organization-wide active Project
27
+ allowance. `project create NAME --workspace NAME_OR_ID` chooses among multiple Workspaces; one
28
+ Workspace is implicit. Names must match exactly and uniquely, or use an id. Quota refusals show
29
+ the server's reason and recovery links.
30
+
31
+ `project use NAME_OR_ID` saves the selected id beside the login for that origin. `run submit`
32
+ checks this selection against the live authorized list each time; `--project ID` overrides it.
33
+ With `VECTEUR_TOKEN`, runs require `--project ID` and `project use` requires a persistent login.
34
+ Project PATs retain their server-defined scope; listing and creating Projects require an account
35
+ login. `project list --json` preserves `{projects, active_projects, selected_project_id}`.
36
+
37
+ `run events` validates dense SSE sequence and returns the canonical direct record only when its
38
+ embedded events match the stream. Redirects, secret-shaped responses, malformed responses and
39
+ timeouts fail nonzero. `--json` sorts object keys for deterministic machine output.
40
+
41
+ Human run output shows the sealed synthesis, input origins, artifact download commands, the typed
42
+ terminal cause/recovery, and—after `run submit`—the selected Project's UTC-month customer cost using
43
+ the server's display strings. Machine run output remains the unchanged canonical record. Artifact
44
+ downloads use the Project-scoped admitted CAS route, verify the declared length, media type and
45
+ SHA-256, and refuse to overwrite a local path. Use `--project ID` when no Project is selected.
46
+
47
+ `vecteur mcp` serves MCP over stdio using the same credentials and public client. It exposes
48
+ `vecteur_project_get`, `vecteur_run_submit`, `vecteur_run_get` and `vecteur_run_events`.
49
+ An MCP host supplies tool arguments; stdout carries only MCP protocol.
package/bin/vecteur.js CHANGED
@@ -1,106 +1,347 @@
1
1
  #!/usr/bin/env node
2
- import { CliError, VecteurClient, configuration, redact } from "../src/client.js";
3
- import { clearCredentials, credentialsPath, readCredentials, writeCredentials } from "../src/credentials.js";
4
- import { canonicalJson, human, progress } from "../src/format.js";
2
+ import { spawn } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
5
4
 
6
- /**
7
- * Read one line from stdin, with no echo concerns: the token is PIPED, never typed into argv.
8
- *
9
- * `--token vct_…` would put a live credential in `ps`, in `/proc/<pid>/cmdline` for every local
10
- * user, and in the shell history file afterwards. That exposure is not hypothetical here —
11
- * `SHIP-007` on this project's own backlog exists to rotate production credentials that reached
12
- * a command line. A door must not ship a convenient way to repeat it.
13
- */
14
- async function readTokenFromStdin() {
15
- if (process.stdin.isTTY) {
16
- throw new CliError("token_required",
17
- "pipe the personal access token in: `vecteur login --url <origin> < token.txt`");
18
- }
19
- const chunks = [];
20
- for await (const chunk of process.stdin) chunks.push(chunk);
21
- const token = Buffer.concat(chunks).toString("utf8").trim();
22
- if (!token) throw new CliError("token_required", "stdin held no personal access token");
23
- return token;
5
+ import {
6
+ CliError, VecteurClient, configuration, isTokenShaped, redact, requestTimeout, resolveOrigin,
7
+ shouldOpenBrowser, waitForDeviceAuthorization,
8
+ } from "../src/client.js";
9
+ import {
10
+ clearCredentials, credentialsPath, readCredentials, setCurrentOrigin, writeCredentials, selectProject,
11
+ } from "../src/credentials.js";
12
+ import { downloadArtifact } from "../src/download.js";
13
+ import { humanProjects, resolveNamed, resolveRunProject, resolveWorkspace } from "../src/projects.js";
14
+ import { canonicalJson, human, humanCost, progress } from "../src/format.js";
15
+
16
+ function manifest() {
17
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
18
+ }
19
+
20
+ function versionParts(raw) {
21
+ const match = String(raw).match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
22
+ return match ? [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)] : null;
23
+ }
24
+
25
+ function refuseOldNode() {
26
+ const declared = manifest().engines?.node;
27
+ if (typeof declared !== "string" || !declared.startsWith(">=")) {
28
+ throw new CliError("engines_invalid", "package engines.node must be a >= floor");
29
+ }
30
+ const floor = versionParts(declared.slice(2));
31
+ const current = versionParts(process.versions.node);
32
+ if (!floor || !current) {
33
+ throw new CliError("engines_invalid", "package engines.node must be a >= floor");
34
+ }
35
+ const below = current[0] < floor[0]
36
+ || (current[0] === floor[0] && current[1] < floor[1])
37
+ || (current[0] === floor[0] && current[1] === floor[1] && current[2] < floor[2]);
38
+ if (below) {
39
+ throw new CliError("node_version_unsupported",
40
+ `Node ${process.versions.node} is below engines.node ${declared}`);
41
+ }
42
+ }
43
+
44
+ function helpText(version) {
45
+ return `vecteur ${version}
46
+ login | logout | whoami | project list | project create <name> [--workspace <name-or-id>] | project use <name-or-id> | project get <id> | run submit [-p <id>] -a <ask> | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | mcp
47
+ --help --version --json --origin <url> --no-browser --local -p/--project -a/--ask --output <path>
48
+ `;
49
+ }
50
+
51
+ function writeStatus(env = process.env) {
52
+ const saved = readCredentials(env);
53
+ if (saved?.current) {
54
+ process.stdout.write(`${saved.current}\nnext: vecteur whoami\n`);
55
+ } else {
56
+ process.stdout.write("not logged in\nnext: vecteur login\n");
57
+ }
58
+ }
59
+
60
+ function openBrowser(url) {
61
+ const command = process.platform === "darwin" ? "open"
62
+ : process.platform === "win32" ? "cmd" : "xdg-open";
63
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
64
+ try {
65
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
66
+ child.on("error", () => {});
67
+ child.unref();
68
+ } catch {
69
+ // The address is already on screen; failing to spawn a browser is not a failed login.
70
+ }
24
71
  }
25
72
 
26
73
  function parseCommand(argv) {
27
- const json = argv.at(-1) === "--json";
28
- const args = json ? argv.slice(0, -1) : [...argv];
74
+ let args = [...argv];
29
75
  const value = (candidate) => typeof candidate === "string" && candidate.length > 0 &&
30
76
  !candidate.startsWith("-");
31
- if (args.length === 3 && args[0] === "login" && args[1] === "--url" && value(args[2])) {
32
- return { kind: "login", origin: args[2], json };
77
+ const takeFlag = (...names) => {
78
+ const indexes = args.flatMap((arg, i) => names.includes(arg) ? [i] : []);
79
+ if (indexes.length > 1) throw new CliError("usage_invalid", "Invalid command grammar");
80
+ if (indexes.length === 0) return false;
81
+ args = args.filter((_, i) => i !== indexes[0]);
82
+ return true;
83
+ };
84
+ const takeOption = (...names) => {
85
+ const indexes = args.flatMap((arg, i) => names.includes(arg) ? [i] : []);
86
+ if (indexes.length > 1) throw new CliError("usage_invalid", "Invalid command grammar");
87
+ if (indexes.length === 0) return undefined;
88
+ const index = indexes[0];
89
+ if (!value(args[index + 1])) throw new CliError("usage_invalid", "Invalid command grammar");
90
+ const option = args[index + 1];
91
+ args = args.filter((_, i) => i !== index && i !== index + 1);
92
+ return option;
93
+ };
94
+ const version = takeFlag("--version");
95
+ const help = takeFlag("--help");
96
+ const json = takeFlag("--json");
97
+ const origin = takeOption("--origin");
98
+ const noBrowser = takeFlag("--no-browser");
99
+ const local = takeFlag("--local");
100
+ const projectId = takeOption("--project", "-p");
101
+ const ask = takeOption("--ask", "-a");
102
+ const workspace = takeOption("--workspace");
103
+ const output = takeOption("--output");
104
+ if (args.some((arg) => typeof arg === "string" && arg.startsWith("-"))) {
105
+ throw new CliError("usage_invalid", "Invalid command grammar");
106
+ }
107
+ if (version) return { kind: "version" };
108
+ if (help) return { kind: "help" };
109
+ const extra = json || origin !== undefined || noBrowser || local
110
+ || projectId !== undefined || ask !== undefined || workspace !== undefined || output !== undefined;
111
+ if (args.length === 0 && !extra) return { kind: "status" };
112
+ if (args.length === 1 && args[0] === "mcp" && !extra) return { kind: "mcp" };
113
+ if (args[0] === "project" && projectId === undefined && ask === undefined && output === undefined
114
+ && !noBrowser && !local) {
115
+ if (args.length === 3 && args[1] === "create" && value(args[2])) {
116
+ return { kind: "project_create", name: args[2], workspace, origin, json };
117
+ }
118
+ if (workspace === undefined && args.length === 2 && args[1] === "list") {
119
+ return { kind: "project_list", origin, json };
120
+ }
121
+ if (workspace === undefined && args.length === 3 && args[1] === "use" && value(args[2])) {
122
+ return { kind: "project_use", name: args[2], origin, json };
123
+ }
124
+ }
125
+ if (workspace !== undefined) throw new CliError("usage_invalid", "Invalid command grammar");
126
+ if (args.length === 1 && args[0] === "login" && !local
127
+ && projectId === undefined && ask === undefined && output === undefined) {
128
+ return { kind: "login", origin, noBrowser, json };
129
+ }
130
+ if (args.length === 1 && args[0] === "logout" && !noBrowser
131
+ && projectId === undefined && ask === undefined && output === undefined) {
132
+ return { kind: "logout", origin, local, json };
33
133
  }
34
- if (args.length === 1 && args[0] === "logout") return { kind: "logout", json };
35
- if (args.length === 1 && args[0] === "whoami") return { kind: "whoami", json };
36
- if (args.length === 3 && args[0] === "project" && args[1] === "get" && value(args[2])) {
37
- return { kind: "project_get", id: args[2], json };
134
+ if (args.length === 1 && args[0] === "whoami" && !noBrowser && !local
135
+ && projectId === undefined && ask === undefined && output === undefined) {
136
+ return { kind: "whoami", origin, json };
38
137
  }
39
- if (args.length === 6 && args[0] === "run" && args[1] === "submit" &&
40
- args[2] === "--project" && value(args[3]) && args[4] === "--ask" && value(args[5])) {
41
- return { kind: "run_submit", projectId: args[3], ask: args[5], json };
138
+ if (noBrowser || local) throw new CliError("usage_invalid", "Invalid command grammar");
139
+ if (args.length === 3 && args[0] === "project" && args[1] === "get" && value(args[2])
140
+ && projectId === undefined && ask === undefined && output === undefined) {
141
+ return { kind: "project_get", id: args[2], origin, json };
42
142
  }
43
- if (args.length === 3 && args[0] === "run" && args[1] === "get" && value(args[2])) {
44
- return { kind: "run_get", id: args[2], json };
143
+ if (args.length === 2 && args[0] === "run" && args[1] === "submit"
144
+ && (projectId === undefined || value(projectId)) && value(ask) && output === undefined) {
145
+ return { kind: "run_submit", projectId, ask, origin, json };
45
146
  }
46
- if (args.length === 3 && args[0] === "run" && args[1] === "events" && value(args[2])) {
47
- return { kind: "run_events", id: args[2], json };
147
+ if (args.length === 3 && args[0] === "run" && args[1] === "get" && value(args[2])
148
+ && projectId === undefined && ask === undefined && output === undefined) {
149
+ return { kind: "run_get", id: args[2], origin, json };
150
+ }
151
+ if (args.length === 3 && args[0] === "run" && args[1] === "events" && value(args[2])
152
+ && projectId === undefined && ask === undefined && output === undefined) {
153
+ return { kind: "run_events", id: args[2], origin, json };
154
+ }
155
+ if (args.length === 3 && args[0] === "artifact" && args[1] === "get" && value(args[2])
156
+ && ask === undefined) {
157
+ return { kind: "artifact_get", id: args[2], projectId, output, origin, json };
48
158
  }
49
159
  throw new CliError("usage_invalid", "Invalid command grammar");
50
160
  }
51
161
 
162
+ /**
163
+ * `--origin` moves the STORE's current origin, and only the store path may do it.
164
+ *
165
+ * When the credential came from the environment there is no stored login for the named origin
166
+ * to become current, writing to the store is the one thing the CI path must not do, and reading
167
+ * it is how an unreadable file broke a run that never needed one. Both callers ask here rather
168
+ * than each remembering the rule, because one of them already forgot it.
169
+ */
170
+ function rememberOrigin(flagOrigin, origin, env) {
171
+ if (flagOrigin && !env.VECTEUR_TOKEN) setCurrentOrigin(origin, env);
172
+ }
173
+
174
+ function writeLoggedOut(json, path) {
175
+ process.stdout.write(json
176
+ ? canonicalJson({ logged_in: false, credentials: path })
177
+ : `logged out\ncredentials removed: ${path}\n`);
178
+ }
179
+
180
+ async function login(command, env = process.env) {
181
+ const origin = resolveOrigin(env, undefined, command.origin);
182
+ const timeout = requestTimeout(env);
183
+ const client = new VecteurClient({ origin, token: "", timeout });
184
+ const grant = await client.deviceMint();
185
+ const prompt = `Visit ${grant.verification_uri_complete}\nUser code: ${grant.user_code}\n`;
186
+ (command.json ? process.stderr : process.stdout).write(prompt);
187
+ if (shouldOpenBrowser(command.noBrowser, env)) openBrowser(grant.verification_uri_complete);
188
+ const created = await waitForDeviceAuthorization(client, grant);
189
+ if (!isTokenShaped(created.token)) {
190
+ throw new CliError("pat_created_invalid", "Server returned malformed owner data");
191
+ }
192
+ const previous = readCredentials(env)?.logins?.[origin]?.token;
193
+ if (previous) {
194
+ try {
195
+ await new VecteurClient({ origin, token: previous, timeout }).credentialSelfRevoke();
196
+ } catch (error) {
197
+ if (!(error instanceof CliError && error.code === "pat_revoked")) throw error;
198
+ }
199
+ }
200
+ const path = writeCredentials({ origin, token: created.token }, env);
201
+ const account = await new VecteurClient({ origin, token: created.token, timeout }).accountMe();
202
+ process.stdout.write(command.json
203
+ ? canonicalJson({ logged_in: true, origin, credentials: path, account })
204
+ : `logged in to ${origin}\n${renderAccount(account, origin)}credentials: ${path}\n`);
205
+ }
206
+
207
+ async function logout(command, env = process.env) {
208
+ const path = credentialsPath(env);
209
+ let saved;
210
+ try {
211
+ saved = readCredentials(env);
212
+ } catch (error) {
213
+ if (!command.local) throw error;
214
+ clearCredentials(env);
215
+ writeLoggedOut(command.json, path);
216
+ return;
217
+ }
218
+ if (!saved) {
219
+ writeLoggedOut(command.json, path);
220
+ return;
221
+ }
222
+ const origin = resolveOrigin(env, saved, command.origin);
223
+ const token = saved.logins[origin]?.token;
224
+ if (!token) {
225
+ throw new CliError("origin_mismatch",
226
+ `the stored login is for ${saved.current} and ${command.origin !== undefined ? "--origin" : "VECTEUR_BASE_URL"} names `
227
+ + `${origin} — set VECTEUR_TOKEN for that origin, or log in to it`);
228
+ }
229
+ if (!command.local) {
230
+ try {
231
+ await new VecteurClient({ origin, token, timeout: requestTimeout(env) }).credentialSelfRevoke();
232
+ } catch (error) {
233
+ if (error instanceof CliError && (error.code === "request_failed" || error.code === "request_timeout")) {
234
+ throw new CliError("logout_offline",
235
+ "logout requires the origin; pass --local to delete only this machine's store");
236
+ }
237
+ throw error;
238
+ }
239
+ }
240
+ writeLoggedOut(command.json, clearCredentials(env, origin));
241
+ }
242
+
243
+ function renderAccount(account, origin) {
244
+ return `${account.display_name} <${account.email}> (${account.id}) at ${origin}\n`;
245
+ }
246
+
247
+ async function whoami(command, env = process.env) {
248
+ const config = configuration(env, undefined, command.origin);
249
+ rememberOrigin(command.origin, config.origin, env);
250
+ const account = await new VecteurClient(config).accountMe();
251
+ process.stdout.write(command.json
252
+ ? canonicalJson(account)
253
+ : renderAccount(account, config.origin));
254
+ }
255
+
52
256
  async function main() {
257
+ refuseOldNode();
53
258
  const command = parseCommand(process.argv.slice(2));
259
+ if (command.kind === "version") {
260
+ process.stdout.write(`vecteur ${manifest().version}\n`);
261
+ return;
262
+ }
263
+ if (command.kind === "help") {
264
+ process.stdout.write(helpText(manifest().version));
265
+ return;
266
+ }
267
+ if (command.kind === "status") {
268
+ writeStatus();
269
+ return;
270
+ }
271
+ if (command.kind === "mcp") {
272
+ const { serveVecteurMcp } = await import("../src/mcp.js");
273
+ serveVecteurMcp();
274
+ return;
275
+ }
54
276
 
55
- // The login commands run BEFORE a client is constructed. Constructing one resolves a
56
- // configuration, and requiring a valid login in order to log in is the shape of bug that
57
- // makes a first run impossible.
277
+ // Login and logout run BEFORE a client is constructed from configuration(): requiring a
278
+ // valid login in order to log in (or to delete a corrupt store locally) is the shape of
279
+ // bug that makes a first run or a recovery impossible. VECTEUR_TOKEN is the CI path and
280
+ // is never written or revoked here.
58
281
  if (command.kind === "login") {
59
- const token = await readTokenFromStdin();
60
- // Shape-check HERE so the refusal names what is wrong with what was just piped in. Falling
61
- // straight through to `configuration` reported "no login found: run `vecteur login`" to a
62
- // user in the middle of running exactly that.
63
- if (!/^vct_[A-Za-z0-9_-]{43}$/.test(token)) {
64
- throw new CliError("token_invalid",
65
- `stdin held ${token.length} characters; a personal access token is \`vct_\` and 43 more`);
66
- }
67
- // Then through the SAME function every command resolves through, so an origin this CLI
68
- // would later refuse is refused now, while the user is still holding the token — not on
69
- // their next command with the credential already on disk.
70
- const checked = configuration({ VECTEUR_TOKEN: token, VECTEUR_BASE_URL: command.origin }, null);
71
- const path = writeCredentials({ origin: checked.origin, token }, process.env);
72
- process.stdout.write(command.json
73
- ? canonicalJson({ logged_in: true, origin: checked.origin, credentials: path })
74
- : `logged in to ${checked.origin}\ncredentials: ${path}\n`);
282
+ await login(command);
75
283
  return;
76
284
  }
77
285
  if (command.kind === "logout") {
78
- const path = clearCredentials(process.env);
79
- process.stdout.write(command.json
80
- ? canonicalJson({ logged_in: false, credentials: path })
81
- : `logged out\ncredentials removed: ${path}\n`);
286
+ await logout(command);
82
287
  return;
83
288
  }
84
289
  if (command.kind === "whoami") {
85
- const saved = readCredentials(process.env);
86
- // NEVER the token, not even a prefix. A door that prints part of a credential teaches the
87
- // habit of pasting it somewhere, and a prefix is enough to confirm a guess.
88
- process.stdout.write(command.json
89
- ? canonicalJson({ logged_in: Boolean(saved), origin: saved?.origin ?? null })
90
- : saved ? `logged in to ${saved.origin}\n` : "not logged in\n");
91
- process.exitCode = saved ? 0 : 1;
290
+ await whoami(command);
92
291
  return;
93
292
  }
94
293
 
95
- const client = new VecteurClient();
294
+ const config = configuration(process.env, undefined, command.origin);
295
+ rememberOrigin(command.origin, config.origin, process.env);
296
+ const client = new VecteurClient(config);
96
297
  const onEvent = command.json ? null : (event) => process.stderr.write(progress(event));
97
298
  let value;
98
- if (command.kind === "project_get") value = await client.projectGet(command.id);
299
+ let renderOptions = {};
300
+ if (command.kind === "project_list") {
301
+ const [account, projects] = await Promise.all([client.accountMe(), client.projectList()]);
302
+ value = { projects, active_projects: account.entitlements.active_projects,
303
+ selected_project_id: config.projectId ?? null };
304
+ process.stdout.write(command.json ? canonicalJson(value) : humanProjects(value));
305
+ return;
306
+ } else if (command.kind === "project_create") {
307
+ const account = await client.accountMe();
308
+ value = await client.projectCreate(command.name, resolveWorkspace(account, command.workspace));
309
+ } else if (command.kind === "project_use") {
310
+ if (process.env.VECTEUR_TOKEN) throw new CliError("persistent_login_required",
311
+ "project use requires a persistent login; unset VECTEUR_TOKEN and run `vecteur login`");
312
+ value = resolveNamed(await client.projectList(), command.name, "project");
313
+ selectProject(config.origin, value.id);
314
+ } else if (command.kind === "project_get") value = await client.projectGet(command.id);
315
+ else if (command.kind === "artifact_get") {
316
+ const projectId = await resolveRunProject(client, command.projectId);
317
+ value = await downloadArtifact(client, command.id, projectId, command.output);
318
+ process.stdout.write(command.json ? canonicalJson(value)
319
+ : `Downloaded ${value.bytes} bytes to ${value.path} (${value.content_type})\n`);
320
+ return;
321
+ }
99
322
  else if (command.kind === "run_submit") {
100
- value = await client.runSubmit(command.projectId, command.ask, onEvent);
323
+ const projectId = await resolveRunProject(client, command.projectId);
324
+ value = await client.runSubmit(projectId, command.ask, onEvent);
325
+ if (!command.json) {
326
+ process.stdout.write(human(value, { projectId }));
327
+ try {
328
+ const cost = await client.customerCost(projectId);
329
+ process.stdout.write(`\n${humanCost(cost)}`);
330
+ } catch (error) {
331
+ if (error instanceof CliError && error.code === "pat_expired") {
332
+ throw new CliError("pat_expired",
333
+ `credential expired after Run ${value.run_id} was admitted; next: vecteur login; then: vecteur run get ${value.run_id}`);
334
+ }
335
+ throw error;
336
+ }
337
+ return;
338
+ }
101
339
  } else if (command.kind === "run_get") value = await client.runGet(command.id);
102
340
  else value = await client.runEvents(command.id, onEvent);
103
- process.stdout.write(command.json ? canonicalJson(value) : human(value));
341
+ if (!command.json && !renderOptions.projectId && config.projectId) {
342
+ renderOptions = { projectId: config.projectId };
343
+ }
344
+ process.stdout.write(command.json ? canonicalJson(value) : human(value, renderOptions));
104
345
  }
105
346
 
106
347
  main().catch((error) => {
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@vecteur/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "bin": {
6
- "cli": "bin/vecteur.js",
7
- "vecteur": "bin/vecteur.js",
8
- "vecteur-mcp": "bin/vecteur-mcp.js"
6
+ "vecteur": "bin/vecteur.js"
9
7
  },
10
8
  "files": [
11
- "bin/vecteur-mcp.js",
9
+ "README.md",
12
10
  "bin/vecteur.js",
13
11
  "src/client.js",
14
12
  "src/contract.js",
15
13
  "src/credentials.js",
14
+ "src/download.js",
16
15
  "src/errors.js",
17
16
  "src/format.js",
18
- "src/mcp.js"
17
+ "src/mcp.js",
18
+ "src/projects.js"
19
19
  ],
20
20
  "scripts": {
21
21
  "test": "node --test tests/*.test.js",