wdyt 0.1.10 → 0.1.13
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 +3 -2
- package/src/cli.ts +39 -32
- package/src/commands/builder.ts +12 -49
- package/src/parseExpression.ts +95 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wdyt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Code review context builder for LLMs - what do you think?",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"prepublishOnly": "bun test"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"citty": "^0.1.6"
|
|
48
|
+
"citty": "^0.1.6",
|
|
49
|
+
"shell-quote": "^1.8.3"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/bun": "^1.1.0"
|
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
|
-
*
|
|
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
|
|
38
|
+
const parsed = parseExpression(expression);
|
|
38
39
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
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 (
|
|
73
|
-
|
|
74
|
-
|
|
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: ${
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
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 (
|
|
96
|
-
|
|
97
|
-
|
|
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: ${
|
|
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
|
-
|
|
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 =
|
|
119
|
+
const payload = parsed.positional.slice(1).join(" ") || "{}";
|
|
113
120
|
return await promptSetCommand(flags.window, flags.tab, payload);
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/commands/builder.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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,95 @@
|
|
|
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
|
+
import { parse as shellParse } from "shell-quote";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tokenize a shell-like string
|
|
14
|
+
*
|
|
15
|
+
* Uses shell-quote for most parsing, but preserves JSON objects literally
|
|
16
|
+
* since shell-quote would strip internal quotes from JSON.
|
|
17
|
+
*/
|
|
18
|
+
export function tokenize(input: string): string[] {
|
|
19
|
+
// Check if input contains a JSON object (starts with {)
|
|
20
|
+
const jsonStart = input.indexOf("{");
|
|
21
|
+
|
|
22
|
+
if (jsonStart !== -1) {
|
|
23
|
+
// Split into pre-JSON and JSON parts
|
|
24
|
+
const preJson = input.slice(0, jsonStart).trim();
|
|
25
|
+
const jsonPart = input.slice(jsonStart);
|
|
26
|
+
|
|
27
|
+
// Parse the pre-JSON part with shell-quote
|
|
28
|
+
const preTokens = preJson
|
|
29
|
+
? shellParse(preJson).filter((t): t is string => typeof t === "string")
|
|
30
|
+
: [];
|
|
31
|
+
|
|
32
|
+
// Keep the JSON part as-is
|
|
33
|
+
return [...preTokens, jsonPart];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// No JSON, use shell-quote for everything
|
|
37
|
+
const parsed = shellParse(input);
|
|
38
|
+
return parsed.filter((t): t is string => typeof t === "string");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parsed expression result
|
|
43
|
+
*/
|
|
44
|
+
export interface ParsedExpression {
|
|
45
|
+
command: string;
|
|
46
|
+
subcommand?: string;
|
|
47
|
+
positional: string[];
|
|
48
|
+
flags: Record<string, string | boolean>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse an expression string into structured parts
|
|
53
|
+
*/
|
|
54
|
+
export function parseExpression(expression: string): ParsedExpression {
|
|
55
|
+
const tokens = tokenize(expression.trim());
|
|
56
|
+
|
|
57
|
+
if (tokens.length === 0) {
|
|
58
|
+
return { command: "", positional: [], flags: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const command = tokens[0];
|
|
62
|
+
const rest = tokens.slice(1);
|
|
63
|
+
|
|
64
|
+
// Use parseArgs for the remaining tokens
|
|
65
|
+
const { values, positionals } = parseArgs({
|
|
66
|
+
args: rest,
|
|
67
|
+
options: {
|
|
68
|
+
"response-type": { type: "string" },
|
|
69
|
+
"new-chat": { type: "boolean" },
|
|
70
|
+
"chat-name": { type: "string" },
|
|
71
|
+
"chat-id": { type: "string" },
|
|
72
|
+
},
|
|
73
|
+
allowPositionals: true,
|
|
74
|
+
strict: false, // Don't error on unknown flags
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Determine subcommand for commands that have them
|
|
78
|
+
let subcommand: string | undefined;
|
|
79
|
+
let finalPositional = positionals;
|
|
80
|
+
|
|
81
|
+
if (command === "prompt" || command === "select") {
|
|
82
|
+
// First positional is the subcommand (get, set, add, export)
|
|
83
|
+
if (positionals.length > 0) {
|
|
84
|
+
subcommand = positionals[0];
|
|
85
|
+
finalPositional = positionals.slice(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
command,
|
|
91
|
+
subcommand,
|
|
92
|
+
positional: finalPositional,
|
|
93
|
+
flags: values as Record<string, string | boolean>,
|
|
94
|
+
};
|
|
95
|
+
}
|