prompt2task 0.1.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/LICENSE +0 -0
- package/README.md +170 -0
- package/dist/chunk-UKLTABSE.js +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1326 -0
- package/dist/prompt-builder-X7TCYJ6E.js +7 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildStructuredPrompt
|
|
4
|
+
} from "./chunk-UKLTABSE.js";
|
|
5
|
+
|
|
6
|
+
// src/cli/index.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk9 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/cli/commands/init.ts
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
|
|
13
|
+
// src/config/config-manager.ts
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import path2 from "path";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
// src/utils/paths.ts
|
|
19
|
+
import os from "os";
|
|
20
|
+
import path from "path";
|
|
21
|
+
function getAppDir() {
|
|
22
|
+
return path.join(os.homedir(), ".prompt2task");
|
|
23
|
+
}
|
|
24
|
+
function getConfigPath() {
|
|
25
|
+
return path.join(getAppDir(), "config.json");
|
|
26
|
+
}
|
|
27
|
+
function getDbPath() {
|
|
28
|
+
return path.join(getAppDir(), "history.db");
|
|
29
|
+
}
|
|
30
|
+
function getLogsDir() {
|
|
31
|
+
return path.join(getAppDir(), "logs");
|
|
32
|
+
}
|
|
33
|
+
function getCredentialsPath() {
|
|
34
|
+
return path.join(getAppDir(), "credentials.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/utils/errors.ts
|
|
38
|
+
var AppError = class extends Error {
|
|
39
|
+
constructor(message, code, hint) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.hint = hint;
|
|
43
|
+
this.name = "AppError";
|
|
44
|
+
}
|
|
45
|
+
code;
|
|
46
|
+
hint;
|
|
47
|
+
};
|
|
48
|
+
var NotInitializedError = class extends AppError {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(
|
|
51
|
+
"prompt2task has not been initialized. Run `prompt2task init` first.",
|
|
52
|
+
"NOT_INITIALIZED",
|
|
53
|
+
"Run: prompt2task init"
|
|
54
|
+
);
|
|
55
|
+
this.name = "NotInitializedError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ConfigError = class extends AppError {
|
|
59
|
+
constructor(message, hint) {
|
|
60
|
+
super(message, "CONFIG_ERROR", hint);
|
|
61
|
+
this.name = "ConfigError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var CredentialError = class extends AppError {
|
|
65
|
+
constructor(message, hint) {
|
|
66
|
+
super(message, "CREDENTIAL_ERROR", hint);
|
|
67
|
+
this.name = "CredentialError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var ProviderError = class extends AppError {
|
|
71
|
+
constructor(message, hint) {
|
|
72
|
+
super(message, "PROVIDER_ERROR", hint);
|
|
73
|
+
this.name = "ProviderError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/config/config-manager.ts
|
|
78
|
+
var configSchema = z.object({
|
|
79
|
+
version: z.number().default(1),
|
|
80
|
+
provider: z.enum(["openai", "claude"]),
|
|
81
|
+
model: z.string().min(1),
|
|
82
|
+
projectType: z.string().min(1)
|
|
83
|
+
});
|
|
84
|
+
function ensureAppDir() {
|
|
85
|
+
const dir = getAppDir();
|
|
86
|
+
if (!fs.existsSync(dir)) {
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
fs.chmodSync(dir, 448);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
const logsDir = path2.join(dir, "logs");
|
|
94
|
+
if (!fs.existsSync(logsDir)) {
|
|
95
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function configExists() {
|
|
99
|
+
return fs.existsSync(getConfigPath());
|
|
100
|
+
}
|
|
101
|
+
function loadConfig() {
|
|
102
|
+
const configPath = getConfigPath();
|
|
103
|
+
if (!fs.existsSync(configPath)) {
|
|
104
|
+
throw new ConfigError(
|
|
105
|
+
"Configuration not found. Run `prompt2task init` to set up.",
|
|
106
|
+
"Run: prompt2task init"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
111
|
+
const parsed = JSON.parse(raw);
|
|
112
|
+
return configSchema.parse(parsed);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err instanceof z.ZodError) {
|
|
115
|
+
throw new ConfigError(
|
|
116
|
+
`Invalid configuration: ${err.errors.map((e) => e.message).join(", ")}`,
|
|
117
|
+
"Run `prompt2task init` to reconfigure or fix ~/.prompt2task/config.json"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (err instanceof ConfigError) throw err;
|
|
121
|
+
throw new ConfigError(
|
|
122
|
+
`Failed to load configuration: ${err.message}`,
|
|
123
|
+
"Check ~/.prompt2task/config.json is valid JSON"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function saveConfig(config) {
|
|
128
|
+
ensureAppDir();
|
|
129
|
+
const parsed = configSchema.parse(config);
|
|
130
|
+
const configPath = getConfigPath();
|
|
131
|
+
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), { mode: 384 });
|
|
132
|
+
try {
|
|
133
|
+
fs.chmodSync(configPath, 384);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/config/credentials.ts
|
|
139
|
+
import fs2 from "fs";
|
|
140
|
+
var ENV_MAP = {
|
|
141
|
+
openai: "OPENAI_API_KEY",
|
|
142
|
+
claude: "ANTHROPIC_API_KEY"
|
|
143
|
+
};
|
|
144
|
+
function ensureCredentialsFile() {
|
|
145
|
+
const credPath = getCredentialsPath();
|
|
146
|
+
if (!fs2.existsSync(credPath)) {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const raw = fs2.readFileSync(credPath, "utf-8");
|
|
151
|
+
if (!raw.trim()) return {};
|
|
152
|
+
return JSON.parse(raw);
|
|
153
|
+
} catch {
|
|
154
|
+
throw new CredentialError(
|
|
155
|
+
"Credentials file is corrupted.",
|
|
156
|
+
"Try removing ~/.prompt2task/credentials.json and re-running `prompt2task provider`"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function getCredential(provider) {
|
|
161
|
+
const envKey = ENV_MAP[provider];
|
|
162
|
+
if (envKey && process.env[envKey]) {
|
|
163
|
+
const val = process.env[envKey].trim();
|
|
164
|
+
if (val) return val;
|
|
165
|
+
}
|
|
166
|
+
const file = ensureCredentialsFile();
|
|
167
|
+
const key = file[provider];
|
|
168
|
+
if (key && typeof key === "string" && key.trim()) return key.trim();
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function saveCredential(provider, apiKey) {
|
|
172
|
+
if (!apiKey || !apiKey.trim()) {
|
|
173
|
+
throw new CredentialError("API key cannot be empty.");
|
|
174
|
+
}
|
|
175
|
+
const dir = getAppDir();
|
|
176
|
+
if (!fs2.existsSync(dir)) {
|
|
177
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
const credPath = getCredentialsPath();
|
|
180
|
+
const existing = ensureCredentialsFile();
|
|
181
|
+
existing[provider] = apiKey.trim();
|
|
182
|
+
fs2.writeFileSync(credPath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
183
|
+
try {
|
|
184
|
+
fs2.chmodSync(credPath, 384);
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
fs2.chmodSync(dir, 448);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function removeCredential(provider) {
|
|
193
|
+
const credPath = getCredentialsPath();
|
|
194
|
+
if (!fs2.existsSync(credPath)) return false;
|
|
195
|
+
const existing = ensureCredentialsFile();
|
|
196
|
+
if (!(provider in existing)) return false;
|
|
197
|
+
delete existing[provider];
|
|
198
|
+
fs2.writeFileSync(credPath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
199
|
+
try {
|
|
200
|
+
fs2.chmodSync(credPath, 384);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function removeAllCredentials() {
|
|
206
|
+
const credPath = getCredentialsPath();
|
|
207
|
+
if (fs2.existsSync(credPath)) {
|
|
208
|
+
fs2.unlinkSync(credPath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/storage/database.ts
|
|
213
|
+
import fs3 from "fs";
|
|
214
|
+
import path3 from "path";
|
|
215
|
+
import { createRequire } from "module";
|
|
216
|
+
var require2 = createRequire(import.meta.url);
|
|
217
|
+
var { DatabaseSync } = require2("node:sqlite");
|
|
218
|
+
var _db = null;
|
|
219
|
+
function getDatabase(dbPath) {
|
|
220
|
+
if (_db && !dbPath) return _db;
|
|
221
|
+
const resolvedPath = dbPath ?? getDbPath();
|
|
222
|
+
const dir = path3.dirname(resolvedPath);
|
|
223
|
+
if (!fs3.existsSync(dir)) {
|
|
224
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
const logsDir = getLogsDir();
|
|
227
|
+
if (!fs3.existsSync(logsDir)) {
|
|
228
|
+
fs3.mkdirSync(logsDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
const sqlite = new DatabaseSync(resolvedPath);
|
|
231
|
+
runMigrations(sqlite);
|
|
232
|
+
if (!dbPath) {
|
|
233
|
+
_db = sqlite;
|
|
234
|
+
}
|
|
235
|
+
return sqlite;
|
|
236
|
+
}
|
|
237
|
+
function runMigrations(sqlite) {
|
|
238
|
+
sqlite.exec(`
|
|
239
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
240
|
+
id TEXT PRIMARY KEY,
|
|
241
|
+
prompt TEXT NOT NULL,
|
|
242
|
+
status TEXT NOT NULL,
|
|
243
|
+
provider TEXT NOT NULL,
|
|
244
|
+
model TEXT NOT NULL,
|
|
245
|
+
project_type TEXT,
|
|
246
|
+
project_path TEXT,
|
|
247
|
+
response TEXT,
|
|
248
|
+
error TEXT,
|
|
249
|
+
created_at TEXT NOT NULL,
|
|
250
|
+
completed_at TEXT
|
|
251
|
+
);
|
|
252
|
+
`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/cli/ui/prompts.ts
|
|
256
|
+
import { select, password } from "@inquirer/prompts";
|
|
257
|
+
async function promptProvider() {
|
|
258
|
+
const answer = await select({
|
|
259
|
+
message: "AI provider",
|
|
260
|
+
choices: [
|
|
261
|
+
{ name: "OpenAI", value: "openai" },
|
|
262
|
+
{ name: "Claude", value: "claude" }
|
|
263
|
+
]
|
|
264
|
+
});
|
|
265
|
+
return answer;
|
|
266
|
+
}
|
|
267
|
+
async function promptApiKey(provider) {
|
|
268
|
+
const answer = await password({
|
|
269
|
+
message: `${provider === "openai" ? "OpenAI" : "Claude"} API Key:`,
|
|
270
|
+
mask: "*",
|
|
271
|
+
validate: (val) => val.trim().length > 0 ? true : "API key cannot be empty"
|
|
272
|
+
});
|
|
273
|
+
return answer.trim();
|
|
274
|
+
}
|
|
275
|
+
async function promptModel(provider) {
|
|
276
|
+
const models = provider === "openai" ? [
|
|
277
|
+
{ name: "GPT-5", value: "gpt-5" },
|
|
278
|
+
{ name: "GPT-5-mini", value: "gpt-5-mini" },
|
|
279
|
+
{ name: "GPT-4o", value: "gpt-4o" },
|
|
280
|
+
{ name: "Other", value: "__other" }
|
|
281
|
+
] : [
|
|
282
|
+
{ name: "Claude 3.5 Sonnet", value: "claude-3-5-sonnet-20241022" },
|
|
283
|
+
{ name: "Claude 3.5 Haiku", value: "claude-3-5-haiku-20241022" },
|
|
284
|
+
{ name: "Claude 3 Opus", value: "claude-3-opus-20240229" },
|
|
285
|
+
{ name: "Other", value: "__other" }
|
|
286
|
+
];
|
|
287
|
+
const answer = await select({
|
|
288
|
+
message: "Default model",
|
|
289
|
+
choices: models
|
|
290
|
+
});
|
|
291
|
+
if (answer === "__other") {
|
|
292
|
+
const { input } = await import("@inquirer/prompts");
|
|
293
|
+
const custom = await input({
|
|
294
|
+
message: "Enter model name:",
|
|
295
|
+
validate: (val) => val.trim() ? true : "Model cannot be empty"
|
|
296
|
+
});
|
|
297
|
+
return custom.trim();
|
|
298
|
+
}
|
|
299
|
+
return answer;
|
|
300
|
+
}
|
|
301
|
+
async function promptProjectType() {
|
|
302
|
+
const answer = await select({
|
|
303
|
+
message: "What do you normally build?",
|
|
304
|
+
choices: [
|
|
305
|
+
{ name: "Web applications", value: "web-applications" },
|
|
306
|
+
{ name: "WordPress", value: "wordpress" },
|
|
307
|
+
{ name: "Laravel", value: "laravel" },
|
|
308
|
+
{ name: "React", value: "react" },
|
|
309
|
+
{ name: "Other", value: "other" }
|
|
310
|
+
]
|
|
311
|
+
});
|
|
312
|
+
if (answer === "other") {
|
|
313
|
+
const { input } = await import("@inquirer/prompts");
|
|
314
|
+
const custom = await input({
|
|
315
|
+
message: "Enter project type:",
|
|
316
|
+
validate: (val) => val.trim() ? true : "Project type cannot be empty"
|
|
317
|
+
});
|
|
318
|
+
return custom.trim().toLowerCase().replace(/\s+/g, "-");
|
|
319
|
+
}
|
|
320
|
+
return answer;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/cli/ui/spinner.ts
|
|
324
|
+
import ora from "ora";
|
|
325
|
+
function createSpinner(text) {
|
|
326
|
+
return ora({ text }).start();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/cli/commands/init.ts
|
|
330
|
+
async function initCommand(options) {
|
|
331
|
+
console.log(chalk.bold("Let's get you set up.\n"));
|
|
332
|
+
const alreadyExists = configExists();
|
|
333
|
+
if (alreadyExists && !options?.force) {
|
|
334
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
335
|
+
const shouldReconfigure = await confirm({
|
|
336
|
+
message: "Configuration already exists. Reconfigure?",
|
|
337
|
+
default: false
|
|
338
|
+
});
|
|
339
|
+
if (!shouldReconfigure) {
|
|
340
|
+
console.log(chalk.dim("Aborted. Existing configuration preserved."));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const provider = await promptProvider();
|
|
345
|
+
let apiKey = getCredential(provider) ?? "";
|
|
346
|
+
const hasEnv = provider === "openai" ? !!process.env.OPENAI_API_KEY : !!process.env.ANTHROPIC_API_KEY;
|
|
347
|
+
if (hasEnv) {
|
|
348
|
+
console.log(chalk.dim(`Using ${provider} API key from environment variable.`));
|
|
349
|
+
apiKey = getCredential(provider);
|
|
350
|
+
} else {
|
|
351
|
+
apiKey = await promptApiKey(provider);
|
|
352
|
+
const spinner = createSpinner("Saving API key...");
|
|
353
|
+
try {
|
|
354
|
+
saveCredential(provider, apiKey);
|
|
355
|
+
spinner.succeed(chalk.green(`${provider === "openai" ? "OpenAI" : "Claude"} API key saved securely.`));
|
|
356
|
+
} catch (err) {
|
|
357
|
+
spinner.fail(chalk.red("Failed to save API key."));
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!hasEnv) {
|
|
362
|
+
} else {
|
|
363
|
+
}
|
|
364
|
+
const model = await promptModel(provider);
|
|
365
|
+
const projectType = await promptProjectType();
|
|
366
|
+
const spinner2 = createSpinner("Saving configuration...");
|
|
367
|
+
try {
|
|
368
|
+
ensureAppDir();
|
|
369
|
+
saveConfig({
|
|
370
|
+
version: 1,
|
|
371
|
+
provider,
|
|
372
|
+
model,
|
|
373
|
+
projectType
|
|
374
|
+
});
|
|
375
|
+
getDatabase();
|
|
376
|
+
spinner2.succeed(chalk.green("Setup complete."));
|
|
377
|
+
} catch (err) {
|
|
378
|
+
spinner2.fail(chalk.red("Failed to save configuration."));
|
|
379
|
+
throw err;
|
|
380
|
+
}
|
|
381
|
+
console.log("");
|
|
382
|
+
console.log(chalk.dim(`Config: ~/.prompt2task/config.json`));
|
|
383
|
+
console.log(chalk.dim(`History: ~/.prompt2task/history.db`));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/cli/commands/run.ts
|
|
387
|
+
import chalk2 from "chalk";
|
|
388
|
+
|
|
389
|
+
// src/storage/repositories/task-repository.ts
|
|
390
|
+
function rowToTask(row) {
|
|
391
|
+
return {
|
|
392
|
+
id: row.id,
|
|
393
|
+
prompt: row.prompt,
|
|
394
|
+
status: row.status,
|
|
395
|
+
provider: row.provider,
|
|
396
|
+
model: row.model,
|
|
397
|
+
projectType: row.project_type ?? void 0,
|
|
398
|
+
projectPath: row.project_path ?? void 0,
|
|
399
|
+
response: row.response ?? void 0,
|
|
400
|
+
error: row.error ?? void 0,
|
|
401
|
+
createdAt: row.created_at,
|
|
402
|
+
completedAt: row.completed_at ?? void 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
var TaskRepository = class {
|
|
406
|
+
constructor(db) {
|
|
407
|
+
this.db = db;
|
|
408
|
+
}
|
|
409
|
+
db;
|
|
410
|
+
create(task) {
|
|
411
|
+
const stmt = this.db.prepare(
|
|
412
|
+
`INSERT INTO tasks (id, prompt, status, provider, model, project_type, project_path, response, error, created_at, completed_at)
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
414
|
+
);
|
|
415
|
+
stmt.run(
|
|
416
|
+
task.id,
|
|
417
|
+
task.prompt,
|
|
418
|
+
task.status,
|
|
419
|
+
task.provider,
|
|
420
|
+
task.model,
|
|
421
|
+
task.projectType ?? null,
|
|
422
|
+
task.projectPath ?? null,
|
|
423
|
+
task.response ?? null,
|
|
424
|
+
task.error ?? null,
|
|
425
|
+
task.createdAt,
|
|
426
|
+
task.completedAt ?? null
|
|
427
|
+
);
|
|
428
|
+
return task;
|
|
429
|
+
}
|
|
430
|
+
update(id, patch) {
|
|
431
|
+
const existing = this.findById(id);
|
|
432
|
+
if (!existing) return null;
|
|
433
|
+
const fields = [];
|
|
434
|
+
const values = [];
|
|
435
|
+
if (patch.status !== void 0) {
|
|
436
|
+
fields.push("status = ?");
|
|
437
|
+
values.push(patch.status);
|
|
438
|
+
}
|
|
439
|
+
if (patch.prompt !== void 0) {
|
|
440
|
+
fields.push("prompt = ?");
|
|
441
|
+
values.push(patch.prompt);
|
|
442
|
+
}
|
|
443
|
+
if (patch.provider !== void 0) {
|
|
444
|
+
fields.push("provider = ?");
|
|
445
|
+
values.push(patch.provider);
|
|
446
|
+
}
|
|
447
|
+
if (patch.model !== void 0) {
|
|
448
|
+
fields.push("model = ?");
|
|
449
|
+
values.push(patch.model);
|
|
450
|
+
}
|
|
451
|
+
if (patch.projectType !== void 0) {
|
|
452
|
+
fields.push("project_type = ?");
|
|
453
|
+
values.push(patch.projectType);
|
|
454
|
+
}
|
|
455
|
+
if (patch.projectPath !== void 0) {
|
|
456
|
+
fields.push("project_path = ?");
|
|
457
|
+
values.push(patch.projectPath);
|
|
458
|
+
}
|
|
459
|
+
if (patch.response !== void 0) {
|
|
460
|
+
fields.push("response = ?");
|
|
461
|
+
values.push(patch.response);
|
|
462
|
+
}
|
|
463
|
+
if (patch.error !== void 0) {
|
|
464
|
+
fields.push("error = ?");
|
|
465
|
+
values.push(patch.error);
|
|
466
|
+
}
|
|
467
|
+
if (patch.completedAt !== void 0) {
|
|
468
|
+
fields.push("completed_at = ?");
|
|
469
|
+
values.push(patch.completedAt);
|
|
470
|
+
}
|
|
471
|
+
if (patch.createdAt !== void 0) {
|
|
472
|
+
fields.push("created_at = ?");
|
|
473
|
+
values.push(patch.createdAt);
|
|
474
|
+
}
|
|
475
|
+
if (fields.length > 0) {
|
|
476
|
+
const sql = `UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`;
|
|
477
|
+
values.push(id);
|
|
478
|
+
this.db.prepare(sql).run(...values);
|
|
479
|
+
}
|
|
480
|
+
return this.findById(id);
|
|
481
|
+
}
|
|
482
|
+
findById(id) {
|
|
483
|
+
const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
484
|
+
if (!row) return null;
|
|
485
|
+
return rowToTask(row);
|
|
486
|
+
}
|
|
487
|
+
list(options) {
|
|
488
|
+
let sql = "SELECT * FROM tasks ORDER BY created_at DESC";
|
|
489
|
+
const params = [];
|
|
490
|
+
if (options?.status) {
|
|
491
|
+
sql = "SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC";
|
|
492
|
+
params.push(options.status);
|
|
493
|
+
}
|
|
494
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
495
|
+
let filtered = rows;
|
|
496
|
+
if (options?.offset) {
|
|
497
|
+
filtered = filtered.slice(options.offset);
|
|
498
|
+
}
|
|
499
|
+
if (options?.limit) {
|
|
500
|
+
filtered = filtered.slice(0, options.limit);
|
|
501
|
+
}
|
|
502
|
+
return filtered.map(rowToTask);
|
|
503
|
+
}
|
|
504
|
+
search(query, options) {
|
|
505
|
+
const pattern = `%${query}%`;
|
|
506
|
+
const rows = this.db.prepare(
|
|
507
|
+
"SELECT * FROM tasks WHERE prompt LIKE ? OR response LIKE ? ORDER BY created_at DESC"
|
|
508
|
+
).all(pattern, pattern);
|
|
509
|
+
let result = rows.map(rowToTask);
|
|
510
|
+
if (options?.limit) result = result.slice(0, options.limit);
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
deleteAll() {
|
|
514
|
+
this.db.exec("DELETE FROM tasks");
|
|
515
|
+
}
|
|
516
|
+
count() {
|
|
517
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM tasks").get();
|
|
518
|
+
return row.cnt;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// src/utils/ids.ts
|
|
523
|
+
import { randomBytes } from "crypto";
|
|
524
|
+
function generateTaskId() {
|
|
525
|
+
const hex = randomBytes(5).toString("hex");
|
|
526
|
+
return `tsk_${hex}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/tasks/task-manager.ts
|
|
530
|
+
var TaskManager = class {
|
|
531
|
+
constructor(repo) {
|
|
532
|
+
this.repo = repo;
|
|
533
|
+
}
|
|
534
|
+
repo;
|
|
535
|
+
createTask(opts) {
|
|
536
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
537
|
+
const task = {
|
|
538
|
+
id: generateTaskId(),
|
|
539
|
+
prompt: opts.prompt,
|
|
540
|
+
status: "pending",
|
|
541
|
+
provider: opts.provider,
|
|
542
|
+
model: opts.model,
|
|
543
|
+
projectType: opts.projectType,
|
|
544
|
+
projectPath: opts.projectPath,
|
|
545
|
+
createdAt: now
|
|
546
|
+
};
|
|
547
|
+
this.repo.create(task);
|
|
548
|
+
return task;
|
|
549
|
+
}
|
|
550
|
+
updateStatus(id, status, patch) {
|
|
551
|
+
return this.repo.update(id, { status, ...patch });
|
|
552
|
+
}
|
|
553
|
+
async runTask(options) {
|
|
554
|
+
const { prompt, provider, model, providerName, projectType, projectContext } = options;
|
|
555
|
+
if (!prompt || !prompt.trim()) {
|
|
556
|
+
throw new Error("Prompt cannot be empty.");
|
|
557
|
+
}
|
|
558
|
+
const task = this.createTask({
|
|
559
|
+
prompt,
|
|
560
|
+
provider: providerName,
|
|
561
|
+
model,
|
|
562
|
+
projectType,
|
|
563
|
+
projectPath: projectContext?.path
|
|
564
|
+
});
|
|
565
|
+
this.repo.update(task.id, { status: "running" });
|
|
566
|
+
const structured = buildStructuredPrompt({
|
|
567
|
+
prompt,
|
|
568
|
+
projectType,
|
|
569
|
+
projectContext
|
|
570
|
+
});
|
|
571
|
+
try {
|
|
572
|
+
const response = await provider.sendTask({
|
|
573
|
+
prompt,
|
|
574
|
+
structuredPrompt: structured,
|
|
575
|
+
model,
|
|
576
|
+
projectType
|
|
577
|
+
});
|
|
578
|
+
const completed = this.repo.update(task.id, {
|
|
579
|
+
status: "completed",
|
|
580
|
+
response: response.content,
|
|
581
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
582
|
+
});
|
|
583
|
+
return completed ?? { ...task, status: "completed", response: response.content };
|
|
584
|
+
} catch (err) {
|
|
585
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
586
|
+
const failed = this.repo.update(task.id, {
|
|
587
|
+
status: "failed",
|
|
588
|
+
error: message,
|
|
589
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
590
|
+
});
|
|
591
|
+
throw err instanceof Error ? err : new Error(message);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/providers/openai.ts
|
|
597
|
+
var OpenAIProvider = class {
|
|
598
|
+
constructor(apiKey) {
|
|
599
|
+
this.apiKey = apiKey;
|
|
600
|
+
}
|
|
601
|
+
apiKey;
|
|
602
|
+
name = "openai";
|
|
603
|
+
getApiKey() {
|
|
604
|
+
const key = this.apiKey ?? getCredential("openai");
|
|
605
|
+
if (!key) {
|
|
606
|
+
throw new ProviderError(
|
|
607
|
+
"Missing OpenAI API key.",
|
|
608
|
+
"Set OPENAI_API_KEY env var or run `prompt2task provider`"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return key;
|
|
612
|
+
}
|
|
613
|
+
async validateCredentials() {
|
|
614
|
+
const key = this.getApiKey();
|
|
615
|
+
try {
|
|
616
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
617
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
618
|
+
});
|
|
619
|
+
return res.ok;
|
|
620
|
+
} catch {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async sendTask(task) {
|
|
625
|
+
const key = this.getApiKey();
|
|
626
|
+
const body = {
|
|
627
|
+
model: task.model,
|
|
628
|
+
messages: [{ role: "user", content: task.structuredPrompt }]
|
|
629
|
+
};
|
|
630
|
+
let res;
|
|
631
|
+
try {
|
|
632
|
+
res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: {
|
|
635
|
+
Authorization: `Bearer ${key}`,
|
|
636
|
+
"Content-Type": "application/json"
|
|
637
|
+
},
|
|
638
|
+
body: JSON.stringify(body)
|
|
639
|
+
});
|
|
640
|
+
} catch (err) {
|
|
641
|
+
throw new ProviderError(
|
|
642
|
+
`Unable to connect to OpenAI: ${err.message}`,
|
|
643
|
+
"Check your network connection"
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (res.status === 401) {
|
|
647
|
+
throw new ProviderError(
|
|
648
|
+
"OpenAI authentication failed. Invalid API key.",
|
|
649
|
+
"Check your API key with: prompt2task provider"
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
if (res.status === 429) {
|
|
653
|
+
throw new ProviderError(
|
|
654
|
+
"OpenAI rate limit exceeded. Please try again later.",
|
|
655
|
+
"Wait a moment and retry"
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
if (!res.ok) {
|
|
659
|
+
const text = await res.text().catch(() => "");
|
|
660
|
+
throw new ProviderError(
|
|
661
|
+
`OpenAI request failed (${res.status}): ${text.slice(0, 500)}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const data = await res.json();
|
|
665
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
666
|
+
if (!content) {
|
|
667
|
+
throw new ProviderError("OpenAI returned an empty response.");
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
content,
|
|
671
|
+
model: data.model ?? task.model,
|
|
672
|
+
usage: data.usage ? {
|
|
673
|
+
promptTokens: data.usage.prompt_tokens,
|
|
674
|
+
completionTokens: data.usage.completion_tokens,
|
|
675
|
+
totalTokens: data.usage.total_tokens
|
|
676
|
+
} : void 0
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/providers/claude.ts
|
|
682
|
+
var ClaudeProvider = class {
|
|
683
|
+
constructor(apiKey) {
|
|
684
|
+
this.apiKey = apiKey;
|
|
685
|
+
}
|
|
686
|
+
apiKey;
|
|
687
|
+
name = "claude";
|
|
688
|
+
getApiKey() {
|
|
689
|
+
const key = this.apiKey ?? getCredential("claude");
|
|
690
|
+
if (!key) {
|
|
691
|
+
throw new ProviderError(
|
|
692
|
+
"Missing Claude API key.",
|
|
693
|
+
"Set ANTHROPIC_API_KEY env var or run `prompt2task provider`"
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return key;
|
|
697
|
+
}
|
|
698
|
+
async validateCredentials() {
|
|
699
|
+
const key = this.getApiKey();
|
|
700
|
+
try {
|
|
701
|
+
const res = await fetch("https://api.anthropic.com/v1/models", {
|
|
702
|
+
headers: {
|
|
703
|
+
"x-api-key": key,
|
|
704
|
+
"anthropic-version": "2023-06-01"
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return res.ok;
|
|
708
|
+
} catch {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async sendTask(task) {
|
|
713
|
+
const key = this.getApiKey();
|
|
714
|
+
const body = {
|
|
715
|
+
model: task.model,
|
|
716
|
+
max_tokens: 4096,
|
|
717
|
+
messages: [{ role: "user", content: task.structuredPrompt }]
|
|
718
|
+
};
|
|
719
|
+
let res;
|
|
720
|
+
try {
|
|
721
|
+
res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
722
|
+
method: "POST",
|
|
723
|
+
headers: {
|
|
724
|
+
"x-api-key": key,
|
|
725
|
+
"Content-Type": "application/json",
|
|
726
|
+
"anthropic-version": "2023-06-01"
|
|
727
|
+
},
|
|
728
|
+
body: JSON.stringify(body)
|
|
729
|
+
});
|
|
730
|
+
} catch (err) {
|
|
731
|
+
throw new ProviderError(
|
|
732
|
+
`Unable to connect to Claude: ${err.message}`,
|
|
733
|
+
"Check your network connection"
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
if (res.status === 401) {
|
|
737
|
+
throw new ProviderError(
|
|
738
|
+
"Claude authentication failed. Invalid API key.",
|
|
739
|
+
"Check your API key with: prompt2task provider"
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if (res.status === 429) {
|
|
743
|
+
throw new ProviderError(
|
|
744
|
+
"Claude rate limit exceeded. Please try again later."
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
if (!res.ok) {
|
|
748
|
+
const text = await res.text().catch(() => "");
|
|
749
|
+
throw new ProviderError(
|
|
750
|
+
`Claude request failed (${res.status}): ${text.slice(0, 500)}`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const data = await res.json();
|
|
754
|
+
const textPart = data.content?.find((c) => c.type === "text");
|
|
755
|
+
const content = textPart?.text ?? "";
|
|
756
|
+
if (!content) {
|
|
757
|
+
throw new ProviderError("Claude returned an empty response.");
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
content,
|
|
761
|
+
model: data.model ?? task.model,
|
|
762
|
+
usage: data.usage ? {
|
|
763
|
+
promptTokens: data.usage.input_tokens,
|
|
764
|
+
completionTokens: data.usage.output_tokens,
|
|
765
|
+
totalTokens: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0)
|
|
766
|
+
} : void 0
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// src/project/detector.ts
|
|
772
|
+
import fs4 from "fs";
|
|
773
|
+
import path4 from "path";
|
|
774
|
+
function fileExists(dir, file) {
|
|
775
|
+
return fs4.existsSync(path4.join(dir, file));
|
|
776
|
+
}
|
|
777
|
+
function readJsonIfExists(dir, file) {
|
|
778
|
+
const full = path4.join(dir, file);
|
|
779
|
+
if (!fs4.existsSync(full)) return null;
|
|
780
|
+
try {
|
|
781
|
+
const raw = fs4.readFileSync(full, "utf-8");
|
|
782
|
+
return JSON.parse(raw);
|
|
783
|
+
} catch {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function detectProject(cwd = process.cwd()) {
|
|
788
|
+
const context = { path: cwd };
|
|
789
|
+
if (fileExists(cwd, "wp-config.php") || fileExists(cwd, "wp-load.php")) {
|
|
790
|
+
context.type = "wordpress";
|
|
791
|
+
context.framework = "WordPress";
|
|
792
|
+
context.language = "php";
|
|
793
|
+
return context;
|
|
794
|
+
}
|
|
795
|
+
if (fileExists(cwd, "artisan")) {
|
|
796
|
+
const composer = readJsonIfExists(cwd, "composer.json");
|
|
797
|
+
const isLaravel = composer && JSON.stringify(composer).toLowerCase().includes("laravel/framework");
|
|
798
|
+
if (isLaravel || fileExists(cwd, "composer.json")) {
|
|
799
|
+
context.type = "laravel";
|
|
800
|
+
context.framework = "Laravel";
|
|
801
|
+
context.language = "php";
|
|
802
|
+
return context;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (fileExists(cwd, "composer.json")) {
|
|
806
|
+
context.type = "php";
|
|
807
|
+
context.language = "php";
|
|
808
|
+
return context;
|
|
809
|
+
}
|
|
810
|
+
const pkg = readJsonIfExists(cwd, "package.json");
|
|
811
|
+
if (pkg) {
|
|
812
|
+
const deps = {
|
|
813
|
+
...pkg.dependencies ?? {},
|
|
814
|
+
...pkg.devDependencies ?? {}
|
|
815
|
+
};
|
|
816
|
+
const depKeys = Object.keys(deps);
|
|
817
|
+
if (depKeys.includes("react")) {
|
|
818
|
+
context.type = "react";
|
|
819
|
+
context.framework = "React";
|
|
820
|
+
context.language = "javascript";
|
|
821
|
+
return context;
|
|
822
|
+
}
|
|
823
|
+
if (depKeys.includes("next")) {
|
|
824
|
+
context.type = "react";
|
|
825
|
+
context.framework = "Next.js";
|
|
826
|
+
context.language = "javascript";
|
|
827
|
+
return context;
|
|
828
|
+
}
|
|
829
|
+
if (depKeys.includes("vue")) {
|
|
830
|
+
context.type = "web-applications";
|
|
831
|
+
context.framework = "Vue";
|
|
832
|
+
context.language = "javascript";
|
|
833
|
+
return context;
|
|
834
|
+
}
|
|
835
|
+
if (depKeys.includes("@angular/core")) {
|
|
836
|
+
context.type = "web-applications";
|
|
837
|
+
context.framework = "Angular";
|
|
838
|
+
context.language = "javascript";
|
|
839
|
+
return context;
|
|
840
|
+
}
|
|
841
|
+
context.type = "web-applications";
|
|
842
|
+
context.framework = "Node.js";
|
|
843
|
+
context.language = "javascript";
|
|
844
|
+
return context;
|
|
845
|
+
}
|
|
846
|
+
return context;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/cli/commands/run.ts
|
|
850
|
+
function getProviderInstance(name) {
|
|
851
|
+
if (name === "openai") return new OpenAIProvider();
|
|
852
|
+
if (name === "claude") return new ClaudeProvider();
|
|
853
|
+
throw new ProviderError(`Unsupported provider: ${name}`);
|
|
854
|
+
}
|
|
855
|
+
async function runPrompt(prompt, options = {}) {
|
|
856
|
+
if (!prompt || !prompt.trim()) {
|
|
857
|
+
console.error(chalk2.red("\u2717 Prompt cannot be empty."));
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
let config;
|
|
861
|
+
try {
|
|
862
|
+
config = loadConfig();
|
|
863
|
+
} catch (err) {
|
|
864
|
+
if (err instanceof NotInitializedError || err.message.includes("not been initialized")) {
|
|
865
|
+
console.error(chalk2.red("\u2717 prompt2task has not been initialized."));
|
|
866
|
+
console.error(chalk2.dim("Run `prompt2task init` first."));
|
|
867
|
+
} else {
|
|
868
|
+
console.error(chalk2.red(`\u2717 ${err.message}`));
|
|
869
|
+
if (err.hint) console.error(chalk2.dim(err.hint));
|
|
870
|
+
}
|
|
871
|
+
process.exit(1);
|
|
872
|
+
}
|
|
873
|
+
const apiKey = getCredential(config.provider);
|
|
874
|
+
if (!apiKey) {
|
|
875
|
+
console.error(chalk2.red(`\u2717 Missing API key for provider "${config.provider}".`));
|
|
876
|
+
console.error(chalk2.dim(`Set ${config.provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"} or run \`prompt2task provider\``));
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
const db = getDatabase();
|
|
880
|
+
const repo = new TaskRepository(db);
|
|
881
|
+
const manager = new TaskManager(repo);
|
|
882
|
+
const provider = getProviderInstance(config.provider);
|
|
883
|
+
const projectContext = detectProject(process.cwd());
|
|
884
|
+
const isJson = options.json ?? false;
|
|
885
|
+
let spinner;
|
|
886
|
+
if (!isJson) {
|
|
887
|
+
spinner = createSpinner("Creating task...");
|
|
888
|
+
}
|
|
889
|
+
let taskId = null;
|
|
890
|
+
try {
|
|
891
|
+
const task = manager.createTask({
|
|
892
|
+
prompt,
|
|
893
|
+
provider: config.provider,
|
|
894
|
+
model: config.model,
|
|
895
|
+
projectType: config.projectType,
|
|
896
|
+
projectPath: projectContext.path
|
|
897
|
+
});
|
|
898
|
+
taskId = task.id;
|
|
899
|
+
if (!isJson && spinner) {
|
|
900
|
+
spinner.succeed(chalk2.green(`Task created: ${task.id}`));
|
|
901
|
+
spinner = createSpinner(`Sending task to ${config.provider === "openai" ? "OpenAI" : "Claude"}...`);
|
|
902
|
+
repo.update(task.id, { status: "running" });
|
|
903
|
+
spinner.text = "AI is working...";
|
|
904
|
+
} else {
|
|
905
|
+
repo.update(task.id, { status: "running" });
|
|
906
|
+
}
|
|
907
|
+
const { buildStructuredPrompt: buildStructuredPrompt2 } = await import("./prompt-builder-X7TCYJ6E.js");
|
|
908
|
+
const structured = buildStructuredPrompt2({
|
|
909
|
+
prompt,
|
|
910
|
+
projectType: config.projectType,
|
|
911
|
+
projectContext
|
|
912
|
+
});
|
|
913
|
+
const response = await provider.sendTask({
|
|
914
|
+
prompt,
|
|
915
|
+
structuredPrompt: structured,
|
|
916
|
+
model: config.model,
|
|
917
|
+
projectType: config.projectType
|
|
918
|
+
});
|
|
919
|
+
repo.update(task.id, {
|
|
920
|
+
status: "completed",
|
|
921
|
+
response: response.content,
|
|
922
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
923
|
+
});
|
|
924
|
+
if (isJson) {
|
|
925
|
+
const finalTask = repo.findById(task.id);
|
|
926
|
+
console.log(
|
|
927
|
+
JSON.stringify(
|
|
928
|
+
{
|
|
929
|
+
id: finalTask.id,
|
|
930
|
+
status: finalTask.status,
|
|
931
|
+
provider: finalTask.provider,
|
|
932
|
+
model: finalTask.model,
|
|
933
|
+
response: finalTask.response
|
|
934
|
+
},
|
|
935
|
+
null,
|
|
936
|
+
2
|
|
937
|
+
)
|
|
938
|
+
);
|
|
939
|
+
} else {
|
|
940
|
+
if (spinner) spinner.succeed(chalk2.green("Task completed."));
|
|
941
|
+
console.log("");
|
|
942
|
+
console.log(chalk2.bold(`Task ID: ${task.id}`));
|
|
943
|
+
console.log("");
|
|
944
|
+
console.log(chalk2.bold("Response:"));
|
|
945
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
946
|
+
console.log(response.content);
|
|
947
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
948
|
+
}
|
|
949
|
+
} catch (err) {
|
|
950
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
951
|
+
if (taskId) {
|
|
952
|
+
try {
|
|
953
|
+
const existing = repo.findById(taskId);
|
|
954
|
+
if (existing && existing.status !== "completed") {
|
|
955
|
+
repo.update(taskId, {
|
|
956
|
+
status: "failed",
|
|
957
|
+
error: message,
|
|
958
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
} catch {
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (isJson) {
|
|
965
|
+
console.log(
|
|
966
|
+
JSON.stringify(
|
|
967
|
+
{
|
|
968
|
+
id: taskId,
|
|
969
|
+
status: "failed",
|
|
970
|
+
error: message
|
|
971
|
+
},
|
|
972
|
+
null,
|
|
973
|
+
2
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
if (spinner) spinner.fail(chalk2.red("Task failed."));
|
|
979
|
+
console.error("");
|
|
980
|
+
console.error(chalk2.red(`\u2717 ${message}`));
|
|
981
|
+
if (err instanceof ProviderError && err.hint) {
|
|
982
|
+
console.error(chalk2.dim(err.hint));
|
|
983
|
+
} else if (message.toLowerCase().includes("authentication") || message.includes("401")) {
|
|
984
|
+
console.error(chalk2.dim("Check your API key with: prompt2task provider"));
|
|
985
|
+
}
|
|
986
|
+
if (options.debug) {
|
|
987
|
+
console.error(chalk2.dim(err.stack ?? ""));
|
|
988
|
+
}
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/cli/commands/list.ts
|
|
994
|
+
import chalk3 from "chalk";
|
|
995
|
+
|
|
996
|
+
// src/cli/ui/table.ts
|
|
997
|
+
import Table from "cli-table3";
|
|
998
|
+
function formatDate(iso) {
|
|
999
|
+
try {
|
|
1000
|
+
const d = new Date(iso);
|
|
1001
|
+
return d.toLocaleString("en-US", {
|
|
1002
|
+
month: "short",
|
|
1003
|
+
day: "2-digit",
|
|
1004
|
+
hour: "2-digit",
|
|
1005
|
+
minute: "2-digit"
|
|
1006
|
+
});
|
|
1007
|
+
} catch {
|
|
1008
|
+
return iso;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function truncate(str, max) {
|
|
1012
|
+
if (str.length <= max) return str;
|
|
1013
|
+
return str.slice(0, max - 3) + "...";
|
|
1014
|
+
}
|
|
1015
|
+
function renderTaskTable(tasks) {
|
|
1016
|
+
const table = new Table({
|
|
1017
|
+
head: ["TASK ID", "STATUS", "CREATED", "PROMPT"],
|
|
1018
|
+
colWidths: [14, 12, 18, 40],
|
|
1019
|
+
wordWrap: true,
|
|
1020
|
+
style: { head: [], border: [] }
|
|
1021
|
+
});
|
|
1022
|
+
for (const t of tasks) {
|
|
1023
|
+
table.push([t.id, t.status, formatDate(t.createdAt), truncate(t.prompt, 38)]);
|
|
1024
|
+
}
|
|
1025
|
+
return table.toString();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/cli/commands/list.ts
|
|
1029
|
+
function listCommand(options) {
|
|
1030
|
+
if (!configExists()) {
|
|
1031
|
+
console.error(chalk3.red("\u2717 Not initialized. Run `prompt2task init` first."));
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
const db = getDatabase();
|
|
1035
|
+
const repo = new TaskRepository(db);
|
|
1036
|
+
let limit;
|
|
1037
|
+
if (options.limit) {
|
|
1038
|
+
limit = parseInt(options.limit, 10);
|
|
1039
|
+
if (isNaN(limit) || limit <= 0) {
|
|
1040
|
+
console.error(chalk3.red("\u2717 Invalid --limit value. Must be a positive number."));
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
let status;
|
|
1045
|
+
if (options.status) {
|
|
1046
|
+
const allowed = ["pending", "running", "completed", "failed", "cancelled"];
|
|
1047
|
+
if (!allowed.includes(options.status)) {
|
|
1048
|
+
console.error(chalk3.red(`\u2717 Invalid --status "${options.status}". Allowed: ${allowed.join(", ")}`));
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
status = options.status;
|
|
1052
|
+
}
|
|
1053
|
+
const tasks = repo.list({ limit, status });
|
|
1054
|
+
if (options.json) {
|
|
1055
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (tasks.length === 0) {
|
|
1059
|
+
console.log(chalk3.dim("No tasks found."));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
console.log(renderTaskTable(tasks));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/cli/commands/show.ts
|
|
1066
|
+
import chalk4 from "chalk";
|
|
1067
|
+
function showCommand(taskId, options) {
|
|
1068
|
+
if (!configExists()) {
|
|
1069
|
+
console.error(chalk4.red("\u2717 Not initialized. Run `prompt2task init` first."));
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
const db = getDatabase();
|
|
1073
|
+
const repo = new TaskRepository(db);
|
|
1074
|
+
const task = repo.findById(taskId);
|
|
1075
|
+
if (!task) {
|
|
1076
|
+
console.error(chalk4.red(`\u2717 Task not found: ${taskId}`));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
if (options.json) {
|
|
1080
|
+
console.log(JSON.stringify(task, null, 2));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
console.log(chalk4.bold("Task details"));
|
|
1084
|
+
console.log(chalk4.dim("\u2500".repeat(40)));
|
|
1085
|
+
console.log(`${chalk4.bold("Task ID:")} ${task.id}`);
|
|
1086
|
+
console.log(`${chalk4.bold("Status:")} ${task.status}`);
|
|
1087
|
+
console.log(`${chalk4.bold("Provider:")} ${task.provider}`);
|
|
1088
|
+
console.log(`${chalk4.bold("Model:")} ${task.model}`);
|
|
1089
|
+
console.log(`${chalk4.bold("Project type:")} ${task.projectType ?? "-"}`);
|
|
1090
|
+
console.log(`${chalk4.bold("Project path:")} ${task.projectPath ?? "-"}`);
|
|
1091
|
+
console.log(`${chalk4.bold("Created:")} ${task.createdAt}`);
|
|
1092
|
+
console.log(`${chalk4.bold("Completed:")} ${task.completedAt ?? "-"}`);
|
|
1093
|
+
console.log("");
|
|
1094
|
+
console.log(chalk4.bold("Prompt:"));
|
|
1095
|
+
console.log(task.prompt);
|
|
1096
|
+
console.log("");
|
|
1097
|
+
if (task.response) {
|
|
1098
|
+
console.log(chalk4.bold("Response:"));
|
|
1099
|
+
console.log(chalk4.dim("\u2500".repeat(40)));
|
|
1100
|
+
console.log(task.response);
|
|
1101
|
+
console.log(chalk4.dim("\u2500".repeat(40)));
|
|
1102
|
+
}
|
|
1103
|
+
if (task.error) {
|
|
1104
|
+
console.log(chalk4.bold(chalk4.red("Error:")));
|
|
1105
|
+
console.log(chalk4.red(task.error));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/cli/commands/search.ts
|
|
1110
|
+
import chalk5 from "chalk";
|
|
1111
|
+
function searchCommand(query, options) {
|
|
1112
|
+
if (!configExists()) {
|
|
1113
|
+
console.error(chalk5.red("\u2717 Not initialized. Run `prompt2task init` first."));
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
if (!query || !query.trim()) {
|
|
1117
|
+
console.error(chalk5.red("\u2717 Search query cannot be empty."));
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
const db = getDatabase();
|
|
1121
|
+
const repo = new TaskRepository(db);
|
|
1122
|
+
let limit;
|
|
1123
|
+
if (options.limit) {
|
|
1124
|
+
limit = parseInt(options.limit, 10);
|
|
1125
|
+
if (isNaN(limit) || limit <= 0) {
|
|
1126
|
+
console.error(chalk5.red("\u2717 Invalid --limit value."));
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const results = repo.search(query, { limit });
|
|
1131
|
+
if (options.json) {
|
|
1132
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (results.length === 0) {
|
|
1136
|
+
console.log(chalk5.dim(`No tasks found for query: "${query}"`));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
console.log(renderTaskTable(results));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/cli/commands/config.ts
|
|
1143
|
+
import chalk6 from "chalk";
|
|
1144
|
+
function configShowCommand() {
|
|
1145
|
+
if (!configExists()) {
|
|
1146
|
+
console.error(chalk6.red("\u2717 Not initialized. Run `prompt2task init` first."));
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
const config = loadConfig();
|
|
1150
|
+
console.log(chalk6.bold("prompt2task configuration"));
|
|
1151
|
+
console.log("");
|
|
1152
|
+
console.log(`${chalk6.dim("Provider:")} ${capitalize(config.provider)}`);
|
|
1153
|
+
console.log(`${chalk6.dim("Model:")} ${config.model}`);
|
|
1154
|
+
console.log(`${chalk6.dim("Project type:")} ${capitalize(config.projectType)}`);
|
|
1155
|
+
console.log(`${chalk6.dim("Config:")} ${getConfigPath()}`);
|
|
1156
|
+
console.log(`${chalk6.dim("History:")} ${getDbPath()}`);
|
|
1157
|
+
console.log(`${chalk6.dim("App dir:")} ${getAppDir()}`);
|
|
1158
|
+
}
|
|
1159
|
+
function configSetCommand(key, value) {
|
|
1160
|
+
if (!configExists()) {
|
|
1161
|
+
console.error(chalk6.red("\u2717 Not initialized. Run `prompt2task init` first."));
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
const config = loadConfig();
|
|
1165
|
+
const normalizedKey = key.replace(/-/g, "").toLowerCase();
|
|
1166
|
+
if (normalizedKey === "model") {
|
|
1167
|
+
config.model = value;
|
|
1168
|
+
} else if (normalizedKey === "projecttype" || normalizedKey === "project_type") {
|
|
1169
|
+
config.projectType = value;
|
|
1170
|
+
} else if (normalizedKey === "provider") {
|
|
1171
|
+
if (value !== "openai" && value !== "claude") {
|
|
1172
|
+
console.error(chalk6.red(`\u2717 Invalid provider "${value}". Allowed: openai, claude`));
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
config.provider = value;
|
|
1176
|
+
} else {
|
|
1177
|
+
console.error(chalk6.red(`\u2717 Unknown config key "${key}". Allowed: model, project-type, provider`));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
saveConfig(config);
|
|
1181
|
+
console.log(chalk6.green(`\u2713 Config updated: ${key} = ${value}`));
|
|
1182
|
+
}
|
|
1183
|
+
function capitalize(s) {
|
|
1184
|
+
if (!s) return s;
|
|
1185
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/cli/commands/provider.ts
|
|
1189
|
+
import chalk7 from "chalk";
|
|
1190
|
+
async function providerCommand() {
|
|
1191
|
+
if (!configExists()) {
|
|
1192
|
+
console.error(chalk7.red("\u2717 Not initialized. Run `prompt2task init` first."));
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
const config = loadConfig();
|
|
1196
|
+
console.log(chalk7.bold("Change provider\n"));
|
|
1197
|
+
console.log(chalk7.dim(`Current: ${config.provider} / ${config.model}
|
|
1198
|
+
`));
|
|
1199
|
+
const provider = await promptProvider();
|
|
1200
|
+
const apiKey = await promptApiKey(provider);
|
|
1201
|
+
const spinner = createSpinner("Saving credentials...");
|
|
1202
|
+
try {
|
|
1203
|
+
saveCredential(provider, apiKey);
|
|
1204
|
+
spinner.succeed(chalk7.green("Credentials saved."));
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
spinner.fail(chalk7.red("Failed to save credentials."));
|
|
1207
|
+
throw err;
|
|
1208
|
+
}
|
|
1209
|
+
const model = await promptModel(provider);
|
|
1210
|
+
config.provider = provider;
|
|
1211
|
+
config.model = model;
|
|
1212
|
+
saveConfig(config);
|
|
1213
|
+
console.log(chalk7.green(`
|
|
1214
|
+
\u2713 Provider updated to ${provider} (${model})`));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/cli/commands/credentials.ts
|
|
1218
|
+
import chalk8 from "chalk";
|
|
1219
|
+
function credentialsRemoveCommand(provider) {
|
|
1220
|
+
if (!provider || provider === "all") {
|
|
1221
|
+
removeAllCredentials();
|
|
1222
|
+
console.log(chalk8.green("\u2713 All credentials removed."));
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (provider !== "openai" && provider !== "claude") {
|
|
1226
|
+
console.error(chalk8.red(`\u2717 Unknown provider "${provider}". Use: openai, claude, or all`));
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
const removed = removeCredential(provider);
|
|
1230
|
+
if (removed) {
|
|
1231
|
+
console.log(chalk8.green(`\u2713 Credentials removed for ${provider}.`));
|
|
1232
|
+
} else {
|
|
1233
|
+
console.log(chalk8.dim(`No credentials found for ${provider}.`));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/utils/logger.ts
|
|
1238
|
+
var debugEnabled = false;
|
|
1239
|
+
function setDebug(enabled) {
|
|
1240
|
+
debugEnabled = enabled;
|
|
1241
|
+
}
|
|
1242
|
+
function debug(...args) {
|
|
1243
|
+
if (debugEnabled) {
|
|
1244
|
+
console.error("[debug]", ...args);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/cli/index.ts
|
|
1249
|
+
function createProgram() {
|
|
1250
|
+
const program = new Command();
|
|
1251
|
+
program.name("prompt2task").description("Turn natural-language prompts into structured tasks and send them to AI providers").version("0.1.0").option("--json", "Output result as JSON (for automation)").option("--debug", "Enable debug output").hook("preAction", (thisCommand) => {
|
|
1252
|
+
const opts = thisCommand.opts();
|
|
1253
|
+
if (opts.debug) setDebug(true);
|
|
1254
|
+
});
|
|
1255
|
+
program.argument("[prompt]", "Natural language prompt to execute").action(async (prompt, _opts, command) => {
|
|
1256
|
+
const opts = command.optsWithGlobals();
|
|
1257
|
+
if (opts.debug) setDebug(true);
|
|
1258
|
+
if (!prompt) {
|
|
1259
|
+
command.help();
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
await runPrompt(prompt, { json: !!opts.json, debug: !!opts.debug });
|
|
1263
|
+
});
|
|
1264
|
+
program.command("init").description("Interactive first-time setup").option("-f, --force", "Force re-initialization").action(async (opts) => {
|
|
1265
|
+
try {
|
|
1266
|
+
await initCommand({ force: !!opts.force });
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
console.error(chalk9.red(`\u2717 ${err.message}`));
|
|
1269
|
+
if (err.hint) console.error(chalk9.dim(err.hint));
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
program.command("list").description("List task history").option("--limit <n>", "Limit number of results").option("--status <status>", "Filter by status (pending, running, completed, failed, cancelled)").option("--json", "Output as JSON").action((opts) => {
|
|
1274
|
+
const globalOpts = program.opts();
|
|
1275
|
+
listCommand({ ...opts, json: opts.json ?? globalOpts.json });
|
|
1276
|
+
});
|
|
1277
|
+
program.command("show").description("Show task details").argument("<id>", "Task ID").option("--json", "Output as JSON").action((id, opts) => {
|
|
1278
|
+
const globalOpts = program.opts();
|
|
1279
|
+
showCommand(id, { json: opts.json ?? globalOpts.json });
|
|
1280
|
+
});
|
|
1281
|
+
program.command("search").description("Search tasks by prompt or response").argument("<query>", "Search query").option("--json", "Output as JSON").option("--limit <n>", "Limit results").action((query, opts) => {
|
|
1282
|
+
const globalOpts = program.opts();
|
|
1283
|
+
searchCommand(query, { ...opts, json: opts.json ?? globalOpts.json });
|
|
1284
|
+
});
|
|
1285
|
+
const configCmd = program.command("config").description("Manage configuration");
|
|
1286
|
+
configCmd.action(() => {
|
|
1287
|
+
configShowCommand();
|
|
1288
|
+
});
|
|
1289
|
+
configCmd.command("set").description("Set a config value (model, project-type, provider)").argument("<key>", "Config key").argument("<value>", "Config value").action((key, value) => {
|
|
1290
|
+
configSetCommand(key, value);
|
|
1291
|
+
});
|
|
1292
|
+
program.command("provider").description("Change AI provider and credentials").action(async () => {
|
|
1293
|
+
try {
|
|
1294
|
+
await providerCommand();
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
console.error(chalk9.red(`\u2717 ${err.message}`));
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
const credentialsCmd = program.command("credentials").description("Manage credentials");
|
|
1301
|
+
credentialsCmd.command("remove").description("Remove stored credentials (openai, claude, or all)").argument("[provider]", "Provider to remove").action((provider) => {
|
|
1302
|
+
credentialsRemoveCommand(provider);
|
|
1303
|
+
});
|
|
1304
|
+
return program;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/index.ts
|
|
1308
|
+
async function main() {
|
|
1309
|
+
const program = createProgram();
|
|
1310
|
+
const rawArgs = process.argv.slice(2);
|
|
1311
|
+
if (rawArgs.includes("--debug")) {
|
|
1312
|
+
debug("Debug mode enabled");
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
await program.parseAsync(process.argv);
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1318
|
+
const sanitized = message.replace(/sk-[a-zA-Z0-9-_]+/g, "[REDACTED]").replace(/sk-ant-[a-zA-Z0-9-_]+/g, "[REDACTED]");
|
|
1319
|
+
console.error(sanitized);
|
|
1320
|
+
if (process.argv.includes("--debug") && err instanceof Error && err.stack) {
|
|
1321
|
+
console.error(err.stack.replace(/sk-[a-zA-Z0-9-_]+/g, "[REDACTED]"));
|
|
1322
|
+
}
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
main();
|