wdyt 0.1.10 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdyt",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "description": "Code review context builder for LLMs - what do you think?",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -26,34 +26,33 @@ import {
26
26
  import { selectGetCommand, selectAddCommand } from "./commands/select";
27
27
  import { chatSendCommand } from "./commands/chat";
28
28
  import { initCommand, parseInitArgs } from "./commands/init";
29
+ import { parseExpression } from "./parseExpression";
29
30
 
30
31
  /**
31
- * Parse and execute an expression
32
+ * Execute a parsed expression
32
33
  */
33
34
  async function executeExpression(
34
35
  expression: string,
35
36
  flags: CLIFlags
36
37
  ): Promise<{ success: boolean; data?: unknown; output?: string; error?: string }> {
37
- const expr = expression.trim();
38
+ const parsed = parseExpression(expression);
38
39
 
39
- // Parse command and arguments
40
- // Expressions can be: "windows", "builder {json}", "prompt get", etc.
41
- const match = expr.match(/^(\w+)(?:\s+(.*))?$/);
42
- if (!match) {
40
+ if (!parsed.command) {
43
41
  return { success: false, error: `Invalid expression: ${expression}` };
44
42
  }
45
43
 
46
- const [, command, args] = match;
47
-
48
- switch (command) {
44
+ switch (parsed.command) {
49
45
  case "windows":
50
46
  return await windowsCommand();
51
47
 
52
- case "builder":
48
+ case "builder": {
53
49
  if (!flags.window) {
54
50
  return { success: false, error: "builder requires -w <window>" };
55
51
  }
56
- return await builderCommand(flags.window, args);
52
+ // Pass the first positional (summary) and flags
53
+ const summary = parsed.positional[0];
54
+ return await builderCommand(flags.window, summary, parsed.flags);
55
+ }
57
56
 
58
57
  case "prompt": {
59
58
  if (!flags.window || !flags.tab) {
@@ -63,19 +62,21 @@ async function executeExpression(
63
62
  };
64
63
  }
65
64
 
66
- // Parse subcommand: "get", "export <file>"
67
- const promptArgs = args?.trim();
68
- if (!promptArgs || promptArgs === "get") {
65
+ const subcommand = parsed.subcommand || "get";
66
+
67
+ if (subcommand === "get") {
69
68
  return await promptGetCommand(flags.window, flags.tab);
70
69
  }
71
70
 
72
- if (promptArgs.startsWith("export ")) {
73
- // Extract file path - may be quoted with shlex.quote
74
- const filePath = promptArgs.slice(7).trim().replace(/^'|'$/g, "");
71
+ if (subcommand === "export") {
72
+ const filePath = parsed.positional[0];
73
+ if (!filePath) {
74
+ return { success: false, error: "prompt export requires a file path" };
75
+ }
75
76
  return await promptExportCommand(flags.window, flags.tab, filePath);
76
77
  }
77
78
 
78
- return { success: false, error: `Unknown prompt subcommand: ${promptArgs}` };
79
+ return { success: false, error: `Unknown prompt subcommand: ${subcommand}` };
79
80
  }
80
81
 
81
82
  case "select": {
@@ -86,49 +87,55 @@ async function executeExpression(
86
87
  };
87
88
  }
88
89
 
89
- // Parse subcommand: "get", "add <paths>"
90
- const selectArgs = args?.trim();
91
- if (!selectArgs || selectArgs === "get") {
90
+ const subcommand = parsed.subcommand || "get";
91
+
92
+ if (subcommand === "get") {
92
93
  return await selectGetCommand(flags.window, flags.tab);
93
94
  }
94
95
 
95
- if (selectArgs.startsWith("add ")) {
96
- const pathsArg = selectArgs.slice(4).trim();
97
- return await selectAddCommand(flags.window, flags.tab, pathsArg);
96
+ if (subcommand === "add") {
97
+ // All remaining positionals are paths
98
+ const paths = parsed.positional.join(" ");
99
+ if (!paths) {
100
+ return { success: false, error: "select add requires file paths" };
101
+ }
102
+ return await selectAddCommand(flags.window, flags.tab, paths);
98
103
  }
99
104
 
100
- return { success: false, error: `Unknown select subcommand: ${selectArgs}` };
105
+ return { success: false, error: `Unknown select subcommand: ${subcommand}` };
101
106
  }
102
107
 
103
108
  case "call": {
104
109
  // Handle "call prompt {json}" and "call chat_send {json}"
105
- if (args?.startsWith("prompt ")) {
110
+ const callTarget = parsed.subcommand || parsed.positional[0];
111
+
112
+ if (callTarget === "prompt") {
106
113
  if (!flags.window || !flags.tab) {
107
114
  return {
108
115
  success: false,
109
116
  error: "prompt requires -w <window> -t <tab>",
110
117
  };
111
118
  }
112
- const payload = args.slice(7).trim();
119
+ const payload = parsed.positional.slice(1).join(" ") || "{}";
113
120
  return await promptSetCommand(flags.window, flags.tab, payload);
114
121
  }
115
122
 
116
- if (args?.startsWith("chat_send")) {
123
+ if (callTarget === "chat_send") {
117
124
  if (!flags.window || !flags.tab) {
118
125
  return {
119
126
  success: false,
120
127
  error: "chat_send requires -w <window> -t <tab>",
121
128
  };
122
129
  }
123
- // Extract payload - "chat_send {json}" or "chat_send"
124
- const payload = args.slice(9).trim() || "{}";
130
+ const payload = parsed.positional.slice(1).join(" ") || "{}";
125
131
  return await chatSendCommand(flags.window, flags.tab, payload);
126
132
  }
127
- return { success: false, error: `Unknown call: ${args}` };
133
+
134
+ return { success: false, error: `Unknown call: ${callTarget}` };
128
135
  }
129
136
 
130
137
  default:
131
- return { success: false, error: `Unknown command: ${command}` };
138
+ return { success: false, error: `Unknown command: ${parsed.command}` };
132
139
  }
133
140
  }
134
141
 
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Builder command - create a new tab
3
3
  *
4
- * Parses JSON arg: {summary: string}
5
4
  * Returns: Tab: <uuid>
6
5
  * Compatible with flowctl.py parsing at line 255-259:
7
6
  * match = re.search(r"Tab:\s*([A-Za-z0-9-]+)", output)
8
7
  */
9
8
 
10
9
  import { createTab, getWindow } from "../state";
11
- import type { BuilderConfig } from "../types";
12
10
 
13
11
  /**
14
12
  * Builder command response
@@ -18,43 +16,11 @@ export interface BuilderResponse {
18
16
  }
19
17
 
20
18
  /**
21
- * Parse builder JSON argument
22
- * Handles formats:
23
- * - {} or {"summary": "..."} (JSON object)
24
- * - "summary text" (JSON string - flowctl format)
25
- * - "summary" --response-type review (flowctl with flags - flags ignored)
26
- *
27
- * @param args - JSON string or object
28
- * @returns Parsed BuilderConfig or null if invalid
19
+ * Builder flags from expression parser
29
20
  */
30
- function parseBuilderArgs(args?: string): BuilderConfig | null {
31
- if (!args) {
32
- // Empty config is valid - just creates a blank tab
33
- return {};
34
- }
35
-
36
- let jsonPart = args.trim();
37
-
38
- // Strip --response-type flag if present (not supported, but don't fail)
39
- // flowctl passes: "summary" --response-type review
40
- const responseTypeMatch = jsonPart.match(/^(.+?)\s+--response-type\s+\w+$/);
41
- if (responseTypeMatch) {
42
- jsonPart = responseTypeMatch[1].trim();
43
- }
44
-
45
- try {
46
- const parsed = JSON.parse(jsonPart);
47
-
48
- // If parsed is a string, convert to BuilderConfig with summary
49
- if (typeof parsed === "string") {
50
- return { summary: parsed };
51
- }
52
-
53
- // Otherwise expect an object
54
- return parsed as BuilderConfig;
55
- } catch {
56
- return null;
57
- }
21
+ export interface BuilderFlags {
22
+ "response-type"?: string;
23
+ [key: string]: string | boolean | undefined;
58
24
  }
59
25
 
60
26
  /**
@@ -62,12 +28,14 @@ function parseBuilderArgs(args?: string): BuilderConfig | null {
62
28
  * Creates a new tab in the specified window
63
29
  *
64
30
  * @param windowId - The window ID to create the tab in
65
- * @param args - JSON string with optional summary, path, name
31
+ * @param summary - Optional summary/description for the tab
32
+ * @param flags - Optional flags (e.g., --response-type)
66
33
  * @returns Tab: <uuid> on success
67
34
  */
68
35
  export async function builderCommand(
69
36
  windowId: number,
70
- args?: string
37
+ summary?: string,
38
+ flags?: BuilderFlags
71
39
  ): Promise<{
72
40
  success: boolean;
73
41
  data?: BuilderResponse;
@@ -78,18 +46,13 @@ export async function builderCommand(
78
46
  // Verify window exists
79
47
  await getWindow(windowId);
80
48
 
81
- // Parse builder config (optional)
82
- const config = parseBuilderArgs(args);
83
- if (config === null) {
84
- return {
85
- success: false,
86
- error: `Invalid builder config JSON: ${args}`,
87
- };
88
- }
89
-
90
49
  // Create the tab
91
50
  const tab = await createTab(windowId);
92
51
 
52
+ // Note: summary and flags like --response-type are accepted but
53
+ // not used in this minimal implementation. The real RepoPrompt
54
+ // uses them for its GUI features.
55
+
93
56
  // Return in the format flowctl.py expects: Tab: <uuid>
94
57
  return {
95
58
  success: true,
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Expression parser for wdyt CLI
3
+ *
4
+ * Properly parses shell-like expressions into command + args + flags
5
+ * Example: 'builder "summary text" --response-type review'
6
+ * -> { command: "builder", positional: ["summary text"], flags: { responseType: "review" } }
7
+ */
8
+
9
+ import { parseArgs } from "node:util";
10
+
11
+ /**
12
+ * Tokenize a shell-like string, respecting quotes
13
+ * "builder \"hello world\" --flag" -> ["builder", "hello world", "--flag"]
14
+ */
15
+ export function tokenize(input: string): string[] {
16
+ const tokens: string[] = [];
17
+ let current = "";
18
+ let inQuote: string | null = null;
19
+ let escape = false;
20
+
21
+ for (let i = 0; i < input.length; i++) {
22
+ const char = input[i];
23
+
24
+ if (escape) {
25
+ current += char;
26
+ escape = false;
27
+ continue;
28
+ }
29
+
30
+ if (char === "\\") {
31
+ escape = true;
32
+ continue;
33
+ }
34
+
35
+ if (char === '"' || char === "'") {
36
+ if (inQuote === char) {
37
+ // End of quoted string
38
+ inQuote = null;
39
+ } else if (inQuote === null) {
40
+ // Start of quoted string
41
+ inQuote = char;
42
+ } else {
43
+ // Different quote inside a quoted string
44
+ current += char;
45
+ }
46
+ continue;
47
+ }
48
+
49
+ if (char === " " && inQuote === null) {
50
+ if (current) {
51
+ tokens.push(current);
52
+ current = "";
53
+ }
54
+ continue;
55
+ }
56
+
57
+ current += char;
58
+ }
59
+
60
+ if (current) {
61
+ tokens.push(current);
62
+ }
63
+
64
+ return tokens;
65
+ }
66
+
67
+ /**
68
+ * Parsed expression result
69
+ */
70
+ export interface ParsedExpression {
71
+ command: string;
72
+ subcommand?: string;
73
+ positional: string[];
74
+ flags: Record<string, string | boolean>;
75
+ }
76
+
77
+ /**
78
+ * Parse an expression string into structured parts
79
+ */
80
+ export function parseExpression(expression: string): ParsedExpression {
81
+ const tokens = tokenize(expression.trim());
82
+
83
+ if (tokens.length === 0) {
84
+ return { command: "", positional: [], flags: {} };
85
+ }
86
+
87
+ const command = tokens[0];
88
+ const rest = tokens.slice(1);
89
+
90
+ // Use parseArgs for the remaining tokens
91
+ const { values, positionals } = parseArgs({
92
+ args: rest,
93
+ options: {
94
+ "response-type": { type: "string" },
95
+ "new-chat": { type: "boolean" },
96
+ "chat-name": { type: "string" },
97
+ "chat-id": { type: "string" },
98
+ },
99
+ allowPositionals: true,
100
+ strict: false, // Don't error on unknown flags
101
+ });
102
+
103
+ // Determine subcommand for commands that have them
104
+ let subcommand: string | undefined;
105
+ let finalPositional = positionals;
106
+
107
+ if (command === "prompt" || command === "select") {
108
+ // First positional is the subcommand (get, set, add, export)
109
+ if (positionals.length > 0) {
110
+ subcommand = positionals[0];
111
+ finalPositional = positionals.slice(1);
112
+ }
113
+ }
114
+
115
+ return {
116
+ command,
117
+ subcommand,
118
+ positional: finalPositional,
119
+ flags: values as Record<string, string | boolean>,
120
+ };
121
+ }