@vecteur/cli 0.2.1 → 0.2.3

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.
@@ -9,8 +9,9 @@
9
9
  import { createInterface } from "node:readline";
10
10
  import { loadConfig } from "../config.js";
11
11
  import { login } from "./auth.js";
12
+ import { refreshUpdateCache, updateNoticeFromCache } from "../update.js";
12
13
  import { streamTurn, webBase, buildLocalContextQuery, openBrowser } from "../runner.js";
13
- import { handleSlashCommand, parseMentions, resolveWorkspaceProject } from "../session.js";
14
+ import { handleSlashCommand, parseMentions, renameProject, resolveWorkspaceProject, titleFromPrompt } from "../session.js";
14
15
  const DIM = "\x1b[2m", RESET = "\x1b[0m", CYAN = "\x1b[36m", BOLD = "\x1b[1m";
15
16
  export async function chat() {
16
17
  let cfg = loadConfig();
@@ -30,6 +31,9 @@ export async function chat() {
30
31
  }
31
32
  const { id: project, created } = await resolveWorkspaceProject();
32
33
  const cwd = process.cwd();
34
+ // Refresh the update cache (throttled to once/day) so the notice shows this run, not next.
35
+ await refreshUpdateCache();
36
+ const updateNotice = updateNoticeFromCache();
33
37
  const useInk = Boolean(process.stdout.isTTY) &&
34
38
  (process.stdout.columns ?? 0) >= 60 &&
35
39
  (process.stdout.rows ?? 0) >= 10 &&
@@ -45,13 +49,17 @@ export async function chat() {
45
49
  cwd,
46
50
  created,
47
51
  userLabel: cfg.tokenPrefix ?? "user",
52
+ updateNotice,
48
53
  }));
49
54
  await instance.waitUntilExit();
50
55
  return;
51
56
  }
52
57
  console.log(`${BOLD}Vecteur${RESET} ${DIM}— space-engineering agent in your terminal${RESET}`);
53
58
  console.log(`${DIM}workspace: ${cwd}${RESET}`);
54
- console.log(`${DIM}project: ${project}${created ? " (new)" : ""} · /help for commands${RESET}\n`);
59
+ console.log(`${DIM}project: ${project}${created ? " (new)" : ""} · /help for commands${RESET}`);
60
+ if (updateNotice)
61
+ console.log(`\x1b[33m${updateNotice}${RESET}`);
62
+ console.log("");
55
63
  const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: `${CYAN}› ${RESET}` });
56
64
  let turns = 0;
57
65
  let lastTaskId; // threads multi-turn context to the next turn
@@ -105,6 +113,9 @@ export async function chat() {
105
113
  console.log("\n" + (res.answer ?? "(no answer)") + "\n");
106
114
  if (res.sawVisual)
107
115
  console.log(`${DIM}(visual artifacts — see ${webBase()}/projects/${project})${RESET}`);
116
+ // First prompt in a freshly-created project becomes its title (self-describing in the web app).
117
+ if (created && turns === 0)
118
+ void renameProject(project, titleFromPrompt(raw));
108
119
  lastTaskId = res.taskId;
109
120
  turns++;
110
121
  }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { listProjects } from "./commands/projects.js";
11
11
  import { ask } from "./commands/ask.js";
12
12
  import { chat } from "./commands/chat.js";
13
13
  import { VERSION } from "./version.js";
14
- import { maybeNotifyUpdate, runUpdate } from "./update.js";
14
+ import { refreshUpdateCache, runUpdate } from "./update.js";
15
15
  const program = new Command();
16
16
  program
17
17
  .name("vecteur")
@@ -77,6 +77,6 @@ async function run(fn) {
77
77
  }
78
78
  }
79
79
  }
80
- // Non-blocking update notice (cached banner now; background registry refresh once/day).
81
- maybeNotifyUpdate();
80
+ // Keep the update cache warm for all commands (throttled once/day); `chat` shows the notice in-TUI.
81
+ void refreshUpdateCache();
82
82
  program.parseAsync(process.argv);
package/dist/session.js CHANGED
@@ -7,14 +7,33 @@ export async function resolveWorkspaceProject() {
7
7
  const bound = getWorkspaceProject(cwd);
8
8
  if (bound)
9
9
  return { id: bound, created: false };
10
- const name = basename(cwd) || "workspace";
10
+ // Provisional, unique title until the first prompt renames it: "<dir> · <datetime>".
11
+ // The directory tells you where it came from; the timestamp keeps same-named dirs distinct.
12
+ const dir = basename(cwd) || "workspace";
13
+ const stamp = new Date().toISOString().slice(0, 16).replace("T", " ");
11
14
  const proj = await api("/api/v1/projects", {
12
15
  method: "POST",
13
- body: { name: `${name} (CLI)` },
16
+ body: { name: `${dir} · ${stamp}` },
14
17
  });
15
18
  setWorkspaceProject(cwd, proj.id);
16
19
  return { id: proj.id, created: true };
17
20
  }
21
+ /** A short, human project title from the user's first prompt (collapsed + capped to 60 chars). */
22
+ export function titleFromPrompt(raw) {
23
+ const t = raw.replace(/\s+/g, " ").trim();
24
+ return t.length > 60 ? `${t.slice(0, 57).trimEnd()}…` : t;
25
+ }
26
+ /** Rename a project (best-effort — a nicer title must never break or block a turn). */
27
+ export async function renameProject(projectId, name) {
28
+ if (!name)
29
+ return;
30
+ try {
31
+ await api(`/api/v1/projects/${projectId}`, { method: "PUT", body: { name } });
32
+ }
33
+ catch {
34
+ /* ignore — title is cosmetic */
35
+ }
36
+ }
18
37
  /** Split a line into the prompt text and any @path file mentions. */
19
38
  export function parseMentions(line) {
20
39
  const files = [];
@@ -27,6 +46,7 @@ export function parseMentions(line) {
27
46
  return { text: text.trim(), files };
28
47
  }
29
48
  export const SLASH_COMMANDS = [
49
+ { name: "usage", desc: "show remaining AI usage, forecast and granted pools" },
30
50
  { name: "files", desc: "list files in this workspace directory" },
31
51
  { name: "project", desc: "show the project bound to this directory" },
32
52
  { name: "open", desc: "open this workspace's run in the web app" },
@@ -35,6 +55,44 @@ export const SLASH_COMMANDS = [
35
55
  { name: "help", desc: "show this help" },
36
56
  { name: "exit", desc: "quit" },
37
57
  ];
58
+ /** 1,000 usage units ≈ 1 minute — same vocabulary as the web app and MCP. */
59
+ export function unitsAsTime(units) {
60
+ if (units < 0)
61
+ return "unlimited";
62
+ const minutes = Math.round(units / 1000);
63
+ if (minutes < 60)
64
+ return `${minutes} min`;
65
+ const hours = Math.floor(minutes / 60);
66
+ const rem = minutes % 60;
67
+ return rem === 0 ? `${hours}h` : `${hours}h ${String(rem).padStart(2, "0")}m`;
68
+ }
69
+ export function formatUsage(sub) {
70
+ const lines = [];
71
+ const t = sub.tokens;
72
+ lines.push(`plan: ${(sub.account_type ?? "free").toUpperCase()}`);
73
+ if (t) {
74
+ const dailyLeft = Math.max(0, t.daily_limit - t.daily_used);
75
+ const monthlyLeft = Math.max(0, t.monthly_limit - t.monthly_used);
76
+ lines.push(`today: ${unitsAsTime(t.daily_used)} used · ≈ ${unitsAsTime(dailyLeft)} left of ${unitsAsTime(t.daily_limit)}`);
77
+ lines.push(`this month: ${unitsAsTime(t.monthly_used)} used · ≈ ${unitsAsTime(monthlyLeft)} left of ${unitsAsTime(t.monthly_limit)}`);
78
+ }
79
+ if (sub.forecast) {
80
+ const f = sub.forecast;
81
+ const pct = f.projected_pct_of_limit != null ? `~${f.projected_pct_of_limit}% of your monthly allowance by month-end` : "";
82
+ const dep = f.depletion_date ? ` · runs out ~${f.depletion_date}` : "";
83
+ if (pct || dep)
84
+ lines.push(`forecast: ${pct}${dep}`);
85
+ }
86
+ for (const g of sub.grants ?? []) {
87
+ const shared = (g.shared_member_count ?? 0) > 1 ? ` · shared with ${g.shared_member_count}` : "";
88
+ const exp = g.expires_at ? ` · expires ${g.expires_at.slice(0, 10)}` : "";
89
+ lines.push(`granted: ${unitsAsTime(g.remaining_units ?? 0)} left (${g.label ?? "grant"})${shared}${exp}`);
90
+ }
91
+ if (typeof sub.credit_balance_eur === "number") {
92
+ lines.push(`credits: €${sub.credit_balance_eur.toFixed(2)}`);
93
+ }
94
+ return lines.map((l) => ` ${l}`).join("\n");
95
+ }
38
96
  export const HELP = `
39
97
  Commands:
40
98
  @path attach a local file as context (e.g. "explain @mission.md")
@@ -59,5 +117,14 @@ export async function handleSlashCommand(cmd, ctx) {
59
117
  const output = (files.files ?? []).map((f) => ` ${f.name ?? f}`).join("\n") || " (none)";
60
118
  return { output };
61
119
  }
120
+ if (name === "usage") {
121
+ try {
122
+ const sub = await api("/api/v1/billing/subscription");
123
+ return { output: formatUsage(sub) };
124
+ }
125
+ catch (e) {
126
+ return { output: `could not load usage (${e.message})` };
127
+ }
128
+ }
62
129
  return { output: `unknown command: /${name} (/help)` };
63
130
  }
package/dist/ui/App.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { Box, Static, Text, useApp, useInput } from "ink";
4
4
  import { buildLocalContextQuery, openBrowser, streamTurn, webBase } from "../runner.js";
5
- import { handleSlashCommand, parseMentions, SLASH_COMMANDS } from "../session.js";
5
+ import { handleSlashCommand, parseMentions, renameProject, SLASH_COMMANDS, titleFromPrompt } from "../session.js";
6
6
  import { markdownToAnsi } from "./markdown.js";
7
7
  import { Header } from "./Header.js";
8
8
  import { Logo } from "./logo.js";
@@ -30,7 +30,7 @@ function SlashMenu({ value, selected }) {
30
30
  const sel = Math.min(selected, matches.length - 1);
31
31
  return (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: matches.map((cmd, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", bold: i === sel, children: [i === sel ? "❯ " : " ", "/", cmd.name] }), _jsxs(Text, { dimColor: true, children: [" ", cmd.desc] })] }, cmd.name))) }));
32
32
  }
33
- export function App({ project, cwd, created }) {
33
+ export function App({ project, cwd, created, updateNotice }) {
34
34
  const { exit } = useApp();
35
35
  const [input, setInput] = useState("");
36
36
  const [history, setHistory] = useState([]);
@@ -110,6 +110,9 @@ export function App({ project, cwd, created }) {
110
110
  }
111
111
  else {
112
112
  pushItem({ user: raw, answer: result.answer ?? "(no answer)", sawVisual: result.sawVisual });
113
+ // First prompt in a freshly-created project becomes its title (self-describing in the web app).
114
+ if (created && turns === 0)
115
+ void renameProject(project, titleFromPrompt(raw));
113
116
  setLastTaskId(result.taskId);
114
117
  setTurns((prev) => prev + 1);
115
118
  setTokenTotal((prev) => prev + (result.tokens?.total ?? 0));
@@ -168,5 +171,5 @@ export function App({ project, cwd, created }) {
168
171
  }
169
172
  }
170
173
  });
171
- return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Header, { cwd: cwd, project: project }), items.length === 0 && turns === 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Logo, {}), _jsx(Text, { dimColor: true, children: "New workspace. Try: \"design a sun-synchronous orbit at 550 km\" \u2014 or @mention a local file." })] })) : null, _jsx(Static, { items: items, children: (item) => _jsx(TranscriptTurn, { item: item, project: project }, item.id) }), streaming ? _jsx(RunStatus, { stages: stages.length ? stages : ["starting run"] }) : null, _jsx(Prompt, { value: input, onChange: onInputChange, onSubmit: submit, disabled: streaming }), _jsx(SlashMenu, { value: input, selected: selected }), notice ? _jsx(Text, { dimColor: true, children: notice }) : null, _jsxs(Box, { justifyContent: "space-between", width: "100%", children: [_jsx(Text, { dimColor: true, children: "enter send \u00B7 /help \u00B7 ctrl-d exit" }), _jsxs(Text, { dimColor: true, children: ["tokens: ", tokenTotal] })] })] }));
174
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Header, { cwd: cwd, project: project }), updateNotice ? _jsxs(Text, { color: "yellow", children: ["\u2191 ", updateNotice] }) : null, items.length === 0 && turns === 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Logo, {}), _jsx(Text, { dimColor: true, children: "New workspace. Try: \"design a sun-synchronous orbit at 550 km\" \u2014 or @mention a local file." })] })) : null, _jsx(Static, { items: items, children: (item) => _jsx(TranscriptTurn, { item: item, project: project }, item.id) }), streaming ? _jsx(RunStatus, { stages: stages.length ? stages : ["starting run"] }) : null, _jsx(Prompt, { value: input, onChange: onInputChange, onSubmit: submit, disabled: streaming }), _jsx(SlashMenu, { value: input, selected: selected }), notice ? _jsx(Text, { dimColor: true, children: notice }) : null, _jsxs(Box, { justifyContent: "space-between", width: "100%", children: [_jsx(Text, { dimColor: true, children: "enter send \u00B7 /help \u00B7 ctrl-d exit" }), _jsxs(Text, { dimColor: true, children: ["tokens: ", tokenTotal] })] })] }));
172
175
  }
package/dist/ui/logo.js CHANGED
@@ -1,11 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useStdout } from "ink";
3
3
  import { VERSION } from "../version.js";
4
- // The Vecteur mark is a downward triangle (the "V"), matching the web wordmark
5
- // (`M16 26 L4 4 H28 Z`). Rendered in the brand blue next to the wordmark.
6
- const TRIANGLE = ["╲ ╱", " ╲ ╱ ", " ╲ ╱ ", " ╲ ╱ ", " ╲ ╱ ", " ╲╱ "];
7
4
  const BRAND_BLUE = "#4a9eff";
8
- /** ASCII welcome banner shown on the first screen of an interactive session. */
5
+ // "VECTEUR" in an ANSI-shadow block font — the brand wordmark for a wide terminal.
6
+ const WORDMARK = [
7
+ "██╗ ██╗███████╗ ██████╗████████╗███████╗██╗ ██╗██████╗",
8
+ "██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝██║ ██║██╔══██╗",
9
+ "██║ ██║█████╗ ██║ ██║ █████╗ ██║ ██║██████╔╝",
10
+ "╚██╗ ██╔╝██╔══╝ ██║ ██║ ██╔══╝ ██║ ██║██╔══██╗",
11
+ " ╚████╔╝ ███████╗╚██████╗ ██║ ███████╗╚██████╔╝██║ ██║",
12
+ " ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝",
13
+ ];
14
+ const WORDMARK_WIDTH = 58;
15
+ /** Brand banner for the interactive welcome. Full wordmark when it fits; a compact mark otherwise. */
9
16
  export function Logo() {
10
- return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Box, { flexDirection: "column", children: TRIANGLE.map((line, i) => (_jsx(Text, { color: BRAND_BLUE, bold: true, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { bold: true, color: "white", children: "V E C T E U R" }), _jsxs(Text, { dimColor: true, children: ["space-engineering agent \u00B7 v", VERSION] })] })] }));
17
+ const { stdout } = useStdout();
18
+ const cols = stdout?.columns ?? 80;
19
+ if (cols >= WORDMARK_WIDTH + 6) {
20
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [WORDMARK.map((line, i) => (_jsx(Text, { color: BRAND_BLUE, children: line }, i))), _jsx(Text, { dimColor: true, children: ` space-engineering agent · v${VERSION}` })] }));
21
+ }
22
+ // Compact mark for narrow (but ink-capable) terminals — a downward-triangle "V" + wordmark.
23
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: BRAND_BLUE, bold: true, children: "▽ " }), _jsx(Text, { bold: true, color: "white", children: "VECTEUR" }), _jsx(Text, { dimColor: true, children: ` space-engineering agent · v${VERSION}` })] }));
11
24
  }
package/dist/update.js CHANGED
@@ -53,17 +53,6 @@ export async function refreshUpdateCache() {
53
53
  /* offline / registry down / timeout — silent, try again tomorrow */
54
54
  }
55
55
  }
56
- /**
57
- * Print the cached update banner (to stderr, TTY only, so it never pollutes piped/JSON output)
58
- * and kick off a background cache refresh that we intentionally do NOT await.
59
- */
60
- export function maybeNotifyUpdate() {
61
- const notice = updateNoticeFromCache();
62
- if (notice && process.stderr.isTTY) {
63
- process.stderr.write(`\x1b[2m${notice}\x1b[0m\n`);
64
- }
65
- void refreshUpdateCache();
66
- }
67
56
  /** `vecteur update` — self-update via the global npm install. */
68
57
  export async function runUpdate() {
69
58
  const { spawn } = await import("node:child_process");
package/dist/version.js CHANGED
@@ -3,4 +3,4 @@
3
3
  * `version.test.ts` (the build/test fails if they drift). Used for `--version`, the
4
4
  * `User-Agent` header (lets the server gate old clients with 426), and the update check.
5
5
  */
6
- export const VERSION = "0.2.1";
6
+ export const VERSION = "0.2.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vecteur/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Vecteur CLI — a thin client for the Vecteur space-engineering platform (login, ask, projects, files). Hosted brain; no IP ships.",
5
5
  "type": "module",
6
6
  "license": "MIT",