whatnext 1.0.0
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/dist/index.js +187 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import readline from "readline";
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
var CONFIG_DIR = path.join(os.homedir(), ".whatnext");
|
|
13
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
14
|
+
function loadConfig() {
|
|
15
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Config file not found at ${CONFIG_FILE}. Run "whatnext login" to configure the CLI.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
21
|
+
const config = JSON.parse(raw);
|
|
22
|
+
if (!config.api_url || !config.api_key) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Invalid config file at ${CONFIG_FILE}. Run "whatnext login" to reconfigure.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function saveConfig(config) {
|
|
30
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/parse.ts
|
|
35
|
+
function parseOptions(optionsStr) {
|
|
36
|
+
return optionsStr.split(",").map((s) => ({ label: s.trim() }));
|
|
37
|
+
}
|
|
38
|
+
function inferDecisionType(options) {
|
|
39
|
+
return options ? "select" : "free_text";
|
|
40
|
+
}
|
|
41
|
+
function parseMetadata(metadataStr) {
|
|
42
|
+
if (!metadataStr) return void 0;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(metadataStr);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`Invalid JSON in --metadata: ${metadataStr}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/index.ts
|
|
51
|
+
function prompt(question, defaultValue) {
|
|
52
|
+
const rl = readline.createInterface({
|
|
53
|
+
input: process.stdin,
|
|
54
|
+
output: process.stderr
|
|
55
|
+
});
|
|
56
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
59
|
+
rl.close();
|
|
60
|
+
resolve(answer.trim() || defaultValue || "");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
var program = new Command();
|
|
65
|
+
program.name("whatnext").description("Human-in-the-loop decision routing for coding agents");
|
|
66
|
+
program.command("login").description("Configure the CLI with API URL and key").action(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const apiUrl = await prompt("API URL", "http://localhost:3001");
|
|
69
|
+
const apiKey = await prompt("API Key");
|
|
70
|
+
if (!apiKey) {
|
|
71
|
+
console.error("API key is required.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
saveConfig({ api_url: apiUrl, api_key: apiKey });
|
|
75
|
+
console.error("Configuration saved successfully.");
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error("Login failed:", err instanceof Error ? err.message : err);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
program.command("ask").description("Send a decision request and wait for a response").requiredOption("--message <text>", "Decision message").option("--options <items>", "Comma-separated options (makes it a select type)").option("--ttl <duration>", "Time-to-live", "10m").option("--metadata <json>", "JSON metadata").action(async (opts) => {
|
|
82
|
+
try {
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
const type = inferDecisionType(opts.options);
|
|
85
|
+
const metadata = parseMetadata(opts.metadata);
|
|
86
|
+
const body = {
|
|
87
|
+
message: opts.message,
|
|
88
|
+
type,
|
|
89
|
+
ttl: opts.ttl,
|
|
90
|
+
...metadata && { metadata },
|
|
91
|
+
...type === "select" && { options: parseOptions(opts.options) }
|
|
92
|
+
};
|
|
93
|
+
const res = await fetch(`${config.api_url}/api/decisions`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
Authorization: `Bearer ${config.api_key}`
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify(body)
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
console.error(`API error (${res.status}): ${text}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const decision = await res.json();
|
|
107
|
+
const wsUrl = `${config.api_url.replace("http", "ws")}/ws?decision_id=${decision.id}`;
|
|
108
|
+
const ws = new WebSocket(wsUrl, {
|
|
109
|
+
headers: { Authorization: `Bearer ${config.api_key}` }
|
|
110
|
+
});
|
|
111
|
+
console.error(`Waiting for response... (expires in ${opts.ttl})`);
|
|
112
|
+
ws.on("message", (data) => {
|
|
113
|
+
try {
|
|
114
|
+
const msg = JSON.parse(data.toString());
|
|
115
|
+
if (msg.type === "decision_response") {
|
|
116
|
+
const payload = msg.payload;
|
|
117
|
+
const label = payload.selected_option?.label;
|
|
118
|
+
const text = payload.text;
|
|
119
|
+
if (label && text) {
|
|
120
|
+
console.log(`${label}: ${text}`);
|
|
121
|
+
} else if (label) {
|
|
122
|
+
console.log(label);
|
|
123
|
+
} else if (text) {
|
|
124
|
+
console.log(text);
|
|
125
|
+
}
|
|
126
|
+
ws.close();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
if (msg.type === "decision_expired") {
|
|
130
|
+
console.error("Decision expired");
|
|
131
|
+
ws.close();
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (msg.type === "error") {
|
|
135
|
+
console.error("Server error:", JSON.stringify(msg.payload));
|
|
136
|
+
ws.close();
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
} catch (parseErr) {
|
|
140
|
+
console.error("Failed to parse WebSocket message:", parseErr);
|
|
141
|
+
ws.close();
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
ws.on("error", (err) => {
|
|
146
|
+
console.error("WebSocket error:", err.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
149
|
+
ws.on("close", (code, reason) => {
|
|
150
|
+
if (code !== 1e3) {
|
|
151
|
+
console.error(`WebSocket closed unexpectedly (code ${code}): ${reason.toString()}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
program.command("notify").description("Send a fire-and-forget notification").requiredOption("--message <text>", "Notification message").option("--metadata <json>", "JSON metadata").action(async (opts) => {
|
|
161
|
+
try {
|
|
162
|
+
const config = loadConfig();
|
|
163
|
+
const metadata = parseMetadata(opts.metadata);
|
|
164
|
+
const body = {
|
|
165
|
+
message: opts.message,
|
|
166
|
+
...metadata && { metadata }
|
|
167
|
+
};
|
|
168
|
+
const res = await fetch(`${config.api_url}/api/notifications`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
Authorization: `Bearer ${config.api_key}`
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(body)
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const text = await res.text();
|
|
178
|
+
console.error(`API error (${res.status}): ${text}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
console.error("Notification sent");
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whatnext",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Human-in-the-loop decision routing for coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"whatnext": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"test": "vitest run"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^13.0.0",
|
|
19
|
+
"ws": "^8.18.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/ws": "^8.5.0",
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"@whatnext/shared": "*",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"tsx": "^4.19.0",
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"vitest": "^3.0.0"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/marcussanatan/whatnext.git",
|
|
34
|
+
"directory": "packages/cli"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"cli",
|
|
38
|
+
"human-in-the-loop",
|
|
39
|
+
"agent",
|
|
40
|
+
"decision",
|
|
41
|
+
"coding-agent"
|
|
42
|
+
]
|
|
43
|
+
}
|