@vecteur/cli 0.2.4 → 0.3.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/bin/vecteur-mcp.js +29 -0
- package/bin/vecteur.js +110 -0
- package/package.json +25 -47
- package/src/client.js +399 -0
- package/src/contract.js +12 -0
- package/src/credentials.js +75 -0
- package/src/errors.js +14 -0
- package/src/format.js +91 -0
- package/src/mcp.js +98 -0
- package/LICENSE +0 -21
- package/README.md +0 -70
- package/dist/api.js +0 -89
- package/dist/commands/ask.js +0 -44
- package/dist/commands/auth.js +0 -76
- package/dist/commands/chat.js +0 -126
- package/dist/commands/projects.js +0 -20
- package/dist/config.js +0 -92
- package/dist/index.js +0 -82
- package/dist/runner.js +0 -175
- package/dist/session.js +0 -138
- package/dist/ui/App.js +0 -175
- package/dist/ui/Header.js +0 -7
- package/dist/ui/Prompt.js +0 -7
- package/dist/ui/RunStatus.js +0 -12
- package/dist/ui/logo.js +0 -24
- package/dist/ui/markdown.js +0 -38
- package/dist/update.js +0 -75
- package/dist/version.js +0 -6
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ONE PERSISTENT LOGIN — SRD 06's release bar for this door, beside "the same product from any
|
|
9
|
+
* working directory".
|
|
10
|
+
*
|
|
11
|
+
* Before this, `configuration()` read `VECTEUR_TOKEN` from the environment and nothing else, so
|
|
12
|
+
* "logged in" lasted exactly as long as one shell. Every new terminal, every cron line and every
|
|
13
|
+
* `cd` into a project began by pasting a credential again — which is not a login, and it is the
|
|
14
|
+
* habit that puts secrets in shell history.
|
|
15
|
+
*
|
|
16
|
+
* THE TOKEN IS NEVER AN ARGUMENT. `vecteur login` reads it from stdin. A credential passed as
|
|
17
|
+
* `--token vct_…` is visible in `ps`, in `/proc/<pid>/cmdline` to every local user, and in the
|
|
18
|
+
* shell history file afterwards — the exact exposure `SHIP-007` exists to rotate away in this
|
|
19
|
+
* same product. A door that ships a convenient way to make that mistake has shipped the mistake.
|
|
20
|
+
*
|
|
21
|
+
* The store lives OUTSIDE every git tree, one file, 0600 — the same shape `scripts/with-secrets.py`
|
|
22
|
+
* holds the fleet's own store in, for the same reason.
|
|
23
|
+
*/
|
|
24
|
+
const FILE_MODE = 0o600;
|
|
25
|
+
const DIRECTORY_MODE = 0o700;
|
|
26
|
+
|
|
27
|
+
/** `$XDG_CONFIG_HOME/vecteur/credentials.json`, or `~/.config/vecteur/credentials.json`. */
|
|
28
|
+
export function credentialsPath(env = process.env) {
|
|
29
|
+
const base = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.startsWith("/")
|
|
30
|
+
? env.XDG_CONFIG_HOME
|
|
31
|
+
: join(env.HOME || homedir(), ".config");
|
|
32
|
+
return join(base, "vecteur", "credentials.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `{ origin, token }` as stored, or `null` when nobody has logged in on this machine. */
|
|
36
|
+
export function readCredentials(env = process.env) {
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = readFileSync(credentialsPath(env), "utf8");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
// A corrupt store is a typed refusal, not a silent fall back to "not logged in": the second
|
|
48
|
+
// reads as "run login again" and loses the fact that a credential IS there and unreadable.
|
|
49
|
+
throw new CliError("credentials_unreadable",
|
|
50
|
+
`${credentialsPath(env)} is not readable JSON — run \`vecteur logout\` and log in again`);
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || typeof parsed !== "object" ||
|
|
53
|
+
typeof parsed.origin !== "string" || typeof parsed.token !== "string") {
|
|
54
|
+
throw new CliError("credentials_unreadable",
|
|
55
|
+
`${credentialsPath(env)} does not hold an origin and a token`);
|
|
56
|
+
}
|
|
57
|
+
return { origin: parsed.origin, token: parsed.token };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function writeCredentials({ origin, token }, env = process.env) {
|
|
61
|
+
const path = credentialsPath(env);
|
|
62
|
+
mkdirSync(dirname(path), { recursive: true, mode: DIRECTORY_MODE });
|
|
63
|
+
// Written 0600 AT CREATION, not chmod'ed after: between an 0644 create and a chmod there is a
|
|
64
|
+
// window in which any local user can read the token, and on a shared machine that window is
|
|
65
|
+
// the whole exposure.
|
|
66
|
+
writeFileSync(path, `${JSON.stringify({ origin, token }, null, 2)}\n`, { mode: FILE_MODE });
|
|
67
|
+
chmodSync(path, FILE_MODE); // an existing file keeps its old mode through writeFileSync
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function clearCredentials(env = process.env) {
|
|
72
|
+
const path = credentialsPath(env);
|
|
73
|
+
rmSync(path, { force: true });
|
|
74
|
+
return path;
|
|
75
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's one typed error, in its own module.
|
|
3
|
+
*
|
|
4
|
+
* It used to live in `client.js`, which `credentials.js` had to import for it while `client.js`
|
|
5
|
+
* needed `credentials.js` to resolve a stored login — a cycle whose first workaround was a
|
|
6
|
+
* function reached through `globalThis`. An error type is not part of either concern; it is
|
|
7
|
+
* shared vocabulary, so it owns a file and neither side reaches through the other.
|
|
8
|
+
*/
|
|
9
|
+
export class CliError extends Error {
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ORIGIN_KINDS, ORIGIN_WORD } from "./contract.js";
|
|
2
|
+
|
|
3
|
+
export function canonicalJson(value) {
|
|
4
|
+
validateNumbers(value);
|
|
5
|
+
return `${JSON.stringify(sortValue(value))}\n`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function keyOrder(left, right) {
|
|
9
|
+
const a = Array.from(left, (character) => character.codePointAt(0));
|
|
10
|
+
const b = Array.from(right, (character) => character.codePointAt(0));
|
|
11
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
12
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
13
|
+
}
|
|
14
|
+
return a.length - b.length;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateNumbers(value) {
|
|
18
|
+
if (typeof value === "number" && (!Number.isFinite(value) ||
|
|
19
|
+
(Number.isInteger(value) && !Number.isSafeInteger(value)))) {
|
|
20
|
+
throw new TypeError("canonical JSON contains an unsafe number");
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) value.forEach(validateNumbers);
|
|
23
|
+
else if (value && typeof value === "object") Object.values(value).forEach(validateNumbers);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sortValue(value) {
|
|
27
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
28
|
+
if (value && typeof value === "object") {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.keys(value).sort(keyOrder).map((key) => [key, sortValue(value[key])]),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ONE CLASSIFICATION OF THE EVIDENCE A RUN USED, SPOKEN BY THIS DOOR TOO.
|
|
38
|
+
*
|
|
39
|
+
* PRD-UX-16: every load-bearing value in a step's inputs shows its origin — your brief, assumed,
|
|
40
|
+
* derived, your file, catalogue, default — and a value the program attributed to nothing renders
|
|
41
|
+
* an explicit marker, NEVER a blank. PRD-CLI-9: every door sees the same state. The web has said
|
|
42
|
+
* this for a while; this door printed `[3] step_finished` and nothing else, so the same run
|
|
43
|
+
* answered "where did this number come from?" on one surface and stayed silent on the other.
|
|
44
|
+
*
|
|
45
|
+
* The words come from `contract.js`, generated from `frontend/src/run/provenance.ts` — the
|
|
46
|
+
* web's own owner — so the two doors cannot drift into different vocabularies without the
|
|
47
|
+
* contract gate going red.
|
|
48
|
+
*/
|
|
49
|
+
export function originWord(origin) {
|
|
50
|
+
if (!origin || typeof origin !== "object" || typeof origin.kind !== "string") {
|
|
51
|
+
return ORIGIN_WORD.unattributed;
|
|
52
|
+
}
|
|
53
|
+
// A kind the engine invented and this build does not know is NOT silently "unstated": that
|
|
54
|
+
// would report an attribution failure where there is a version skew, and the two read
|
|
55
|
+
// completely differently to whoever is trying to trust the number.
|
|
56
|
+
if (!ORIGIN_KINDS.includes(origin.kind)) return `unknown origin \`${origin.kind}\``;
|
|
57
|
+
return ORIGIN_WORD[origin.kind];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `name = value unit (origin word, ref)` — one line per input, in wire order. */
|
|
61
|
+
export function inputLines(inputs) {
|
|
62
|
+
if (!Array.isArray(inputs)) return [];
|
|
63
|
+
return inputs.map((row) => {
|
|
64
|
+
const unit = typeof row?.unit === "string" && row.unit ? ` ${row.unit}` : "";
|
|
65
|
+
const word = originWord(row?.origin);
|
|
66
|
+
// The REF is what makes the word actionable — "your file" is a category, "your file
|
|
67
|
+
// (orbit.csv)" is an answer. It is omitted only when the origin does not carry one.
|
|
68
|
+
const ref = typeof row?.origin?.ref === "string" && row.origin.ref
|
|
69
|
+
? ` ${row.origin.ref}` : "";
|
|
70
|
+
return ` ${row?.name ?? "?"} = ${JSON.stringify(row?.value)}${unit} (${word}${ref})`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function human(value) {
|
|
75
|
+
if (value && typeof value === "object" && typeof value.run_id === "string") {
|
|
76
|
+
return `Run ${value.run_id}: ${value.terminal ?? value.state_hash ?? "accepted"}\n`;
|
|
77
|
+
}
|
|
78
|
+
if (value && typeof value === "object" && typeof value.name === "string") {
|
|
79
|
+
return `${value.name} (${value.id})\n`;
|
|
80
|
+
}
|
|
81
|
+
return canonicalJson(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function progress(event) {
|
|
85
|
+
const head = `[${event.seq}] ${event.kind}\n`;
|
|
86
|
+
// A finished step is where a run's evidence becomes visible: its inputs each carry an
|
|
87
|
+
// origin. Every other kind keeps the one-line shape.
|
|
88
|
+
if (event?.kind !== "step_finished") return head;
|
|
89
|
+
const lines = inputLines(event?.payload?.step?.inputs ?? event?.payload?.inputs);
|
|
90
|
+
return lines.length ? `${head}${lines.join("\n")}\n` : head;
|
|
91
|
+
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
|
|
4
|
+
import { CliError, VecteurClient } from "./client.js";
|
|
5
|
+
import { EVENT_KINDS } from "./contract.js";
|
|
6
|
+
import { canonicalJson } from "./format.js";
|
|
7
|
+
|
|
8
|
+
const projectInput = z.object({ project_id: z.string().min(1) }).strict();
|
|
9
|
+
const submitInput = z.object({
|
|
10
|
+
project_id: z.string().min(1),
|
|
11
|
+
ask: z.string().min(1),
|
|
12
|
+
}).strict();
|
|
13
|
+
const runInput = z.object({ run_id: z.string().min(1) }).strict();
|
|
14
|
+
const eventKind = z.enum(EVENT_KINDS);
|
|
15
|
+
const eventOutput = z.object({
|
|
16
|
+
run_id: z.string().min(1), seq: z.number().int().safe().nonnegative(),
|
|
17
|
+
kind: eventKind,
|
|
18
|
+
payload: z.record(z.string(), z.json()),
|
|
19
|
+
}).strict();
|
|
20
|
+
const memberOutput = z.object({
|
|
21
|
+
user_id: z.string().min(1), email: z.string().min(1), display_name: z.string().min(1),
|
|
22
|
+
role: z.enum(["OWNER", "ADMIN", "EDITOR", "VIEWER"]),
|
|
23
|
+
}).strict();
|
|
24
|
+
const projectOutput = z.object({
|
|
25
|
+
id: z.string().min(1), name: z.string().min(1), org_id: z.string().min(1),
|
|
26
|
+
workspace_id: z.string().min(1), role: z.enum(["OWNER", "ADMIN", "EDITOR", "VIEWER"]),
|
|
27
|
+
role_source: z.enum(["workspace", "project"]), members: z.array(memberOutput),
|
|
28
|
+
created_at: z.number().int().safe(), updated_at: z.number().int().safe(),
|
|
29
|
+
}).strict();
|
|
30
|
+
const admittedOutput = z.object({
|
|
31
|
+
run_id: z.string().min(1), state_hash: z.string().min(1), result_hash: z.string().min(1),
|
|
32
|
+
profile: z.string().min(1),
|
|
33
|
+
terminal: z.enum(["ok", "partial", "blocked", "refused", "infeasible_physics", "capability_gap"]),
|
|
34
|
+
usage: z.object({
|
|
35
|
+
state: z.enum(["recorded", "usage_unavailable"]), provider: z.string().min(1),
|
|
36
|
+
model_id: z.string().min(1), input_tokens: z.number().int().safe().nonnegative().nullable(),
|
|
37
|
+
output_tokens: z.number().int().safe().nonnegative().nullable(),
|
|
38
|
+
cost_usd: z.number().finite().nonnegative().nullable(),
|
|
39
|
+
}).strict(),
|
|
40
|
+
events: z.array(eventOutput),
|
|
41
|
+
}).strict();
|
|
42
|
+
|
|
43
|
+
function success(value) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: canonicalJson(value) }],
|
|
46
|
+
structuredContent: value,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function failure(error) {
|
|
51
|
+
const code = error instanceof CliError && /^[a-z][a-z0-9_]{0,63}$/.test(error.code)
|
|
52
|
+
? error.code : "internal_error";
|
|
53
|
+
return {
|
|
54
|
+
isError: true,
|
|
55
|
+
content: [{ type: "text", text: canonicalJson({ error: { code } }) }],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tool(server, name, description, inputSchema, outputSchema, call) {
|
|
60
|
+
server.registerTool(name, { description, inputSchema, outputSchema }, async (input, context) => {
|
|
61
|
+
try {
|
|
62
|
+
return success(await call(input, context));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return failure(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createVecteurMcpServer(client = new VecteurClient(), shutdownSignal = undefined) {
|
|
70
|
+
const server = new McpServer({ name: "vecteur", version: "0.1.0" });
|
|
71
|
+
const signal = (context) => shutdownSignal
|
|
72
|
+
? AbortSignal.any([context.mcpReq.signal, shutdownSignal]) : context.mcpReq.signal;
|
|
73
|
+
|
|
74
|
+
tool(server, "vecteur_project_get", "Get one PAT-bound Vecteur project.", projectInput, projectOutput,
|
|
75
|
+
({ project_id: projectId }, context) => client.projectGet(projectId, signal(context)));
|
|
76
|
+
|
|
77
|
+
tool(server, "vecteur_run_submit", "Submit one project turn and return its terminal record.",
|
|
78
|
+
submitInput, admittedOutput, async ({ project_id: projectId, ask }, context) => {
|
|
79
|
+
const progressToken = context.mcpReq._meta?.progressToken;
|
|
80
|
+
const onEvent = progressToken === undefined ? null : (event) => context.mcpReq.notify({
|
|
81
|
+
method: "notifications/progress",
|
|
82
|
+
params: {
|
|
83
|
+
progressToken,
|
|
84
|
+
progress: event.seq + 1,
|
|
85
|
+
message: event.kind,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
return client.runSubmit(projectId, ask, onEvent, signal(context));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
tool(server, "vecteur_run_get", "Get one terminal Vecteur run record.", runInput, admittedOutput,
|
|
92
|
+
({ run_id: runId }, context) => client.runGet(runId, signal(context)));
|
|
93
|
+
|
|
94
|
+
tool(server, "vecteur_run_events", "Validate run events against its direct record.", runInput,
|
|
95
|
+
admittedOutput, ({ run_id: runId }, context) => client.runEvents(runId, null, signal(context)));
|
|
96
|
+
|
|
97
|
+
return server;
|
|
98
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Vecteur
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# Vecteur CLI
|
|
2
|
-
|
|
3
|
-
Run space-mission-engineering queries from your terminal — an interactive agent that works on
|
|
4
|
-
your local files. The `vecteur` CLI is a **thin, open-source client**: the agent, physics
|
|
5
|
-
libraries, and models run on Vecteur's servers, so nothing proprietary ships in this package.
|
|
6
|
-
|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
npm install -g @vecteur/cli
|
|
11
|
-
vecteur login # opens your browser to approve this device
|
|
12
|
-
cd my-mission/ # this directory becomes your workspace
|
|
13
|
-
vecteur # start an interactive session
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install -g @vecteur/cli
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Standalone binaries (no Node needed) and `brew` / `winget` / `curl` installers are published with
|
|
23
|
-
each release — see the [releases page](https://github.com/vecteurspace/vecteur-cli/releases).
|
|
24
|
-
|
|
25
|
-
## Use
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
vecteur # interactive session; the current directory is your workspace
|
|
29
|
-
vecteur ask "period at 550 km circular" --project <id>
|
|
30
|
-
vecteur ask "explain this" --file ./mission.md # attach local files as context
|
|
31
|
-
vecteur projects # your projects (same as the web app at vecteur.space)
|
|
32
|
-
vecteur whoami # who you're signed in as
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Inside the interactive session: type naturally, use `@path` to attach a local file, and
|
|
36
|
-
`/help` for commands (`/files`, `/new`, `/open`, `/exit`).
|
|
37
|
-
|
|
38
|
-
## Use it from an AI assistant (MCP)
|
|
39
|
-
|
|
40
|
-
No install needed — connect Vecteur to Claude, Cursor, or any MCP-capable assistant:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
claude mcp add --transport http vecteur https://api.vecteur.space/mcp
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Configuration
|
|
47
|
-
|
|
48
|
-
- `VECTEUR_API_URL` — point at a different Vecteur instance (default `https://api.vecteur.space`).
|
|
49
|
-
- `VECTEUR_TOKEN` — supply an access key non-interactively (CI).
|
|
50
|
-
|
|
51
|
-
Credentials are stored at `~/.config/vecteur/config.json` (owner-only).
|
|
52
|
-
|
|
53
|
-
## What this package contains (and doesn't)
|
|
54
|
-
|
|
55
|
-
This is a client. It sends your queries (and any files you explicitly attach) to the Vecteur API
|
|
56
|
-
and streams results back. It contains **no** agent logic, physics code, prompts, or API keys — a
|
|
57
|
-
CI gate (`.github/workflows/ci.yml`) scans every change and the published tarball ships only
|
|
58
|
-
compiled client code + LICENSE + README.
|
|
59
|
-
|
|
60
|
-
## Development
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
npm install
|
|
64
|
-
npm run build # tsc typecheck + emit to dist/
|
|
65
|
-
npm run typecheck
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## License
|
|
69
|
-
|
|
70
|
-
MIT — see [LICENSE](./LICENSE).
|
package/dist/api.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin HTTP client for the Vecteur backend. Bearer auth (PAT or JWT — the CLI is
|
|
3
|
-
* auth-method-agnostic so it works with whatever credential the platform issues).
|
|
4
|
-
* Structured errors so commands can print actionable messages and set exit codes.
|
|
5
|
-
*/
|
|
6
|
-
import { loadConfig } from "./config.js";
|
|
7
|
-
import { VERSION } from "./version.js";
|
|
8
|
-
export class ApiError extends Error {
|
|
9
|
-
status;
|
|
10
|
-
body;
|
|
11
|
-
retryAfter;
|
|
12
|
-
constructor(status, message, body, retryAfter) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.status = status;
|
|
15
|
-
this.body = body;
|
|
16
|
-
this.retryAfter = retryAfter;
|
|
17
|
-
this.name = "ApiError";
|
|
18
|
-
}
|
|
19
|
-
/** A human, actionable line for the terminal. */
|
|
20
|
-
hint() {
|
|
21
|
-
switch (this.status) {
|
|
22
|
-
case 401:
|
|
23
|
-
return "Not authenticated. Run `vecteur login` (or set VECTEUR_TOKEN).";
|
|
24
|
-
case 403:
|
|
25
|
-
// Prefer the server's actual reason (e.g. "Project limit reached…", a plan gate);
|
|
26
|
-
// fall back to the scope hint only when the server gave no detail (bare status text).
|
|
27
|
-
return this.message && this.message.toLowerCase() !== "forbidden"
|
|
28
|
-
? this.message
|
|
29
|
-
: "Forbidden — your token may lack the required scope for this action.";
|
|
30
|
-
case 429:
|
|
31
|
-
return `Rate limit / quota exceeded${this.retryAfter ? ` — retry in ${this.retryAfter}s` : ""}.`;
|
|
32
|
-
case 426:
|
|
33
|
-
return "This CLI is out of date. Run `vecteur update`.";
|
|
34
|
-
default:
|
|
35
|
-
return this.message;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function apiBase() {
|
|
40
|
-
return loadConfig().apiUrl.replace(/\/+$/, "");
|
|
41
|
-
}
|
|
42
|
-
export async function api(path, opts = {}) {
|
|
43
|
-
const cfg = loadConfig();
|
|
44
|
-
const token = opts.token ?? cfg.token;
|
|
45
|
-
const url = new URL(apiBase() + path);
|
|
46
|
-
if (opts.query) {
|
|
47
|
-
for (const [k, v] of Object.entries(opts.query)) {
|
|
48
|
-
if (v !== undefined)
|
|
49
|
-
url.searchParams.set(k, String(v));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const headers = {
|
|
53
|
-
Accept: "application/json",
|
|
54
|
-
"User-Agent": `vecteur-cli/${VERSION}`, // lets the server 426-gate clients below a min version
|
|
55
|
-
};
|
|
56
|
-
if (token)
|
|
57
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
58
|
-
if (opts.body !== undefined)
|
|
59
|
-
headers["Content-Type"] = "application/json";
|
|
60
|
-
let res;
|
|
61
|
-
try {
|
|
62
|
-
res = await fetch(url, {
|
|
63
|
-
method: opts.method ?? "GET",
|
|
64
|
-
headers,
|
|
65
|
-
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
throw new ApiError(0, `Network error reaching ${apiBase()} — is the API URL correct and are you online? (${e.message})`);
|
|
70
|
-
}
|
|
71
|
-
const text = await res.text();
|
|
72
|
-
let parsed = undefined;
|
|
73
|
-
if (text) {
|
|
74
|
-
try {
|
|
75
|
-
parsed = JSON.parse(text);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
parsed = text;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (!res.ok) {
|
|
82
|
-
const retryAfter = Number(res.headers.get("retry-after")) || undefined;
|
|
83
|
-
const detail = (parsed && typeof parsed === "object" && "detail" in parsed
|
|
84
|
-
? String(parsed.detail)
|
|
85
|
-
: undefined) ?? res.statusText;
|
|
86
|
-
throw new ApiError(res.status, detail, parsed, retryAfter);
|
|
87
|
-
}
|
|
88
|
-
return parsed;
|
|
89
|
-
}
|
package/dist/commands/ask.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ask: one-shot — run an engineering query and stream the answer. A thin wrapper over the
|
|
3
|
-
* shared runner (same streaming path the interactive `chat` REPL uses).
|
|
4
|
-
*/
|
|
5
|
-
import { loadConfig } from "../config.js";
|
|
6
|
-
import { buildLocalContextQuery, openBrowser, streamTurn, webBase } from "../runner.js";
|
|
7
|
-
export async function ask(query, opts) {
|
|
8
|
-
const cfg = loadConfig();
|
|
9
|
-
if (!cfg.token) {
|
|
10
|
-
console.error("Not logged in. Run `vecteur login` first.");
|
|
11
|
-
process.exitCode = 1;
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
if (!opts.project) {
|
|
15
|
-
console.error('A project is required: `vecteur ask "…" --project <id>` (or use `vecteur` for an interactive session).');
|
|
16
|
-
process.exitCode = 1;
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const effectiveQuery = buildLocalContextQuery(query, opts.file);
|
|
20
|
-
if (!opts.json)
|
|
21
|
-
console.error("▸ running…");
|
|
22
|
-
const res = await streamTurn({
|
|
23
|
-
project: opts.project,
|
|
24
|
-
query: effectiveQuery,
|
|
25
|
-
followUp: opts.followUp,
|
|
26
|
-
agent: opts.agent,
|
|
27
|
-
json: opts.json,
|
|
28
|
-
onStep: opts.json ? undefined : (s) => process.stderr.write(` · ${s}\n`),
|
|
29
|
-
});
|
|
30
|
-
if (opts.json)
|
|
31
|
-
return;
|
|
32
|
-
if (res.failed) {
|
|
33
|
-
console.error(`✗ ${res.failed}`);
|
|
34
|
-
process.exitCode = 1;
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
console.log("\n" + (res.answer ?? "(no answer)"));
|
|
38
|
-
const link = `${webBase()}/projects/${opts.project}`;
|
|
39
|
-
console.log(`\nView the full run (globe, tables, provenance): ${link}`);
|
|
40
|
-
if (res.sawVisual)
|
|
41
|
-
console.log("(this run produced visual artifacts best seen in the web view)");
|
|
42
|
-
if (opts.open)
|
|
43
|
-
void openBrowser(link);
|
|
44
|
-
}
|
package/dist/commands/auth.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** login / logout / whoami. */
|
|
2
|
-
import { api, ApiError } from "../api.js";
|
|
3
|
-
import { clearToken, loadConfig, saveToken } from "../config.js";
|
|
4
|
-
import { openBrowser } from "../runner.js";
|
|
5
|
-
export async function login(opts) {
|
|
6
|
-
if (opts.token) {
|
|
7
|
-
// Verify the token works before persisting.
|
|
8
|
-
await api("/api/v1/auth/me", { token: opts.token });
|
|
9
|
-
saveToken(opts.token, opts.apiUrl);
|
|
10
|
-
console.log("Logged in (token stored).");
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
if (opts.email && opts.password) {
|
|
14
|
-
const res = await api("/api/v1/auth/login", {
|
|
15
|
-
method: "POST",
|
|
16
|
-
body: { email: opts.email, password: opts.password },
|
|
17
|
-
});
|
|
18
|
-
saveToken(res.access_token, opts.apiUrl);
|
|
19
|
-
console.log(`Logged in as ${res.user?.email ?? opts.email}.`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
// Default: browser device flow (RFC 8628). No password touches the terminal.
|
|
23
|
-
await deviceLogin(opts.apiUrl, opts.noBrowser);
|
|
24
|
-
}
|
|
25
|
-
export function formatDeviceInstructions(verification_uri, user_code) {
|
|
26
|
-
return `To authorize this device, open:
|
|
27
|
-
${verification_uri}?code=${user_code}
|
|
28
|
-
or go to ${verification_uri} and enter the code: ${user_code}`;
|
|
29
|
-
}
|
|
30
|
-
async function deviceLogin(apiUrl, noBrowser = false) {
|
|
31
|
-
const dc = await api("/api/v1/auth/device/code", { method: "POST", body: {} });
|
|
32
|
-
console.log(`\n${formatDeviceInstructions(dc.verification_uri, dc.user_code)}\n`);
|
|
33
|
-
if (!noBrowser) {
|
|
34
|
-
await openBrowser(`${dc.verification_uri}?code=${dc.user_code}`);
|
|
35
|
-
}
|
|
36
|
-
console.log(`Waiting for approval (expires in ${Math.round(dc.expires_in / 60)} min)…`);
|
|
37
|
-
const deadline = Date.now() + dc.expires_in * 1000;
|
|
38
|
-
const intervalMs = Math.max(2, dc.interval ?? 3) * 1000;
|
|
39
|
-
while (Date.now() < deadline) {
|
|
40
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
41
|
-
try {
|
|
42
|
-
const res = await api("/api/v1/auth/device/token", {
|
|
43
|
-
method: "POST",
|
|
44
|
-
body: { device_code: dc.device_code },
|
|
45
|
-
});
|
|
46
|
-
saveToken(res.token, apiUrl);
|
|
47
|
-
console.log("Approved — logged in.");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
if (e instanceof ApiError && e.status === 428)
|
|
52
|
-
continue; // authorization_pending
|
|
53
|
-
throw e;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
throw new ApiError(408, "Device authorization timed out. Run `vecteur login` again.");
|
|
57
|
-
}
|
|
58
|
-
export function logout() {
|
|
59
|
-
clearToken();
|
|
60
|
-
console.log("Logged out (local token cleared).");
|
|
61
|
-
}
|
|
62
|
-
export async function whoami() {
|
|
63
|
-
const cfg = loadConfig();
|
|
64
|
-
if (!cfg.token) {
|
|
65
|
-
console.log("Not logged in. Run `vecteur login`.");
|
|
66
|
-
process.exitCode = 1;
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const me = await api("/api/v1/auth/me");
|
|
70
|
-
console.log(`user: ${me.email ?? me.username ?? me.id}`);
|
|
71
|
-
if (me.role)
|
|
72
|
-
console.log(`role: ${me.role}`);
|
|
73
|
-
console.log(`token: ${cfg.tokenPrefix ?? "(hidden)"}…`);
|
|
74
|
-
console.log(`api: ${cfg.apiUrl}`);
|
|
75
|
-
// Quota is shown here once the platform exposes a per-credential quota endpoint (M-U3).
|
|
76
|
-
}
|