@ordewell/cli 0.4.14 → 0.4.16

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.
Files changed (2) hide show
  1. package/dist/main.js +113 -33
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -376,6 +376,14 @@ var import_http2 = __toESM(require("http"));
376
376
  var import_ws = __toESM(require("ws"));
377
377
  var import_core3 = require("@ordewell/core");
378
378
  var DEFAULT_HTTP_TIMEOUT_MS = 15 * 60 * 1e3;
379
+ var WorkspaceInitNeededError = class extends Error {
380
+ constructor(message, workspace) {
381
+ super(message);
382
+ this.workspace = workspace;
383
+ this.name = "WorkspaceInitNeededError";
384
+ }
385
+ workspace;
386
+ };
379
387
  var ApiClient = class {
380
388
  port;
381
389
  constructor(port) {
@@ -446,13 +454,17 @@ var ApiClient = class {
446
454
  * the verify toggle, so the returned plan may be a question
447
455
  * (empty `tasks`, last word in `conversationHistory`) rather than a committed plan.
448
456
  */
449
- async startConversation(sessionId, goal, runners2, workspace) {
457
+ async startConversation(sessionId, goal, runners2, workspace, allowInit) {
450
458
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/converse/start`, {
451
459
  goal,
452
460
  runners: runners2 || void 0,
453
- workspace
461
+ workspace,
462
+ allowInit
454
463
  });
455
464
  if (res.status !== 200) {
465
+ if (res.data?.code === "workspace_not_a_project") {
466
+ throw new WorkspaceInitNeededError(res.data.error || "Not a project directory", res.data.workspace || workspace || "");
467
+ }
456
468
  throw new Error(res.data?.error || "Planning failed");
457
469
  }
458
470
  return res.data.plan;
@@ -3023,12 +3035,39 @@ function findCommand(name) {
3023
3035
  const key = name.replace(/^\//, "").toLowerCase();
3024
3036
  return BY_NAME.get(key) ?? skillByName.get(key);
3025
3037
  }
3026
- function completions(text) {
3027
- if (!text.startsWith("/")) return [];
3028
- const typed = text.slice(1);
3029
- if (/\s/.test(typed)) return [];
3030
- const prefix = typed.toLowerCase();
3031
- return [...SLASH_COMMANDS, ...skillCommands].filter((c) => c.name.startsWith(prefix));
3038
+ var TRAILING_PUNCT = /[,.!?;:]+$/;
3039
+ function slashTokens(text) {
3040
+ const tokens = [];
3041
+ const re = /(^|\s)\/(\S*)/g;
3042
+ let match;
3043
+ while (match = re.exec(text)) {
3044
+ const start = match.index + match[1].length;
3045
+ const raw = match[2];
3046
+ const punct = raw.match(TRAILING_PUNCT)?.[0] ?? "";
3047
+ const nameLen = raw.length - punct.length;
3048
+ tokens.push({
3049
+ start,
3050
+ end: start + 1 + raw.length,
3051
+ name: raw.slice(0, nameLen).toLowerCase(),
3052
+ nameEnd: start + 1 + nameLen
3053
+ });
3054
+ }
3055
+ return tokens;
3056
+ }
3057
+ function activeToken(text, cursor) {
3058
+ return slashTokens(text).find((t) => t.end === cursor) ?? null;
3059
+ }
3060
+ function skillMatchKind(name) {
3061
+ if (name.length === 0) return null;
3062
+ if (skillByName.has(name)) return "exact";
3063
+ for (const key of skillByName.keys()) {
3064
+ if (key.startsWith(name)) return "prefix";
3065
+ }
3066
+ return null;
3067
+ }
3068
+ function tokenCompletions(token) {
3069
+ const pool = token.start === 0 ? [...SLASH_COMMANDS, ...skillCommands] : skillCommands;
3070
+ return pool.filter((c) => c.name.startsWith(token.name));
3032
3071
  }
3033
3072
 
3034
3073
  // src/tui/markdown.ts
@@ -3756,17 +3795,32 @@ function renderStatus(state, cols) {
3756
3795
  const tail = thinking.length > room ? `\u2026${thinking.slice(-(room - 1)).trimStart()}` : thinking;
3757
3796
  return truncate(`${head}${style.grey(` \xB7 ${tail}`)}`, cols);
3758
3797
  }
3759
- var LEADING_TOKEN = /^\/\S*/;
3760
- function skillTokenLength(text) {
3761
- const match = LEADING_TOKEN.exec(text);
3762
- if (!match) return 0;
3763
- return findCommand(match[0])?.source === "skill" ? match[0].length : 0;
3798
+ function highlightSpans(text) {
3799
+ const spans = [];
3800
+ const expandedExact = /* @__PURE__ */ new Set();
3801
+ for (const token of slashTokens(text)) {
3802
+ const kind = skillMatchKind(token.name);
3803
+ if (kind === null) continue;
3804
+ if (kind === "exact") {
3805
+ if (expandedExact.has(token.name)) continue;
3806
+ expandedExact.add(token.name);
3807
+ }
3808
+ spans.push({ start: token.start, end: token.nameEnd });
3809
+ }
3810
+ return spans;
3764
3811
  }
3765
- function paintRange(text, base, tokenEnd) {
3766
- const localEnd = Math.max(0, Math.min(text.length, tokenEnd - base));
3767
- if (localEnd <= 0) return text;
3768
- if (localEnd >= text.length) return style.cyan(text);
3769
- return style.cyan(text.slice(0, localEnd)) + text.slice(localEnd);
3812
+ function paintSpans(text, base, spans) {
3813
+ if (spans.length === 0) return text;
3814
+ let out = "";
3815
+ let cursor = 0;
3816
+ for (const span of spans) {
3817
+ const localStart = Math.max(cursor, Math.min(text.length, span.start - base));
3818
+ const localEnd = Math.max(0, Math.min(text.length, span.end - base));
3819
+ if (localEnd <= localStart) continue;
3820
+ out += text.slice(cursor, localStart) + style.cyan(text.slice(localStart, localEnd));
3821
+ cursor = localEnd;
3822
+ }
3823
+ return out + text.slice(cursor);
3770
3824
  }
3771
3825
  function renderInput(state, cols) {
3772
3826
  const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
@@ -3774,28 +3828,31 @@ function renderInput(state, cols) {
3774
3828
  const room = chatEditorRoom(cols, lines !== null);
3775
3829
  const { text, cursor } = state.editor;
3776
3830
  const continuation = " ".repeat(width(prompt));
3777
- const tokenEnd = skillTokenLength(text);
3831
+ const spans = highlightSpans(text);
3778
3832
  if (!lines) {
3779
3833
  const visible = text.replace(/\n/g, "\xB6").replace(/\t/g, " ");
3780
- const painted = tokenEnd > 0 ? paintRange(visible, 0, tokenEnd) : visible;
3834
+ const painted = paintSpans(visible, 0, spans);
3781
3835
  return [prompt + painted + suggest(state, room - width(visible))];
3782
3836
  }
3783
3837
  const { line: lineIndex, col } = cursorInLines(lines, cursor, text.length);
3784
- const rawLines = lines.map((l) => l.line);
3785
- return rawLines.map((raw, i) => {
3838
+ return lines.map(({ line: raw, start: base }, i) => {
3786
3839
  const prefix = i === 0 ? prompt : continuation;
3787
3840
  const line = raw.replace(/\t/g, " ");
3788
3841
  const isCursorLine = i === lineIndex;
3789
- const lineTokenEnd = i === 0 ? tokenEnd : 0;
3790
- const rendered = isCursorLine ? paintRange(line.slice(0, col), 0, lineTokenEnd) + style.inverse(line.slice(col, col + 1) || " ") + paintRange(line.slice(col + 1), col + 1, lineTokenEnd) : paintRange(line, 0, lineTokenEnd);
3791
- const used = width(line) + (isCursorLine && col >= line.length ? 1 : 0);
3792
- const hint = i === rawLines.length - 1 ? suggest(state, room - used) : "";
3793
- return prefix + rendered + hint;
3842
+ if (!isCursorLine) return prefix + paintSpans(line, base, spans);
3843
+ const before = paintSpans(line.slice(0, col), base, spans);
3844
+ const caretChar = line.slice(col, col + 1) || " ";
3845
+ const after = paintSpans(line.slice(col + 1), base + col + 1, spans);
3846
+ const used = width(line.slice(0, col)) + width(caretChar);
3847
+ const hint = suggest(state, room - used);
3848
+ return prefix + before + style.inverse(caretChar) + hint + after;
3794
3849
  });
3795
3850
  }
3796
3851
  function suggest(state, room) {
3797
3852
  if (state.focus === "plan" || room <= 2) return "";
3798
- const matches = completions(state.editor.text);
3853
+ const token = activeToken(state.editor.text, state.editor.cursor);
3854
+ if (!token) return "";
3855
+ const matches = tokenCompletions(token);
3799
3856
  if (matches.length === 0) return "";
3800
3857
  const names = matches.map((c) => c.name).join(" ");
3801
3858
  return style.grey(` ${truncate(names, Math.max(0, room - 2))}`);
@@ -4133,6 +4190,19 @@ function reduce(state, action) {
4133
4190
  }, ["set-planner", "set-task-runner"]));
4134
4191
  case "failed":
4135
4192
  return step({ ...say(state, "error", action.message), status: "idle", busyLabel: "", thinkingLine: "" });
4193
+ case "workspaceNeedsInit":
4194
+ return step({
4195
+ ...state,
4196
+ status: "idle",
4197
+ busyLabel: "",
4198
+ thinkingLine: "",
4199
+ overlay: {
4200
+ kind: "confirm",
4201
+ title: "Initialize new workspace?",
4202
+ message: `"${action.workspace}" isn't a recognized project yet (no .git or manifest found). Start a new ordewell workspace here?`,
4203
+ action: { kind: "init-workspace", goal: action.goal, workspace: action.workspace }
4204
+ }
4205
+ });
4136
4206
  case "notice":
4137
4207
  return step(say(state, "system", action.message));
4138
4208
  case "resize": {
@@ -4409,10 +4479,13 @@ function handleKey2(state, key) {
4409
4479
  if (isMouseSelect(key)) return handleMouseSelect(state, key);
4410
4480
  if (key.name === "wheelignored") return step(state);
4411
4481
  if (key.name === "tab" && state.focus === "chat") {
4412
- const matches = completions(state.editor.text);
4413
- if (matches.length > 0) {
4414
- const text = `/${matches[0].name} `;
4415
- return step({ ...state, editor: { ...state.editor, text, cursor: text.length } });
4482
+ const token = activeToken(state.editor.text, state.editor.cursor);
4483
+ const matches = token ? tokenCompletions(token) : [];
4484
+ if (token && matches.length > 0) {
4485
+ const { text: full } = state.editor;
4486
+ const completed = `/${matches[0].name} `;
4487
+ const text = full.slice(0, token.start) + completed + full.slice(token.end);
4488
+ return step({ ...state, editor: { ...state.editor, text, cursor: token.start + completed.length } });
4416
4489
  }
4417
4490
  }
4418
4491
  if (key.name === "tab") return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
@@ -4561,6 +4634,9 @@ function handleConfirmKey(state, overlay, key) {
4561
4634
  if (!state.sessionId) return step(closed);
4562
4635
  return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
4563
4636
  }
4637
+ if (overlay.action.kind === "init-workspace") {
4638
+ return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
4639
+ }
4564
4640
  return step(closed);
4565
4641
  }
4566
4642
  function handleHelpKey(state, scroll, key) {
@@ -5431,9 +5507,13 @@ async function perform(effect, deps) {
5431
5507
  const sessionId = deps.newSessionId();
5432
5508
  dispatch({ type: "sessionStarted", sessionId, goal: effect.goal });
5433
5509
  try {
5434
- await converse(deps, sessionId, () => api.startConversation(sessionId, effect.goal, void 0, workspace));
5510
+ await converse(deps, sessionId, () => api.startConversation(sessionId, effect.goal, void 0, workspace, effect.allowInit));
5435
5511
  } catch (err) {
5436
5512
  dispatch({ type: "sessionCleared" });
5513
+ if (err instanceof WorkspaceInitNeededError && !effect.allowInit) {
5514
+ dispatch({ type: "workspaceNeedsInit", goal: effect.goal, workspace: err.workspace });
5515
+ return;
5516
+ }
5437
5517
  throw err;
5438
5518
  }
5439
5519
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",