projectinator 0.3.1 → 0.4.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/README.md +7 -6
- package/bin/projectinator.mjs +22 -15
- package/dist/bakeoff.js +163 -0
- package/dist/bakeoff.js.map +1 -0
- package/dist/build-state.js +26 -0
- package/dist/build-state.js.map +1 -0
- package/dist/burndown.js +19 -0
- package/dist/burndown.js.map +1 -0
- package/dist/calibration.js +86 -0
- package/dist/calibration.js.map +1 -0
- package/dist/cli.js +289 -0
- package/dist/cli.js.map +1 -0
- package/dist/cost.js +35 -0
- package/dist/cost.js.map +1 -0
- package/dist/council.js +147 -0
- package/dist/council.js.map +1 -0
- package/dist/demo.js +93 -0
- package/dist/demo.js.map +1 -0
- package/dist/estimate.js +98 -0
- package/dist/estimate.js.map +1 -0
- package/dist/executor.js +118 -0
- package/dist/executor.js.map +1 -0
- package/dist/git.js +82 -0
- package/dist/git.js.map +1 -0
- package/dist/intake.js +98 -0
- package/dist/intake.js.map +1 -0
- package/dist/models.js +171 -0
- package/dist/models.js.map +1 -0
- package/dist/narrate.js +76 -0
- package/dist/narrate.js.map +1 -0
- package/dist/openrouter.js +112 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/orchestrator.js +273 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/pm.js +245 -0
- package/dist/pm.js.map +1 -0
- package/dist/preview.js +194 -0
- package/dist/preview.js.map +1 -0
- package/dist/registry-store.js +38 -0
- package/dist/registry-store.js.map +1 -0
- package/dist/registry.js +115 -0
- package/dist/registry.js.map +1 -0
- package/dist/research.js +98 -0
- package/dist/research.js.map +1 -0
- package/dist/retro.js +83 -0
- package/dist/retro.js.map +1 -0
- package/dist/roles.js +357 -0
- package/dist/roles.js.map +1 -0
- package/dist/router.js +95 -0
- package/dist/router.js.map +1 -0
- package/dist/run-bakeoff.js +68 -0
- package/dist/run-bakeoff.js.map +1 -0
- package/dist/run-build.js +193 -0
- package/dist/run-build.js.map +1 -0
- package/{src/run-dev.ts → dist/run-dev.js} +23 -39
- package/dist/run-dev.js.map +1 -0
- package/{src/run-pm.ts → dist/run-pm.js} +34 -46
- package/dist/run-pm.js.map +1 -0
- package/{src/run-research.ts → dist/run-research.js} +19 -30
- package/dist/run-research.js.map +1 -0
- package/{src/run-scout.ts → dist/run-scout.js} +17 -27
- package/dist/run-scout.js.map +1 -0
- package/dist/run-web.js +93 -0
- package/dist/run-web.js.map +1 -0
- package/dist/scout.js +81 -0
- package/dist/scout.js.map +1 -0
- package/dist/session-cost.js +15 -0
- package/dist/session-cost.js.map +1 -0
- package/dist/stack.js +34 -0
- package/dist/stack.js.map +1 -0
- package/dist/stuck.js +21 -0
- package/dist/stuck.js.map +1 -0
- package/dist/tui/App.js +1269 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/BakeOff.js +95 -0
- package/dist/tui/BakeOff.js.map +1 -0
- package/dist/tui/BoardEditor.js +183 -0
- package/dist/tui/BoardEditor.js.map +1 -0
- package/dist/tui/EditableBoard.js +140 -0
- package/dist/tui/EditableBoard.js.map +1 -0
- package/dist/tui/Frame.js +78 -0
- package/dist/tui/Frame.js.map +1 -0
- package/dist/tui/Intake.js +59 -0
- package/dist/tui/Intake.js.map +1 -0
- package/dist/tui/Kanban.js +75 -0
- package/dist/tui/Kanban.js.map +1 -0
- package/dist/tui/Settings.js +299 -0
- package/dist/tui/Settings.js.map +1 -0
- package/dist/tui/StackPick.js +44 -0
- package/dist/tui/StackPick.js.map +1 -0
- package/dist/tui/WebAccounts.js +131 -0
- package/dist/tui/WebAccounts.js.map +1 -0
- package/dist/tui/components.js +178 -0
- package/dist/tui/components.js.map +1 -0
- package/dist/tui/config.js +129 -0
- package/dist/tui/config.js.map +1 -0
- package/dist/tui/deploy.js +114 -0
- package/dist/tui/deploy.js.map +1 -0
- package/dist/tui/engine.js +710 -0
- package/dist/tui/engine.js.map +1 -0
- package/dist/tui/notify.js +47 -0
- package/dist/tui/notify.js.map +1 -0
- package/dist/tui/panels.js +42 -0
- package/dist/tui/panels.js.map +1 -0
- package/dist/tui/templates.js +106 -0
- package/dist/tui/templates.js.map +1 -0
- package/dist/tui/theme.js +42 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/validate.js +51 -0
- package/dist/tui/validate.js.map +1 -0
- package/{src/tui.tsx → dist/tui.js} +23 -33
- package/dist/tui.js.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/web/oauth-anthropic.js +179 -0
- package/dist/web/oauth-anthropic.js.map +1 -0
- package/dist/web/session.js +274 -0
- package/dist/web/session.js.map +1 -0
- package/package.json +5 -4
- package/src/bakeoff.ts +0 -214
- package/src/build-state.ts +0 -47
- package/src/burndown.ts +0 -35
- package/src/calibration.ts +0 -88
- package/src/cli.ts +0 -263
- package/src/cost.ts +0 -43
- package/src/council.ts +0 -175
- package/src/demo.ts +0 -106
- package/src/estimate.ts +0 -111
- package/src/executor.ts +0 -169
- package/src/git.ts +0 -72
- package/src/intake.ts +0 -125
- package/src/models.ts +0 -175
- package/src/narrate.ts +0 -87
- package/src/openrouter.ts +0 -119
- package/src/orchestrator.ts +0 -310
- package/src/pm.ts +0 -302
- package/src/preview.ts +0 -206
- package/src/registry-store.ts +0 -41
- package/src/registry.ts +0 -132
- package/src/research.ts +0 -123
- package/src/retro.ts +0 -99
- package/src/roles.ts +0 -401
- package/src/router.ts +0 -120
- package/src/run-bakeoff.ts +0 -77
- package/src/run-build.ts +0 -203
- package/src/run-web.ts +0 -87
- package/src/scout.ts +0 -121
- package/src/session-cost.ts +0 -17
- package/src/stack.ts +0 -46
- package/src/tui/App.tsx +0 -1760
- package/src/tui/BakeOff.tsx +0 -190
- package/src/tui/BoardEditor.tsx +0 -260
- package/src/tui/EditableBoard.tsx +0 -179
- package/src/tui/Frame.tsx +0 -142
- package/src/tui/Intake.tsx +0 -111
- package/src/tui/Kanban.tsx +0 -158
- package/src/tui/Settings.tsx +0 -513
- package/src/tui/StackPick.tsx +0 -79
- package/src/tui/WebAccounts.tsx +0 -197
- package/src/tui/components.tsx +0 -340
- package/src/tui/config.ts +0 -150
- package/src/tui/deploy.ts +0 -137
- package/src/tui/engine.ts +0 -749
- package/src/tui/notify.ts +0 -21
- package/src/tui/panels.tsx +0 -89
- package/src/tui/templates.ts +0 -119
- package/src/tui/theme.ts +0 -44
- package/src/tui/validate.ts +0 -53
- package/src/types.ts +0 -208
- package/src/web/oauth-anthropic.ts +0 -206
- package/src/web/session.ts +0 -299
package/src/bakeoff.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
// Model bake-off — the founding idea: run ONE task across several models, then
|
|
2
|
-
// compare cost, latency, and quality so you can pick the best model per role and
|
|
3
|
-
// feed that back into the routing registry.
|
|
4
|
-
//
|
|
5
|
-
// v1 covers TEXT roles (plan, design, test-reasoning) where the deliverable is
|
|
6
|
-
// text a judge can score. Code bake-off (per-candidate sandbox + real test
|
|
7
|
-
// scoring) is a later step.
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
createAgentSession,
|
|
11
|
-
defineTool,
|
|
12
|
-
type AgentSession,
|
|
13
|
-
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { Type, type Static } from "typebox";
|
|
15
|
-
import type { Capability, Difficulty, Provider, Task } from "./types.js";
|
|
16
|
-
import { piRuntime, resolvePiModel } from "./executor.js"
|
|
17
|
-
import { buildRolePrompt } from "./roles.js";
|
|
18
|
-
import { estimateTokens } from "./estimate.js";
|
|
19
|
-
import { addSessionCost } from "./session-cost.js";
|
|
20
|
-
|
|
21
|
-
export interface Candidate {
|
|
22
|
-
provider: Provider;
|
|
23
|
-
model: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface BakeoffEntry {
|
|
27
|
-
provider: Provider;
|
|
28
|
-
model: string;
|
|
29
|
-
output: string;
|
|
30
|
-
cost: number;
|
|
31
|
-
ms: number;
|
|
32
|
-
outputTokens: number;
|
|
33
|
-
error?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface JudgeScore {
|
|
37
|
-
model: string;
|
|
38
|
-
score: number; // 0-10
|
|
39
|
-
reason: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface BakeoffResult {
|
|
43
|
-
task: Task;
|
|
44
|
-
entries: BakeoffEntry[];
|
|
45
|
-
scores: JudgeScore[];
|
|
46
|
-
winner?: string; // "provider/model"
|
|
47
|
-
judge?: string; // judge model id
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function lastAssistantText(session: AgentSession): string {
|
|
51
|
-
const msgs = session.messages as Array<{ role?: string; content?: unknown }>;
|
|
52
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
53
|
-
const m = msgs[i];
|
|
54
|
-
if (m?.role !== "assistant") continue;
|
|
55
|
-
const c = m.content;
|
|
56
|
-
if (typeof c === "string") return c;
|
|
57
|
-
if (Array.isArray(c)) {
|
|
58
|
-
return c
|
|
59
|
-
.map((p: unknown) => (typeof p === "string" ? p : p && typeof p === "object" && "text" in p ? String((p as { text: unknown }).text) : ""))
|
|
60
|
-
.join("")
|
|
61
|
-
.trim();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return "";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const id = (c: Candidate) => `${c.provider}/${c.model}`;
|
|
68
|
-
|
|
69
|
-
/** Run one candidate on the task, capturing output, cost, and latency. */
|
|
70
|
-
async function runCandidate(task: Task, cand: Candidate): Promise<BakeoffEntry> {
|
|
71
|
-
const base: BakeoffEntry = { provider: cand.provider, model: cand.model, output: "", cost: 0, ms: 0, outputTokens: 0 };
|
|
72
|
-
try {
|
|
73
|
-
const runtime = await piRuntime();
|
|
74
|
-
const model = resolvePiModel(runtime, cand.provider, cand.model);
|
|
75
|
-
const { session } = await createAgentSession({
|
|
76
|
-
model,
|
|
77
|
-
modelRuntime: runtime,
|
|
78
|
-
thinkingLevel: "medium",
|
|
79
|
-
noTools: "all",
|
|
80
|
-
});
|
|
81
|
-
try {
|
|
82
|
-
const t0 = Date.now();
|
|
83
|
-
await session.prompt(buildRolePrompt(task, ""));
|
|
84
|
-
const ms = Date.now() - t0;
|
|
85
|
-
const stats = session.getSessionStats();
|
|
86
|
-
addSessionCost(stats.cost);
|
|
87
|
-
const out: BakeoffEntry = {
|
|
88
|
-
...base,
|
|
89
|
-
output: lastAssistantText(session),
|
|
90
|
-
cost: Math.round(stats.cost * 10000) / 10000,
|
|
91
|
-
ms,
|
|
92
|
-
outputTokens: stats.tokens.output,
|
|
93
|
-
};
|
|
94
|
-
if (stats.tokens.total === 0) out.error = "returned 0 tokens (invalid key, no credit/balance, or no model access)";
|
|
95
|
-
return out;
|
|
96
|
-
} finally {
|
|
97
|
-
session.dispose(); // dispose even when prompt() throws (expected for inaccessible models)
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
return { ...base, error: e instanceof Error ? e.message : String(e) };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---- judge: score every output on one rubric, forced structured output ----
|
|
105
|
-
|
|
106
|
-
const JudgeSchema = Type.Object(
|
|
107
|
-
{
|
|
108
|
-
scores: Type.Array(
|
|
109
|
-
Type.Object({
|
|
110
|
-
option: Type.String({ description: "the option letter, e.g. A" }),
|
|
111
|
-
score: Type.Number({ description: "0-10 quality for this deliverable" }),
|
|
112
|
-
reason: Type.String({ description: "one sentence" }),
|
|
113
|
-
}),
|
|
114
|
-
),
|
|
115
|
-
winner: Type.String({ description: "the option letter of the best output" }),
|
|
116
|
-
},
|
|
117
|
-
{ additionalProperties: true },
|
|
118
|
-
);
|
|
119
|
-
type JudgeRaw = Static<typeof JudgeSchema>;
|
|
120
|
-
|
|
121
|
-
function buildJudgeTool() {
|
|
122
|
-
let captured: JudgeRaw | undefined;
|
|
123
|
-
const tool = defineTool({
|
|
124
|
-
name: "submit_scores",
|
|
125
|
-
label: "Submit Scores",
|
|
126
|
-
description: "Submit a 0-10 quality score and one-sentence reason for every option, plus the winning option letter.",
|
|
127
|
-
parameters: JudgeSchema,
|
|
128
|
-
execute: async (_id, params: JudgeRaw) => {
|
|
129
|
-
captured = params;
|
|
130
|
-
return { content: [{ type: "text", text: `Scored ${params.scores.length} options; winner ${params.winner}.` }], details: {} };
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
return { tool, get: () => captured };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Judge anonymised outputs (A, B, C…) on one rubric for the task's capability. */
|
|
137
|
-
async function judge(task: Task, entries: BakeoffEntry[], judgeCand: Candidate): Promise<{ scores: JudgeScore[]; winner?: string; judgeId: string }> {
|
|
138
|
-
const scored = entries.filter((e) => !e.error && e.output);
|
|
139
|
-
if (scored.length < 2) return { scores: [], winner: undefined, judgeId: id(judgeCand) };
|
|
140
|
-
|
|
141
|
-
const letters = scored.map((_, i) => String.fromCharCode(65 + i)); // A, B, C…
|
|
142
|
-
const blocks = scored.map((e, i) => `### Option ${letters[i]}\n${e.output}`).join("\n\n");
|
|
143
|
-
const runtime = await piRuntime();
|
|
144
|
-
const model = resolvePiModel(runtime, judgeCand.provider, judgeCand.model);
|
|
145
|
-
const { tool, get } = buildJudgeTool();
|
|
146
|
-
const { session } = await createAgentSession({
|
|
147
|
-
model,
|
|
148
|
-
modelRuntime: runtime,
|
|
149
|
-
thinkingLevel: "medium",
|
|
150
|
-
noTools: "all",
|
|
151
|
-
customTools: [tool],
|
|
152
|
-
tools: ["submit_scores"],
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const prompt = [
|
|
156
|
-
`You are judging ${scored.length} anonymous attempts at the same ${task.capability} task. Be a strict, fair critic.`,
|
|
157
|
-
`Task: ${task.title}`,
|
|
158
|
-
"",
|
|
159
|
-
`Score each option 0-10 on how well it delivers a high-quality ${task.capability} result (correctness, completeness, clarity, usefulness). Then pick the single best.`,
|
|
160
|
-
"Call submit_scores exactly once with a score+reason for EVERY option letter and the winner.",
|
|
161
|
-
"",
|
|
162
|
-
blocks,
|
|
163
|
-
].join("\n");
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
await session.prompt(prompt);
|
|
167
|
-
let raw = get();
|
|
168
|
-
for (let i = 0; i < 2 && !raw; i++) {
|
|
169
|
-
await session.prompt("Call submit_scores now with a score for every option letter and the winner.");
|
|
170
|
-
raw = get();
|
|
171
|
-
}
|
|
172
|
-
addSessionCost(session.getSessionStats().cost);
|
|
173
|
-
if (!raw) return { scores: [], winner: undefined, judgeId: id(judgeCand) };
|
|
174
|
-
|
|
175
|
-
const byLetter = new Map(letters.map((l, i) => [l, scored[i]!]));
|
|
176
|
-
const scores: JudgeScore[] = raw.scores
|
|
177
|
-
.map((s) => {
|
|
178
|
-
const e = byLetter.get(s.option.trim().toUpperCase().slice(0, 1));
|
|
179
|
-
return e ? { model: id(e), score: s.score, reason: s.reason } : undefined;
|
|
180
|
-
})
|
|
181
|
-
.filter((x): x is JudgeScore => !!x);
|
|
182
|
-
const winEntry = byLetter.get(String(raw.winner).trim().toUpperCase().slice(0, 1));
|
|
183
|
-
return { scores, winner: winEntry ? id(winEntry) : undefined, judgeId: id(judgeCand) };
|
|
184
|
-
} finally {
|
|
185
|
-
session.dispose();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface BakeoffOptions {
|
|
190
|
-
/** Model that scores the outputs. Defaults to the first candidate. */
|
|
191
|
-
judge?: Candidate;
|
|
192
|
-
onProgress?: (msg: string) => void;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Run the full bake-off: every candidate on the task, then judge. */
|
|
196
|
-
export async function runBakeoff(task: Task, candidates: Candidate[], opts: BakeoffOptions = {}): Promise<BakeoffResult> {
|
|
197
|
-
const log = opts.onProgress ?? (() => {});
|
|
198
|
-
const entries: BakeoffEntry[] = [];
|
|
199
|
-
for (const c of candidates) {
|
|
200
|
-
log(`running ${id(c)}…`);
|
|
201
|
-
const e = await runCandidate(task, c);
|
|
202
|
-
log(e.error ? ` ${id(c)}: ERROR ${e.error}` : ` ${id(c)}: $${e.cost.toFixed(4)} ${(e.ms / 1000).toFixed(1)}s ${e.outputTokens} tok`);
|
|
203
|
-
entries.push(e);
|
|
204
|
-
}
|
|
205
|
-
const judgeCand = opts.judge ?? candidates[0]!;
|
|
206
|
-
log(`judging with ${id(judgeCand)}…`);
|
|
207
|
-
const { scores, winner, judgeId } = await judge(task, entries, judgeCand);
|
|
208
|
-
return { task, entries, scores, winner, judge: judgeId };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Convenience: build a one-off Task for a capability/difficulty from a prompt. */
|
|
212
|
-
export function bakeoffTask(prompt: string, capability: Capability, difficulty: Difficulty = "medium"): Task {
|
|
213
|
-
return { id: "BAKE", title: prompt, capability, difficulty, dependsOn: [], estTokens: estimateTokens(capability, difficulty) };
|
|
214
|
-
}
|
package/src/build-state.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Build persistence — checkpoint a run so a halt/crash/cancel can resume without
|
|
2
|
-
// re-paying for finished tasks. The orchestrator itself stays fs-free; this module
|
|
3
|
-
// (and run-build) own the disk I/O.
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import type { Task, TaskOutcome } from "./types.js";
|
|
7
|
-
|
|
8
|
-
export interface BuildState {
|
|
9
|
-
id: string;
|
|
10
|
-
/** The original idea/request text, for display in the projects list. */
|
|
11
|
-
idea?: string;
|
|
12
|
-
/** Workflow used: auto-run, or approval-gated. */
|
|
13
|
-
mode?: "auto" | "approval";
|
|
14
|
-
tasks: Task[];
|
|
15
|
-
/** Full record of every task run, including feedback-loop retries (append-only). */
|
|
16
|
-
outcomes: TaskOutcome[];
|
|
17
|
-
totalCost: number;
|
|
18
|
-
status: "running" | "complete" | "halted";
|
|
19
|
-
haltReason?: string;
|
|
20
|
-
/** Per-project budget cap (USD). Overrides the global default when set. */
|
|
21
|
-
budgetCapUSD?: number;
|
|
22
|
-
/** Cached AI retro narrative (generated on demand). */
|
|
23
|
-
retroNarrative?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function newBuildState(id: string, tasks: Task[], idea?: string, mode?: "auto" | "approval"): BuildState {
|
|
27
|
-
return { id, idea, mode, tasks, outcomes: [], totalCost: 0, status: "running" };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function saveState(state: BuildState, path: string): void {
|
|
31
|
-
writeFileSync(path, JSON.stringify(state, null, 2) + "\n");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function loadState(path: string): BuildState | undefined {
|
|
35
|
-
if (!existsSync(path)) return undefined;
|
|
36
|
-
try {
|
|
37
|
-
return JSON.parse(readFileSync(path, "utf-8")) as BuildState;
|
|
38
|
-
} catch (e) {
|
|
39
|
-
throw new Error(`Bad build state at ${path}: ${e instanceof Error ? e.message : e}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Which task ids are already finished (last outcome wins; a failed attempt does not
|
|
44
|
-
* count). Used to skip on resume. */
|
|
45
|
-
export function completedIds(state: BuildState): Set<string> {
|
|
46
|
-
return new Set(state.outcomes.filter((o) => !o.error).map((o) => o.taskId));
|
|
47
|
-
}
|
package/src/burndown.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Burndown — tasks remaining and cumulative spend across the build. There are no
|
|
2
|
-
// timestamps in build-state, so the X axis is task-completion order (step 1..N),
|
|
3
|
-
// which is the natural timeline for a build. Retries add a step (and cost) without
|
|
4
|
-
// burning down a task, so they show up as flat-remaining / rising-cost.
|
|
5
|
-
|
|
6
|
-
import type { BuildState } from "./build-state.js";
|
|
7
|
-
|
|
8
|
-
export interface BurndownStep {
|
|
9
|
-
taskId: string;
|
|
10
|
-
remaining: number; // distinct tasks still to do after this step
|
|
11
|
-
cumCost: number; // cumulative spend through this step
|
|
12
|
-
retry: boolean; // this step re-ran an already-done task
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Burndown {
|
|
16
|
-
taskCount: number;
|
|
17
|
-
totalCost: number;
|
|
18
|
-
steps: BurndownStep[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
22
|
-
|
|
23
|
-
export function computeBurndown(state: BuildState): Burndown {
|
|
24
|
-
const taskCount = state.tasks.length;
|
|
25
|
-
const done = new Set<string>();
|
|
26
|
-
let cum = 0;
|
|
27
|
-
const steps: BurndownStep[] = [];
|
|
28
|
-
for (const o of state.outcomes) {
|
|
29
|
-
const retry = done.has(o.taskId);
|
|
30
|
-
done.add(o.taskId);
|
|
31
|
-
cum += o.cost;
|
|
32
|
-
steps.push({ taskId: o.taskId, remaining: taskCount - done.size, cumCost: round2(cum), retry });
|
|
33
|
-
}
|
|
34
|
-
return { taskCount, totalCost: round2(cum), steps };
|
|
35
|
-
}
|
package/src/calibration.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// Self-calibrating token estimates. After each real task, we record its measured
|
|
2
|
-
// token usage per (capability, difficulty). estimateTokens uses the running average
|
|
3
|
-
// once there are enough samples, so estimates sharpen with use. Persisted globally.
|
|
4
|
-
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { Capability, Difficulty } from "./types.js";
|
|
9
|
-
|
|
10
|
-
interface Sample {
|
|
11
|
-
input: number; // total input tokens (fresh + cache-read)
|
|
12
|
-
output: number;
|
|
13
|
-
cachedFraction: number; // share of input served from cache
|
|
14
|
-
n: number; // sample count (capped so recent runs still move the average)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type Calibration = Record<string, Sample>;
|
|
18
|
-
|
|
19
|
-
const MIN_SAMPLES = 2; // trust calibration only after a couple of runs
|
|
20
|
-
const MAX_N = 20; // cap so old runs don't dominate
|
|
21
|
-
|
|
22
|
-
function calPath(): string {
|
|
23
|
-
return join(homedir(), ".projectinator", "calibration.json");
|
|
24
|
-
}
|
|
25
|
-
const key = (c: Capability, d: Difficulty) => `${c}/${d}`;
|
|
26
|
-
|
|
27
|
-
function load(): Calibration {
|
|
28
|
-
try {
|
|
29
|
-
if (!existsSync(calPath())) return {};
|
|
30
|
-
return JSON.parse(readFileSync(calPath(), "utf-8")) as Calibration;
|
|
31
|
-
} catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function save(cal: Calibration): void {
|
|
37
|
-
try {
|
|
38
|
-
mkdirSync(join(homedir(), ".projectinator"), { recursive: true });
|
|
39
|
-
writeFileSync(calPath(), JSON.stringify(cal, null, 2) + "\n");
|
|
40
|
-
} catch {
|
|
41
|
-
/* best effort — never break a build on a calibration write */
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Fold a real measurement into the running average for its bucket. */
|
|
46
|
-
export function recordActual(
|
|
47
|
-
capability: Capability,
|
|
48
|
-
difficulty: Difficulty,
|
|
49
|
-
inputTotal: number,
|
|
50
|
-
output: number,
|
|
51
|
-
cachedFraction: number,
|
|
52
|
-
): void {
|
|
53
|
-
if (!(inputTotal > 0)) return;
|
|
54
|
-
const cal = load();
|
|
55
|
-
const k = key(capability, difficulty);
|
|
56
|
-
const prev = cal[k];
|
|
57
|
-
if (!prev) {
|
|
58
|
-
cal[k] = { input: inputTotal, output, cachedFraction, n: 1 };
|
|
59
|
-
} else {
|
|
60
|
-
const n = Math.min(prev.n, MAX_N);
|
|
61
|
-
cal[k] = {
|
|
62
|
-
input: (prev.input * n + inputTotal) / (n + 1),
|
|
63
|
-
output: (prev.output * n + output) / (n + 1),
|
|
64
|
-
cachedFraction: (prev.cachedFraction * n + cachedFraction) / (n + 1),
|
|
65
|
-
n: prev.n + 1,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
save(cal);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** All recorded samples keyed "capability/difficulty" (for the accuracy view). */
|
|
72
|
-
export function allSamples(): Record<string, { input: number; output: number; cachedFraction: number; n: number }> {
|
|
73
|
-
return load();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Calibrated estimate for a bucket, once enough samples exist. */
|
|
77
|
-
export function calibratedTokens(
|
|
78
|
-
capability: Capability,
|
|
79
|
-
difficulty: Difficulty,
|
|
80
|
-
): { input: number; output: number; cachedInputFraction: number } | undefined {
|
|
81
|
-
const s = load()[key(capability, difficulty)];
|
|
82
|
-
if (!s || s.n < MIN_SAMPLES) return undefined;
|
|
83
|
-
return {
|
|
84
|
-
input: Math.round(s.input),
|
|
85
|
-
output: Math.round(s.output),
|
|
86
|
-
cachedInputFraction: Math.min(0.95, Math.max(0, s.cachedFraction)),
|
|
87
|
-
};
|
|
88
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
// Headless CLI — `projectinator <command>`. Same engine as the cockpit (engine.ts),
|
|
2
|
-
// same workspace root, so builds started here show up in the TUI's project list.
|
|
3
|
-
//
|
|
4
|
-
// doctor check Node, keys, Chromium, Pi catalog, git
|
|
5
|
-
// build "<idea>" [...] plan + build without the TUI
|
|
6
|
-
// projects list past builds
|
|
7
|
-
// models the roster as it will actually run, with prices
|
|
8
|
-
//
|
|
9
|
-
// The launcher (bin/projectinator.mjs) handles --version/--help itself and only
|
|
10
|
-
// spawns this file for a real command, so those stay instant.
|
|
11
|
-
|
|
12
|
-
import { createInterface } from "node:readline";
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { accessSync, constants, mkdirSync } from "node:fs";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import type { OrchestratorEvent } from "./orchestrator.js";
|
|
18
|
-
import type { Provider } from "./types.js";
|
|
19
|
-
import { MODELS, getModel } from "./models.js";
|
|
20
|
-
import { REGISTRY } from "./registry.js";
|
|
21
|
-
import { piRuntime, resolvePiModel } from "./executor.js";
|
|
22
|
-
import { chromiumAvailable, CHROMIUM_INSTALL_HINT } from "./preview.js";
|
|
23
|
-
import { applyKeysToEnv, getPrefs, loadConfig, ENV_VAR } from "./tui/config.js";
|
|
24
|
-
import {
|
|
25
|
-
availableProviders,
|
|
26
|
-
effectiveRoster,
|
|
27
|
-
listProjects,
|
|
28
|
-
planBuild,
|
|
29
|
-
startBuild,
|
|
30
|
-
PROVIDER_LABEL,
|
|
31
|
-
} from "./tui/engine.js";
|
|
32
|
-
|
|
33
|
-
const NODE_MIN = "22.19.0";
|
|
34
|
-
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
35
|
-
|
|
36
|
-
// ---- tiny argv parser: `--flag value`, `--flag=value`, `--bool`, positionals ----
|
|
37
|
-
|
|
38
|
-
interface Argv {
|
|
39
|
-
positional: string[];
|
|
40
|
-
flags: Record<string, string | true>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function parseArgv(args: string[]): Argv {
|
|
44
|
-
const out: Argv = { positional: [], flags: {} };
|
|
45
|
-
for (let i = 0; i < args.length; i++) {
|
|
46
|
-
const a = args[i]!;
|
|
47
|
-
if (!a.startsWith("--")) { out.positional.push(a); continue; }
|
|
48
|
-
const eq = a.indexOf("=");
|
|
49
|
-
if (eq > 0) { out.flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
|
50
|
-
const next = args[i + 1];
|
|
51
|
-
if (next !== undefined && !next.startsWith("--")) { out.flags[a.slice(2)] = next; i++; }
|
|
52
|
-
else out.flags[a.slice(2)] = true;
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function num(v: string | true | undefined, fallback: number): number {
|
|
58
|
-
if (v === undefined || v === true) return fallback;
|
|
59
|
-
const n = parseFloat(v);
|
|
60
|
-
return Number.isFinite(n) ? n : fallback;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function semverGte(a: string, b: string): boolean {
|
|
64
|
-
const pa = a.split(".").map(Number);
|
|
65
|
-
const pb = b.split(".").map(Number);
|
|
66
|
-
for (let i = 0; i < 3; i++) {
|
|
67
|
-
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
68
|
-
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
69
|
-
}
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---- doctor ----
|
|
74
|
-
|
|
75
|
-
type Check = { label: string; ok: boolean; detail: string; fatal?: boolean };
|
|
76
|
-
|
|
77
|
-
async function doctor(): Promise<number> {
|
|
78
|
-
const checks: Check[] = [];
|
|
79
|
-
|
|
80
|
-
const node = process.versions.node;
|
|
81
|
-
checks.push({ label: "Node", ok: semverGte(node, NODE_MIN), detail: `v${node} (need ≥ ${NODE_MIN})`, fatal: true });
|
|
82
|
-
|
|
83
|
-
const cfg = loadConfig();
|
|
84
|
-
const providers = availableProviders();
|
|
85
|
-
for (const p of Object.keys(ENV_VAR) as Provider[]) {
|
|
86
|
-
const has = providers.includes(p);
|
|
87
|
-
const src = cfg.keys[p] ? "~/.projectinator/config.json" : has ? "env" : "";
|
|
88
|
-
checks.push({ label: `Key: ${PROVIDER_LABEL[p]}`, ok: has, detail: has ? `set (${src})` : `not set — Settings → API keys, or export ${ENV_VAR[p]}` });
|
|
89
|
-
}
|
|
90
|
-
if (providers.length === 0) checks.push({ label: "Any provider key", ok: false, detail: "no keys at all — nothing can run", fatal: true });
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const runtime = await piRuntime();
|
|
94
|
-
const bad: string[] = [];
|
|
95
|
-
for (const e of REGISTRY) for (const b of ["api", "web"] as const) {
|
|
96
|
-
try { resolvePiModel(runtime, e.byBackend[b].provider, e.byBackend[b].model); } catch { bad.push(`${e.capability}/${e.tier}/${b}`); }
|
|
97
|
-
}
|
|
98
|
-
checks.push({ label: "Pi model catalog", ok: bad.length === 0, detail: bad.length ? `unresolved: ${bad.join(", ")}` : `${Object.keys(MODELS).length} models priced, every registry pick resolves`, fatal: true });
|
|
99
|
-
} catch (e) {
|
|
100
|
-
checks.push({ label: "Pi model catalog", ok: false, detail: e instanceof Error ? e.message : String(e), fatal: true });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const chromium = await chromiumAvailable();
|
|
104
|
-
checks.push({ label: "Headless Chromium", ok: chromium, detail: chromium ? "installed — tester runs the app" : `missing — tests will be code-reading only (PASS*); ${CHROMIUM_INSTALL_HINT}` });
|
|
105
|
-
|
|
106
|
-
const git = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
107
|
-
checks.push({ label: "git", ok: git.status === 0, detail: git.status === 0 ? git.stdout.trim() : "not found — builds won't be versioned (undo/history disabled)" });
|
|
108
|
-
|
|
109
|
-
const home = join(homedir(), ".projectinator");
|
|
110
|
-
try { mkdirSync(home, { recursive: true }); accessSync(home, constants.W_OK); checks.push({ label: "Data dir", ok: true, detail: home }); }
|
|
111
|
-
catch { checks.push({ label: "Data dir", ok: false, detail: `${home} not writable`, fatal: true }); }
|
|
112
|
-
|
|
113
|
-
const prefs = getPrefs();
|
|
114
|
-
checks.push({ label: "Prefs", ok: true, detail: `budget cap ${money(prefs.budgetCapUSD)} · ${prefs.concurrency} at once · task limits ${prefs.taskTimeoutMin || "∞"} min / ${prefs.taskCostCapUSD ? money(prefs.taskCostCapUSD) : "∞"}` });
|
|
115
|
-
|
|
116
|
-
console.log("\n projectinator doctor\n");
|
|
117
|
-
for (const c of checks) console.log(` ${c.ok ? "✓" : c.fatal ? "✗" : "!"} ${c.label.padEnd(22)} ${c.detail}`);
|
|
118
|
-
const fatal = checks.filter((c) => !c.ok && c.fatal);
|
|
119
|
-
const warn = checks.filter((c) => !c.ok && !c.fatal);
|
|
120
|
-
console.log(`\n ${fatal.length ? `${fatal.length} blocking problem${fatal.length === 1 ? "" : "s"}` : "Ready"}${warn.length ? ` · ${warn.length} warning${warn.length === 1 ? "" : "s"}` : ""}\n`);
|
|
121
|
-
return fatal.length ? 1 : 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ---- projects ----
|
|
125
|
-
|
|
126
|
-
function projects(): number {
|
|
127
|
-
const list = listProjects();
|
|
128
|
-
if (!list.length) { console.log("\n No projects yet. Run `projectinator` or `projectinator build \"an idea\"`.\n"); return 0; }
|
|
129
|
-
console.log("");
|
|
130
|
-
for (const p of list) {
|
|
131
|
-
const done = p.state.outcomes.filter((o) => !o.error).length;
|
|
132
|
-
const mark = p.status === "complete" ? "✓" : p.status === "halted" ? "⚠" : "…";
|
|
133
|
-
console.log(` ${mark} ${p.slug.padEnd(40)} ${p.status.padEnd(9)} ${money(p.totalCost).padStart(8)} ${done}/${p.taskCount} tasks`);
|
|
134
|
-
console.log(` ${p.idea.slice(0, 90)}${p.idea.length > 90 ? "…" : ""}`);
|
|
135
|
-
}
|
|
136
|
-
console.log(`\n ${list.length} project${list.length === 1 ? "" : "s"} · ${money(list.reduce((a, p) => a + p.totalCost, 0))} all time · ${join(homedir(), "…")}\n`);
|
|
137
|
-
return 0;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ---- models ----
|
|
141
|
-
|
|
142
|
-
function models(): number {
|
|
143
|
-
const providers = availableProviders();
|
|
144
|
-
console.log(`\n Roster as it will run now${providers.length ? ` (keys: ${providers.join(", ")})` : " (no keys — best-of-breed picks shown)"}\n`);
|
|
145
|
-
for (const r of effectiveRoster()) {
|
|
146
|
-
const m = r.model ? getModel(r.model) : undefined;
|
|
147
|
-
const price = m ? `$${m.cost.input}/$${m.cost.output} per 1M` : "";
|
|
148
|
-
console.log(` ${r.label.padEnd(18)} ${(r.model ?? "—").padEnd(28)} ${(r.provider ?? "").padEnd(11)} ${price}`);
|
|
149
|
-
}
|
|
150
|
-
console.log("\n Change in the app: Settings → Models. Prices are from Pi's catalog; actual cost is measured per run.\n");
|
|
151
|
-
return 0;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---- build ----
|
|
155
|
-
|
|
156
|
-
function ask(question: string): Promise<string> {
|
|
157
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
158
|
-
const { promise, resolve } = Promise.withResolvers<string>();
|
|
159
|
-
rl.question(question, (a) => { rl.close(); resolve(a.trim()); });
|
|
160
|
-
return promise;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function build(argv: Argv): Promise<number> {
|
|
164
|
-
const idea = argv.positional.join(" ").trim();
|
|
165
|
-
const json = argv.flags.json === true;
|
|
166
|
-
const yes = json || argv.flags.yes === true || argv.flags.y === true;
|
|
167
|
-
const dry = argv.flags["dry-run"] === true;
|
|
168
|
-
const emit = (o: Record<string, unknown>) => { if (json) process.stdout.write(JSON.stringify(o) + "\n"); };
|
|
169
|
-
const say = (s: string) => { if (!json) console.log(s); };
|
|
170
|
-
|
|
171
|
-
if (!idea) { console.error(" build: give an idea, e.g. projectinator build \"a tip calculator\""); return 2; }
|
|
172
|
-
|
|
173
|
-
let providers = availableProviders();
|
|
174
|
-
const lock = argv.flags.provider;
|
|
175
|
-
if (typeof lock === "string") {
|
|
176
|
-
if (!(lock in PROVIDER_LABEL)) { console.error(` build: unknown provider "${lock}" (anthropic | openai | google | openrouter)`); return 2; }
|
|
177
|
-
if (!providers.includes(lock as Provider)) { console.error(` build: no key for ${lock} — export ${ENV_VAR[lock as Provider]} or set it in the app`); return 1; }
|
|
178
|
-
providers = [lock as Provider];
|
|
179
|
-
}
|
|
180
|
-
if (!providers.length) { console.error(" build: no API key found. Run `projectinator doctor`."); return 1; }
|
|
181
|
-
|
|
182
|
-
const prefs = getPrefs();
|
|
183
|
-
const budget = num(argv.flags.budget, prefs.budgetCapUSD);
|
|
184
|
-
const concurrency = Math.max(1, Math.floor(num(argv.flags.concurrency, prefs.concurrency)));
|
|
185
|
-
const taskLimits = {
|
|
186
|
-
timeoutMs: num(argv.flags["task-timeout"], prefs.taskTimeoutMin) * 60_000,
|
|
187
|
-
costCapUSD: num(argv.flags["task-cap"], prefs.taskCostCapUSD),
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
say(`\n Planning with the PM (${providers.join("/")})…`);
|
|
191
|
-
const plan = await planBuild(idea, providers);
|
|
192
|
-
emit({ event: "plan", provider: plan.provider, modelId: plan.modelId, estCost: plan.estCost, tasks: plan.tasks });
|
|
193
|
-
say(`\n ${plan.tasks.length} tasks · estimated ${money(plan.estCost)} · cap ${money(budget)}${plan.lock ? ` · locked to ${plan.lock}` : ""}\n`);
|
|
194
|
-
for (const t of plan.tasks) {
|
|
195
|
-
const dep = t.dependsOn?.length ? ` ← ${t.dependsOn.join(", ")}` : "";
|
|
196
|
-
say(` ${t.id.padEnd(6)} ${t.capability.padEnd(7)} ${t.difficulty.padEnd(8)} ${t.title}${dep}`);
|
|
197
|
-
}
|
|
198
|
-
if (plan.estCost > budget) say(`\n ⚠ Estimate exceeds the cap — the build may halt partway.`);
|
|
199
|
-
|
|
200
|
-
if (dry) { say("\n Dry run — nothing built (the PM call above was the only spend).\n"); return 0; }
|
|
201
|
-
if (!yes) {
|
|
202
|
-
const a = await ask(`\n Build it for ~${money(plan.estCost)}? [y/N] `);
|
|
203
|
-
if (!/^y(es)?$/i.test(a)) { say(" Cancelled.\n"); return 0; }
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const onEvent = (e: OrchestratorEvent) => {
|
|
207
|
-
emit({ event: e.type, ...e });
|
|
208
|
-
if (json) return;
|
|
209
|
-
if (e.type === "task_start") console.log(` ▶ ${e.task.id} [${e.task.capability}] → ${e.provider}/${e.modelId}${e.round ? ` (round ${e.round})` : ""}`);
|
|
210
|
-
else if (e.type === "task_done") console.log(` ✓ ${e.outcome.taskId} ${money(e.outcome.cost)} running ${money(e.runningTotal)}${e.outcome.verdict ? ` ${e.outcome.verdict.passed ? (e.outcome.verdict.runtimeChecked || e.outcome.capability !== "test" ? "PASS" : "PASS* (app not executed)") : "FAIL"}` : ""}`);
|
|
211
|
-
else if (e.type === "task_failed") console.log(` ⛔ ${e.outcome.taskId} aborted: ${e.outcome.error} — billed ${money(e.outcome.cost)}`);
|
|
212
|
-
else if (e.type === "task_skipped") console.log(` · ${e.taskId} skipped`);
|
|
213
|
-
else if (e.type === "test_failed") console.log(` ✗ ${e.taskId} failed (${e.bugs} bugs) — round ${e.round}`);
|
|
214
|
-
else if (e.type === "retry_dev") console.log(` ↻ re-running ${e.taskId} to fix ${e.forTest}`);
|
|
215
|
-
else if (e.type === "budget_halt") console.log(` ⚠ budget halt at ${money(e.runningTotal)} (cap ${money(e.cap)})`);
|
|
216
|
-
else if (e.type === "cycle_or_error") console.log(` ✗ ${e.message}`);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
say(`\n Building…\n`);
|
|
220
|
-
const handle = startBuild(idea, plan, { concurrency, budgetCapUSD: budget, taskLimits, onEvent, mode: "auto" });
|
|
221
|
-
const r = await handle.promise;
|
|
222
|
-
emit({ event: "done", halted: r.halted, haltReason: r.haltReason, totalCost: r.totalCost, files: r.files, workspace: handle.workspace });
|
|
223
|
-
say(`\n ${r.halted ? `⚠ Halted (${r.haltReason ?? "?"})` : "✓ Complete"} · ${money(r.totalCost)} · ${r.files.length} file${r.files.length === 1 ? "" : "s"}`);
|
|
224
|
-
say(` ${handle.workspace}\n`);
|
|
225
|
-
return r.halted ? 3 : 0;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ---- main ----
|
|
229
|
-
|
|
230
|
-
const USAGE = `Usage: projectinator <command> [options]
|
|
231
|
-
|
|
232
|
-
doctor check Node, API keys, Chromium, Pi catalog, git
|
|
233
|
-
build "<idea>" [options] plan + build headless (same workspace as the app)
|
|
234
|
-
--dry-run plan and estimate only (spends one PM call)
|
|
235
|
-
--yes, -y don't ask before building
|
|
236
|
-
--json NDJSON events on stdout (implies --yes)
|
|
237
|
-
--budget <usd> cap for this build (default: your prefs)
|
|
238
|
-
--provider <name> anthropic|openai|google|openrouter (default: prefs / keys)
|
|
239
|
-
--concurrency <n> tasks at once (default: your prefs)
|
|
240
|
-
--task-cap <usd> per-task cost ceiling (default: your prefs; 0 = off)
|
|
241
|
-
--task-timeout <min> per-task timeout (default: your prefs; 0 = off)
|
|
242
|
-
projects list past builds with status and cost
|
|
243
|
-
models the roster as it will run, with prices
|
|
244
|
-
|
|
245
|
-
Exit codes: 0 ok · 1 environment problem · 2 bad usage · 3 build halted`;
|
|
246
|
-
|
|
247
|
-
export async function main(args: string[]): Promise<number> {
|
|
248
|
-
applyKeysToEnv();
|
|
249
|
-
const argv = parseArgv(args);
|
|
250
|
-
const cmd = argv.positional.shift();
|
|
251
|
-
switch (cmd) {
|
|
252
|
-
case "doctor": return doctor();
|
|
253
|
-
case "projects": return projects();
|
|
254
|
-
case "models": return models();
|
|
255
|
-
case "build": return build(argv);
|
|
256
|
-
case undefined: case "help": console.log(USAGE); return cmd ? 0 : 2;
|
|
257
|
-
default: console.error(`projectinator: unknown command "${cmd}".\n\n${USAGE}`); return 2;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file://").href) {
|
|
262
|
-
process.exitCode = await main(process.argv.slice(2));
|
|
263
|
-
}
|