@vecteur/cli 0.6.0 → 0.6.2
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 +10 -0
- package/bin/vecteur.js +13 -12
- package/package.json +1 -1
- package/src/client.js +90 -11
- package/src/contract.js +1 -1
- package/src/mcp.js +2 -1
package/README.md
CHANGED
|
@@ -34,6 +34,16 @@ With `VECTEUR_TOKEN`, runs require `--project ID` and `project use` requires a p
|
|
|
34
34
|
Project PATs retain their server-defined scope; listing and creating Projects require an account
|
|
35
35
|
login. `project list --json` preserves `{projects, active_projects, selected_project_id}`.
|
|
36
36
|
|
|
37
|
+
`run submit` continues the Project's conversation: it reads the Project's current head (the newest
|
|
38
|
+
Run's `state_hash`) and submits the next turn on it, so two submits in a row are two turns of one
|
|
39
|
+
study, exactly as in the web app. A new Project's first turn has no head. `--parent STATE_HASH`
|
|
40
|
+
skips the read and admits the turn only if that hash is still the head. The CLI never forks a
|
|
41
|
+
Project and never retries: a moved head exits `state_fork` naming the current head, and a Project
|
|
42
|
+
whose Run is still open exits `state_busy` naming that Run (and, when it waits on a question, the
|
|
43
|
+
web address where it can be answered). Nothing is admitted in either case. Once a turn is admitted,
|
|
44
|
+
any later failure names the Run: check it with `vecteur run get RUN_ID` instead of submitting
|
|
45
|
+
again, which would start another turn. Start a fresh study with `vecteur project create`.
|
|
46
|
+
|
|
37
47
|
`run events` validates dense SSE sequence and returns the canonical direct record only when its
|
|
38
48
|
embedded events match the stream. Redirects, secret-shaped responses, malformed responses and
|
|
39
49
|
timeouts fail nonzero. `--json` sorts object keys for deterministic machine output.
|
package/bin/vecteur.js
CHANGED
|
@@ -3,8 +3,8 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
CliError, VecteurClient,
|
|
7
|
-
shouldOpenBrowser, waitForDeviceAuthorization,
|
|
6
|
+
CliError, VecteurClient, admittedFailure, configuration, isStateHash, isTokenShaped, redact,
|
|
7
|
+
requestTimeout, resolveOrigin, shouldOpenBrowser, waitForDeviceAuthorization,
|
|
8
8
|
} from "../src/client.js";
|
|
9
9
|
import {
|
|
10
10
|
clearCredentials, credentialsPath, readCredentials, setCurrentOrigin, writeCredentials, selectProject,
|
|
@@ -43,8 +43,9 @@ function refuseOldNode() {
|
|
|
43
43
|
|
|
44
44
|
function helpText(version) {
|
|
45
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>] | projection get <run-id> <projection-id> [--output <path>] [-p <id>] | mcp
|
|
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> [--parent <state-hash>] | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | projection get <run-id> <projection-id> [--output <path>] [-p <id>] | mcp
|
|
47
47
|
--help --version --json --origin <url> --no-browser --local -p/--project -a/--ask --output <path>
|
|
48
|
+
run submit continues the Project's current head; --parent <state-hash> admits it only if that is still the head
|
|
48
49
|
`;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -101,6 +102,10 @@ function parseCommand(argv) {
|
|
|
101
102
|
const ask = takeOption("--ask", "-a");
|
|
102
103
|
const workspace = takeOption("--workspace");
|
|
103
104
|
const output = takeOption("--output");
|
|
105
|
+
const parent = takeOption("--parent");
|
|
106
|
+
if (parent !== undefined && (!isStateHash(parent) || args[0] !== "run" || args[1] !== "submit")) {
|
|
107
|
+
throw new CliError("usage_invalid", "Invalid command grammar");
|
|
108
|
+
}
|
|
104
109
|
if (args.some((arg) => typeof arg === "string" && arg.startsWith("-"))) {
|
|
105
110
|
throw new CliError("usage_invalid", "Invalid command grammar");
|
|
106
111
|
}
|
|
@@ -142,7 +147,7 @@ function parseCommand(argv) {
|
|
|
142
147
|
}
|
|
143
148
|
if (args.length === 2 && args[0] === "run" && args[1] === "submit"
|
|
144
149
|
&& (projectId === undefined || value(projectId)) && value(ask) && output === undefined) {
|
|
145
|
-
return { kind: "run_submit", projectId, ask, origin, json };
|
|
150
|
+
return { kind: "run_submit", projectId, ask, parent, origin, json };
|
|
146
151
|
}
|
|
147
152
|
if (args.length === 3 && args[0] === "run" && args[1] === "get" && value(args[2])
|
|
148
153
|
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
@@ -336,19 +341,15 @@ async function main() {
|
|
|
336
341
|
}
|
|
337
342
|
else if (command.kind === "run_submit") {
|
|
338
343
|
const projectId = await resolveRunProject(client, command.projectId);
|
|
339
|
-
value = await client.runSubmit(projectId, command.ask, onEvent);
|
|
344
|
+
value = await client.runSubmit(projectId, command.ask, onEvent, undefined, command.parent);
|
|
340
345
|
if (!command.json) {
|
|
341
|
-
const currentTransferIds = await verifiedTransferIds(client, value);
|
|
342
|
-
process.stdout.write(human(value, { projectId, currentTransferIds }));
|
|
343
346
|
try {
|
|
347
|
+
const currentTransferIds = await verifiedTransferIds(client, value);
|
|
348
|
+
process.stdout.write(human(value, { projectId, currentTransferIds }));
|
|
344
349
|
const cost = await client.customerCost(projectId);
|
|
345
350
|
process.stdout.write(`\n${humanCost(cost)}`);
|
|
346
351
|
} catch (error) {
|
|
347
|
-
|
|
348
|
-
throw new CliError("pat_expired",
|
|
349
|
-
`credential expired after Run ${value.run_id} was admitted; next: vecteur login; then: vecteur run get ${value.run_id}`);
|
|
350
|
-
}
|
|
351
|
-
throw error;
|
|
352
|
+
throw admittedFailure(error, value.run_id);
|
|
352
353
|
}
|
|
353
354
|
return;
|
|
354
355
|
}
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -23,8 +23,28 @@ const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
|
23
23
|
const MAX_FRAME_BYTES = 1024 * 1024;
|
|
24
24
|
const MAX_EVENTS = 10_000;
|
|
25
25
|
const CAS_REF_SHAPE = /^sha256:([0-9a-f]{64})$/;
|
|
26
|
+
const STATE_HASH_SHAPE = /^[0-9a-f]{64}$/;
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
/** The one shape of a design head, so `--parent` and the head read cannot disagree. */
|
|
30
|
+
export function isStateHash(value) {
|
|
31
|
+
return typeof value === "string" && STATE_HASH_SHAPE.test(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A failure after the acknowledgement names the admitted Run, which may still be running:
|
|
36
|
+
* retrying the submit would now continue its head and admit a second, billable turn.
|
|
37
|
+
*/
|
|
38
|
+
export function admittedFailure(error, runId) {
|
|
39
|
+
if (!(error instanceof CliError)) return error;
|
|
40
|
+
if (error.code === "pat_expired") {
|
|
41
|
+
return new CliError("pat_expired",
|
|
42
|
+
`credential expired after Run ${runId} was admitted; next: vecteur login; then: vecteur run get ${runId}`);
|
|
43
|
+
}
|
|
44
|
+
return new CliError(error.code,
|
|
45
|
+
`Run ${runId} was admitted and may still be running; next: vecteur run get ${runId}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export function redact(value) {
|
|
29
49
|
return String(value).replace(TOKEN_PATTERN, "[REDACTED]");
|
|
30
50
|
}
|
|
@@ -585,24 +605,83 @@ export class VecteurClient {
|
|
|
585
605
|
}
|
|
586
606
|
}
|
|
587
607
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
608
|
+
/**
|
|
609
|
+
* The Project's newest Run from the owner page — the record the web chains on
|
|
610
|
+
* (`CanvasPage.vue` designHead). Its public `state_hash` is the design head once the Run is
|
|
611
|
+
* terminal. An empty page with a cursor is a newest Run this login may not see, not an empty
|
|
612
|
+
* Project, so it refuses rather than let the caller guess null.
|
|
613
|
+
*/
|
|
614
|
+
async projectNewestRun(projectId, signal = undefined) {
|
|
615
|
+
const page = await this.#json("GET",
|
|
616
|
+
`/api/projects/${encodeURIComponent(projectId)}/runs?limit=1`, { signal });
|
|
617
|
+
const newest = page?.runs?.[0];
|
|
618
|
+
if (!exactKeys(page, ["runs", "next_before"]) || !Array.isArray(page.runs) || page.runs.length > 1
|
|
619
|
+
|| (page.next_before !== null && typeof page.next_before !== "string")
|
|
620
|
+
|| (newest !== undefined && (newest?.request?.project_id !== projectId
|
|
621
|
+
|| !validateAdmitted(newest.response) || !isStateHash(newest.response.state_hash)))) {
|
|
622
|
+
throw new CliError("project_runs_invalid", "Server returned malformed Project run history");
|
|
623
|
+
}
|
|
624
|
+
if (!newest && page.next_before !== null) {
|
|
625
|
+
throw new CliError("project_head_hidden",
|
|
626
|
+
`the newest Run in Project ${projectId} is not visible to this login; nothing was submitted; `
|
|
627
|
+
+ "next: continue in the web app, or pass --parent <state-hash>");
|
|
628
|
+
}
|
|
629
|
+
return newest?.response ?? null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* One turn, chained onto the Project's design head (OPE admits null only on a Project with no
|
|
634
|
+
* head, otherwise only the exact head). `parentStateHash` pins a compare-and-swap instead of
|
|
635
|
+
* reading the head. A 409 is re-read ONCE to name the head or the open Run, and never
|
|
636
|
+
* re-posted: the Idempotency-Key is per invocation, so a second POST is a second billable
|
|
637
|
+
* admission, written against a head the ask never saw.
|
|
638
|
+
*/
|
|
639
|
+
async runSubmit(projectId, ask, onEvent = null, signal = undefined, parentStateHash = undefined) {
|
|
640
|
+
const parent = parentStateHash === undefined
|
|
641
|
+
? (await this.projectNewestRun(projectId, signal))?.state_hash ?? null
|
|
642
|
+
: parentStateHash;
|
|
643
|
+
let value;
|
|
644
|
+
try {
|
|
645
|
+
value = await this.#json("POST", `/api/projects/${encodeURIComponent(projectId)}/runs`, {
|
|
646
|
+
body: { ask, attachments: [], parent_state_hash: parent, ui_context: null },
|
|
647
|
+
idempotencyKey: randomUUID(),
|
|
648
|
+
signal,
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (error instanceof CliError && (error.code === "state_fork" || error.code === "state_busy")) {
|
|
652
|
+
throw await this.#admissionRefused(error.code, projectId, parent, signal);
|
|
653
|
+
}
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
594
656
|
if (!validateRunAck(value)) {
|
|
595
657
|
throw new CliError("run_ack_invalid", "Server returned malformed run acknowledgement");
|
|
596
658
|
}
|
|
597
659
|
try {
|
|
598
660
|
return await this.runEvents(value.run_id, onEvent, signal);
|
|
599
661
|
} catch (error) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
662
|
+
throw admittedFailure(error, value.run_id);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async #admissionRefused(code, projectId, sent, signal) {
|
|
667
|
+
let newest;
|
|
668
|
+
try { newest = await this.projectNewestRun(projectId, signal); } catch { newest = undefined; }
|
|
669
|
+
if (code === "state_busy" && newest?.terminal === "") {
|
|
670
|
+
const run = newest.run_id;
|
|
671
|
+
const question = newest.events.filter((event) => event.kind === "clarification").at(-1);
|
|
672
|
+
const waiting = question?.payload?.state === "requested"
|
|
673
|
+
? ` [waiting for an answer: ${this.config.origin}/dashboard/${encodeURIComponent(projectId)}?run=${encodeURIComponent(run)}]`
|
|
674
|
+
: "";
|
|
675
|
+
return new CliError(code, `Run ${run} is still open in Project ${projectId}${waiting}; `
|
|
676
|
+
+ `nothing was admitted; next: vecteur run get ${run}`);
|
|
677
|
+
}
|
|
678
|
+
if (code === "state_fork" && newest !== undefined) {
|
|
679
|
+
const moved = newest?.state_hash ?? null;
|
|
680
|
+
return new CliError(code, `Project ${projectId} head is ${moved}, this submit sent ${sent}; `
|
|
681
|
+
+ "nothing was admitted; next: " + (moved === null ? "submit again without --parent"
|
|
682
|
+
: `submit again to continue ${moved}, or pass --parent ${moved}`));
|
|
605
683
|
}
|
|
684
|
+
return new CliError(code, `nothing was admitted to Project ${projectId}; next: submit again`);
|
|
606
685
|
}
|
|
607
686
|
|
|
608
687
|
async runGet(runId, signal = undefined) {
|
package/src/contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-contract.mjs from owner TypeScript projections.
|
|
2
|
-
export const OWNER_CONTRACT_IDENTITY = "api-sha256:8f81ea544eecd9e81da3fd56ad816ac24991cb0e68f3b1cb45180711eaf16056;run-wire-sha256:
|
|
2
|
+
export const OWNER_CONTRACT_IDENTITY = "api-sha256:8f81ea544eecd9e81da3fd56ad816ac24991cb0e68f3b1cb45180711eaf16056;run-wire-sha256:03db13f2f5305ded2432fdd33c0db2f1459938584dce250bced24c218eda63ea";
|
|
3
3
|
export const RECOVERY_ACTIONS = Object.freeze({"delete_project":{"label":"Delete a project","href":"/projects"},"upgrade_plan":{"label":"Upgrade your plan","href":"/dashboard/billing"},"wait_for_reset":{"label":"Wait until the UTC reset","href":null}});
|
|
4
4
|
export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","tool_call","tool_result","synthesis","artifact","terminal","heartbeat","assistant_delta","knowledge_binding","engineering_projection","provider_attempt","clarification","clarification_evidence","input_rows"]);
|
|
5
5
|
export const RETIRED_EVENT_KINDS = Object.freeze(["context_frame","cta","step_label"]);
|
package/src/mcp.js
CHANGED
|
@@ -102,7 +102,8 @@ export function createVecteurMcpServer(client = new VecteurClient(), shutdownSig
|
|
|
102
102
|
tool(server, "vecteur_project_get", "Get one PAT-bound Vecteur project.", projectInput, projectOutput,
|
|
103
103
|
({ project_id: projectId }, context) => client.projectGet(projectId, signal(context)));
|
|
104
104
|
|
|
105
|
-
tool(server, "vecteur_run_submit",
|
|
105
|
+
tool(server, "vecteur_run_submit",
|
|
106
|
+
"Submit one project turn that continues the Project's current head and return its terminal record.",
|
|
106
107
|
submitInput, admittedOutput, async ({ project_id: projectId, ask }, context) => {
|
|
107
108
|
const progressToken = context.mcpReq._meta?.progressToken;
|
|
108
109
|
const onEvent = progressToken === undefined ? null : (event) => context.mcpReq.notify({
|