projectinator 0.3.0 → 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 +30 -11
- package/bin/projectinator.mjs +56 -11
- 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/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/tui/notify.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// Fire a desktop notification + sound when a build finishes, so you can look away
|
|
2
|
-
// during a build and get pinged. macOS uses osascript; other platforms ring the bell.
|
|
3
|
-
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
export function notifyBuildDone(title: string, message: string): void {
|
|
7
|
-
try {
|
|
8
|
-
if (process.platform === "darwin") {
|
|
9
|
-
const script = `display notification ${JSON.stringify(message)} with title ${JSON.stringify(title)} sound name "Glass"`;
|
|
10
|
-
const child = spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" });
|
|
11
|
-
child.unref();
|
|
12
|
-
} else if (process.platform === "win32") {
|
|
13
|
-
// PowerShell balloon-free ping: just the console bell.
|
|
14
|
-
process.stdout.write("\x07");
|
|
15
|
-
} else {
|
|
16
|
-
process.stdout.write("\x07"); // terminal bell
|
|
17
|
-
}
|
|
18
|
-
} catch {
|
|
19
|
-
/* best effort — never break the build on a notification failure */
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/tui/panels.tsx
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Small PM panels: the AI Team roster, the Standup summary line, and a List view.
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { Box, Text } from "ink";
|
|
5
|
-
import Spinner from "ink-spinner";
|
|
6
|
-
import { C, ROLE_META, Panel, Chip } from "./components.js";
|
|
7
|
-
import { groupByEpic, type BoardTask } from "./Kanban.js";
|
|
8
|
-
import { effectiveRoster, modelLabel } from "./engine.js";
|
|
9
|
-
|
|
10
|
-
/** The AI team: each role and the model playing it right now. */
|
|
11
|
-
export function Team(): React.ReactElement {
|
|
12
|
-
const rows = effectiveRoster();
|
|
13
|
-
return (
|
|
14
|
-
<Panel title="Your team">
|
|
15
|
-
{rows.map((r) => (
|
|
16
|
-
<Box key={r.capability}>
|
|
17
|
-
<Text>{ROLE_META[r.capability].emoji} </Text>
|
|
18
|
-
<Box width={16}><Text color={C.dim}>{ROLE_META[r.capability].label}</Text></Box>
|
|
19
|
-
<Text color={C.text}>{modelLabel(r.model ?? "—")}</Text>
|
|
20
|
-
</Box>
|
|
21
|
-
))}
|
|
22
|
-
</Panel>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isDone(t: BoardTask): boolean {
|
|
27
|
-
return t.status === "done" || t.status === "skipped";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** One-line standup: what's done, running, ready, spent, and needing attention. */
|
|
31
|
-
export function Standup({ tasks, spent }: { tasks: BoardTask[]; spent?: number }): React.ReactElement {
|
|
32
|
-
const done = tasks.filter(isDone).length;
|
|
33
|
-
const running = tasks.filter((t) => t.status === "running").length;
|
|
34
|
-
const failed = tasks.filter((t) => t.status === "failed" || t.verdict === "FAIL").length;
|
|
35
|
-
const doneIds = new Set(tasks.filter(isDone).map((t) => t.id));
|
|
36
|
-
const ready = tasks.filter((t) => t.status === "pending" && (t.dependsOn ?? []).every((d) => doneIds.has(d))).length;
|
|
37
|
-
const backlog = tasks.filter((t) => t.status === "pending" && !(t.dependsOn ?? []).every((d) => doneIds.has(d))).length;
|
|
38
|
-
const cost = spent ?? tasks.reduce((a, t) => a + (t.cost ?? 0), 0);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<Box gap={1} flexWrap="wrap">
|
|
42
|
-
<Chip label={`${done} done`} dotColor={C.good} />
|
|
43
|
-
{running > 0 ? <Chip label={`${running} running`} dotColor={C.info} /> : null}
|
|
44
|
-
<Chip label={`${ready} ready`} dotColor="cyan" />
|
|
45
|
-
<Chip label={`${backlog} backlog`} />
|
|
46
|
-
<Chip label={`$${cost.toFixed(2)}`} dotColor={C.accent} />
|
|
47
|
-
{failed > 0 ? <Chip label={`${failed} review`} dotColor={C.bad} active /> : null}
|
|
48
|
-
</Box>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const STATUS_MARK: Record<BoardTask["status"], { m: string; c: string }> = {
|
|
53
|
-
done: { m: "✓", c: C.good },
|
|
54
|
-
skipped: { m: "·", c: C.dim },
|
|
55
|
-
running: { m: "●", c: "cyan" },
|
|
56
|
-
failed: { m: "✗", c: C.bad },
|
|
57
|
-
pending: { m: "○", c: C.dim },
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** Flat list view, grouped by epic. Read-only alternative to the board. */
|
|
61
|
-
export function ListView({ tasks }: { tasks: BoardTask[] }): React.ReactElement {
|
|
62
|
-
const lanes = groupByEpic(tasks);
|
|
63
|
-
return (
|
|
64
|
-
<Box flexDirection="column">
|
|
65
|
-
{lanes.map((lane) => (
|
|
66
|
-
<Box key={lane.epic} flexDirection="column" marginBottom={1}>
|
|
67
|
-
<Text color={C.accent}>▊ {lane.epic}</Text>
|
|
68
|
-
{lane.tasks.map((t) => {
|
|
69
|
-
const s = STATUS_MARK[t.status];
|
|
70
|
-
return (
|
|
71
|
-
<Box key={t.id}>
|
|
72
|
-
<Box width={2}>
|
|
73
|
-
{t.status === "running" ? <Text color="cyan"><Spinner type="dots" /></Text> : <Text color={s.c}>{s.m}</Text>}
|
|
74
|
-
</Box>
|
|
75
|
-
<Box width={7}><Text color={C.dim}>{t.id}</Text></Box>
|
|
76
|
-
<Box width={3}><Text>{ROLE_META[t.capability].emoji}</Text></Box>
|
|
77
|
-
<Box flexGrow={1}><Text color={C.text} wrap="truncate-end">{t.title}</Text></Box>
|
|
78
|
-
<Box width={10} justifyContent="flex-end">
|
|
79
|
-
{t.verdict ? <Text color={t.verdict === "FAIL" ? C.bad : t.verdict === "PASS*" ? C.warn : C.good}>{t.verdict}</Text>
|
|
80
|
-
: t.cost ? <Text color={C.dim}>${t.cost.toFixed(2)}</Text> : <Text> </Text>}
|
|
81
|
-
</Box>
|
|
82
|
-
</Box>
|
|
83
|
-
);
|
|
84
|
-
})}
|
|
85
|
-
</Box>
|
|
86
|
-
))}
|
|
87
|
-
</Box>
|
|
88
|
-
);
|
|
89
|
-
}
|
package/src/tui/templates.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Starter templates — curated, detailed idea prompts. Picking one skips the blank
|
|
2
|
-
// page and gives the PM a strong brief to decompose. Users can also save their own
|
|
3
|
-
// (persisted in ~/.projectinator/templates.json) and share them as portable files.
|
|
4
|
-
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
export interface Template {
|
|
10
|
-
name: string;
|
|
11
|
-
blurb: string;
|
|
12
|
-
idea: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const TEMPLATES: Template[] = [
|
|
16
|
-
{
|
|
17
|
-
name: "Product landing page",
|
|
18
|
-
blurb: "Hero, features, pricing, CTA",
|
|
19
|
-
idea: "A single-page product landing site: a hero with headline + subheadline + call-to-action button, a 3-column features section, a simple pricing table, a short testimonials row, and a footer. Clean modern design, responsive, self-contained HTML with embedded CSS.",
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: "Personal portfolio",
|
|
23
|
-
blurb: "About, projects, contact",
|
|
24
|
-
idea: "A personal portfolio site: a hero with my name and one-line intro, an about section, a projects grid with 3 sample cards (image, title, short description, link), and a contact section with a simple form. Tasteful typography, responsive, single self-contained HTML file.",
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "Link-in-bio page",
|
|
28
|
-
blurb: "Avatar, bio, link buttons",
|
|
29
|
-
idea: "A centered link-in-bio page: a round avatar, a name, a one-line bio, and a vertical stack of 4 link buttons (Twitter, GitHub, Email, Website). Soft gradient background, mobile-first, single self-contained HTML file.",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: "Coming-soon / waitlist",
|
|
33
|
-
blurb: "Countdown + email signup",
|
|
34
|
-
idea: "A coming-soon page: a big headline, a short pitch, a live countdown timer to a launch date, and an email signup form with inline validation and a success message. Bold, centered, single self-contained HTML file.",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: "Restaurant / cafe site",
|
|
38
|
-
blurb: "Hero, menu, hours, contact",
|
|
39
|
-
idea: "A cafe website: a hero with the cafe name and a photo-style gradient banner, a menu section grouped into Coffee / Food / Pastries with prices, an opening-hours block, a location + contact section with a simple form. Warm, inviting design, responsive, single self-contained HTML file.",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: "Blog home page",
|
|
43
|
-
blurb: "Header, post list, sidebar",
|
|
44
|
-
idea: "A blog home page: a header with the blog title and nav, a main column listing 4 post previews (title, date, excerpt, read-more link), a sidebar with an about box and a tag list, and a footer. Readable editorial typography, responsive, single self-contained HTML file.",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: "SaaS dashboard UI",
|
|
48
|
-
blurb: "Sidebar, stat cards, table",
|
|
49
|
-
idea: "A SaaS dashboard UI (front-end only, mock data): a left sidebar nav, a top bar, a row of 4 stat cards, a simple line-chart placeholder, and a recent-activity table. Clean product design, responsive, single self-contained HTML file with embedded CSS/JS.",
|
|
50
|
-
},
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
// ---- user templates (saved + shared) ----
|
|
54
|
-
|
|
55
|
-
function homeDir(): string {
|
|
56
|
-
const d = join(homedir(), ".projectinator");
|
|
57
|
-
mkdirSync(d, { recursive: true });
|
|
58
|
-
return d;
|
|
59
|
-
}
|
|
60
|
-
function userTemplatesPath(): string {
|
|
61
|
-
return join(homeDir(), "templates.json");
|
|
62
|
-
}
|
|
63
|
-
function slug(name: string): string {
|
|
64
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "template";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function loadUserTemplates(): Template[] {
|
|
68
|
-
try {
|
|
69
|
-
const raw = JSON.parse(readFileSync(userTemplatesPath(), "utf8")) as Template[];
|
|
70
|
-
return Array.isArray(raw) ? raw : [];
|
|
71
|
-
} catch {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function writeUserTemplates(list: Template[]): void {
|
|
76
|
-
writeFileSync(userTemplatesPath(), JSON.stringify(list, null, 2) + "\n");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Built-in + user templates, tagged. */
|
|
80
|
-
export function allTemplates(): (Template & { builtin: boolean })[] {
|
|
81
|
-
return [
|
|
82
|
-
...TEMPLATES.map((t) => ({ ...t, builtin: true })),
|
|
83
|
-
...loadUserTemplates().map((t) => ({ ...t, builtin: false })),
|
|
84
|
-
];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Save (or overwrite by name) a user template. */
|
|
88
|
-
export function saveUserTemplate(t: Template): void {
|
|
89
|
-
const list = loadUserTemplates().filter((x) => x.name !== t.name);
|
|
90
|
-
list.push(t);
|
|
91
|
-
writeUserTemplates(list);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function deleteUserTemplate(name: string): void {
|
|
95
|
-
writeUserTemplates(loadUserTemplates().filter((x) => x.name !== name));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Write a template to a portable file others can import. Returns the path. */
|
|
99
|
-
export function exportTemplate(t: Template): string {
|
|
100
|
-
const dir = join(homeDir(), "exports");
|
|
101
|
-
mkdirSync(dir, { recursive: true });
|
|
102
|
-
const p = join(dir, `${slug(t.name)}.pitemplate.json`);
|
|
103
|
-
writeFileSync(p, JSON.stringify(t, null, 2) + "\n");
|
|
104
|
-
return p;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Import a template from a shared file. Adds it to the user's templates. */
|
|
108
|
-
export function importTemplate(filePath: string): Template {
|
|
109
|
-
const clean = filePath.trim().replace(/^['"]|['"]$/g, "").replace(/\\(.)/g, "$1");
|
|
110
|
-
const abs = clean.startsWith("~") ? homedir() + clean.slice(1) : clean;
|
|
111
|
-
if (!existsSync(abs)) throw new Error(`File not found: ${abs}`);
|
|
112
|
-
const raw = JSON.parse(readFileSync(abs, "utf8")) as Partial<Template>;
|
|
113
|
-
if (!raw || typeof raw.name !== "string" || typeof raw.idea !== "string") {
|
|
114
|
-
throw new Error("Not a valid template file (needs name + idea).");
|
|
115
|
-
}
|
|
116
|
-
const t: Template = { name: raw.name, blurb: String(raw.blurb ?? ""), idea: raw.idea };
|
|
117
|
-
saveUserTemplate(t);
|
|
118
|
-
return t;
|
|
119
|
-
}
|
package/src/tui/theme.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Custom @inkjs/ui theme — recolors the library's components (blue/green by default)
|
|
2
|
-
// to our amber accent so the whole app reads as one system.
|
|
3
|
-
|
|
4
|
-
import { defaultTheme, extendTheme } from "@inkjs/ui";
|
|
5
|
-
|
|
6
|
-
const AMBER = "#e0a72d";
|
|
7
|
-
|
|
8
|
-
export const uiTheme = extendTheme(defaultTheme, {
|
|
9
|
-
components: {
|
|
10
|
-
Select: {
|
|
11
|
-
styles: {
|
|
12
|
-
focusIndicator: () => ({ color: AMBER }),
|
|
13
|
-
selectedIndicator: () => ({ color: AMBER }),
|
|
14
|
-
// Unselected items are explicitly white (not `undefined`, which renders
|
|
15
|
-
// as the terminal's default fg and reads gray) so Select rows match the
|
|
16
|
-
// custom GroupedMenu's white unselected rows.
|
|
17
|
-
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
|
18
|
-
color: isFocused || isSelected ? AMBER : "white",
|
|
19
|
-
}),
|
|
20
|
-
highlightedText: () => ({ color: AMBER, bold: true }),
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
MultiSelect: {
|
|
24
|
-
styles: {
|
|
25
|
-
focusIndicator: () => ({ color: AMBER }),
|
|
26
|
-
selectedIndicator: () => ({ color: AMBER }),
|
|
27
|
-
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
|
|
28
|
-
color: isFocused || isSelected ? AMBER : "white",
|
|
29
|
-
}),
|
|
30
|
-
highlightedText: () => ({ color: AMBER, bold: true }),
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
Spinner: {
|
|
34
|
-
styles: {
|
|
35
|
-
frame: () => ({ color: AMBER }),
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
ProgressBar: {
|
|
39
|
-
styles: {
|
|
40
|
-
completed: () => ({ color: AMBER }),
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
});
|
package/src/tui/validate.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// Validate an API key by hitting each provider's models endpoint (free, no tokens).
|
|
2
|
-
// 200 = the key works; 401/403 = rejected. Used before saving a key in Settings so
|
|
3
|
-
// bad keys never get stored (the root cause of silent 0-token routing failures).
|
|
4
|
-
|
|
5
|
-
import type { Provider } from "../types.js";
|
|
6
|
-
|
|
7
|
-
export interface KeyCheck {
|
|
8
|
-
ok: boolean;
|
|
9
|
-
error?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function withTimeout(url: string, init: RequestInit, ms = 8000): Promise<Response> {
|
|
13
|
-
const ctrl = new AbortController();
|
|
14
|
-
const timer = setTimeout(() => ctrl.abort(), ms);
|
|
15
|
-
try {
|
|
16
|
-
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
17
|
-
} finally {
|
|
18
|
-
clearTimeout(timer);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function validateKey(provider: Provider, key: string): Promise<KeyCheck> {
|
|
23
|
-
if (!key.trim()) return { ok: false, error: "Empty key." };
|
|
24
|
-
try {
|
|
25
|
-
let res: Response;
|
|
26
|
-
if (provider === "anthropic") {
|
|
27
|
-
res = await withTimeout("https://api.anthropic.com/v1/models", {
|
|
28
|
-
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
29
|
-
});
|
|
30
|
-
} else if (provider === "openai") {
|
|
31
|
-
res = await withTimeout("https://api.openai.com/v1/models", {
|
|
32
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
33
|
-
});
|
|
34
|
-
} else if (provider === "openrouter") {
|
|
35
|
-
// Returns the key's rate-limit/usage info; 401 if the key is bad.
|
|
36
|
-
res = await withTimeout("https://openrouter.ai/api/v1/key", {
|
|
37
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
38
|
-
});
|
|
39
|
-
} else {
|
|
40
|
-
res = await withTimeout(
|
|
41
|
-
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}`,
|
|
42
|
-
{},
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (res.ok) return { ok: true };
|
|
46
|
-
if (res.status === 401 || res.status === 403) return { ok: false, error: "Key rejected — check it's correct and active." };
|
|
47
|
-
if (res.status === 400) return { ok: false, error: "Key rejected (bad request) — likely wrong key." };
|
|
48
|
-
return { ok: false, error: `Provider returned HTTP ${res.status}.` };
|
|
49
|
-
} catch (e) {
|
|
50
|
-
const msg = e instanceof Error && e.name === "AbortError" ? "Timed out — check your connection." : "Couldn't reach the provider.";
|
|
51
|
-
return { ok: false, error: msg };
|
|
52
|
-
}
|
|
53
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
// Core domain types for Projectinator's brain layer.
|
|
2
|
-
// Phase 1 is pure data + logic — no Pi, no network, no agents.
|
|
3
|
-
|
|
4
|
-
/** How we reach a model. `api` = metered + stable. `web` = user's existing
|
|
5
|
-
* web subscription, ~free but brittle (breaks on UI changes, no structured output). */
|
|
6
|
-
export type Backend = "api" | "web";
|
|
7
|
-
|
|
8
|
-
/** What kind of work a task needs. Roles bind to capabilities, never to model names. */
|
|
9
|
-
export type Capability = "plan" | "design" | "code" | "review" | "test" | "ops";
|
|
10
|
-
|
|
11
|
-
/** How hard the task is. Drives which tier of model we spend on. */
|
|
12
|
-
export type Difficulty = "trivial" | "low" | "medium" | "high";
|
|
13
|
-
|
|
14
|
-
/** Capability tier — the abstract "how strong a model" axis. */
|
|
15
|
-
export type Tier = "fast" | "mid" | "high";
|
|
16
|
-
|
|
17
|
-
export type Provider = "anthropic" | "openai" | "google" | "openrouter";
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Model pricing — mirrors Pi's models.json `cost` shape so it ports 1:1 later.
|
|
21
|
-
// All rates are USD per 1,000,000 tokens.
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export interface CostTier {
|
|
25
|
-
/** This tier applies once cumulative input tokens exceed this threshold. */
|
|
26
|
-
inputTokensAbove: number;
|
|
27
|
-
input: number;
|
|
28
|
-
output: number;
|
|
29
|
-
cacheRead?: number;
|
|
30
|
-
cacheWrite?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ModelCost {
|
|
34
|
-
input: number;
|
|
35
|
-
output: number;
|
|
36
|
-
cacheRead?: number;
|
|
37
|
-
cacheWrite?: number;
|
|
38
|
-
/** Optional volume tiers; highest matching threshold wins. */
|
|
39
|
-
tiers?: CostTier[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface Model {
|
|
43
|
-
id: string;
|
|
44
|
-
provider: Provider;
|
|
45
|
-
name: string;
|
|
46
|
-
contextWindow: number;
|
|
47
|
-
cost: ModelCost;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Registry — the swappable brain. capability + tier -> model, per backend.
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
export interface BackendPick {
|
|
55
|
-
provider: Provider;
|
|
56
|
-
/** Model id, must exist in the model table. */
|
|
57
|
-
model: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface RegistryEntry {
|
|
61
|
-
capability: Capability;
|
|
62
|
-
tier: Tier;
|
|
63
|
-
/** Model depends on how you connect. Web (free) can afford the best;
|
|
64
|
-
* API (paid) can pick a cheaper equivalent. */
|
|
65
|
-
byBackend: Record<Backend, BackendPick>;
|
|
66
|
-
/** On API, prompt the user which model to use for this role. */
|
|
67
|
-
ask?: boolean;
|
|
68
|
-
/** Why this pick — benchmark / source note. */
|
|
69
|
-
evidence?: string;
|
|
70
|
-
updated?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Task — what the PM emits, what the router reads.
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
export interface TokenEstimate {
|
|
78
|
-
input: number;
|
|
79
|
-
output: number;
|
|
80
|
-
/** Fraction of input served from cache (0..1). Cuts cost via cacheRead rate. */
|
|
81
|
-
cachedInputFraction?: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface Task {
|
|
85
|
-
id: string;
|
|
86
|
-
title: string;
|
|
87
|
-
capability: Capability;
|
|
88
|
-
difficulty: Difficulty;
|
|
89
|
-
estTokens: TokenEstimate;
|
|
90
|
-
epic?: string;
|
|
91
|
-
story?: string;
|
|
92
|
-
dependsOn?: string[];
|
|
93
|
-
/** Human-only annotation shown on the board. Never sent to any model. */
|
|
94
|
-
notes?: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// Policy — the user's knobs.
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
export type BackendMode = "ask" | "cost-first" | "api" | "web";
|
|
102
|
-
|
|
103
|
-
export interface RoutingPolicy {
|
|
104
|
-
backendMode: BackendMode;
|
|
105
|
-
/** Halt the run and ask the user if the cumulative bill would cross this. */
|
|
106
|
-
budgetCapUSD: number;
|
|
107
|
-
difficultyToTier: Record<Difficulty, Tier>;
|
|
108
|
-
/** Tester -> Developer feedback loop stop condition (used later, in the orchestrator). */
|
|
109
|
-
maxFeedbackRounds: number;
|
|
110
|
-
/** Per-task limits. A task that runs longer or spends more is aborted, recorded as
|
|
111
|
-
* failed, and the build halts (resumable). 0 = unlimited. */
|
|
112
|
-
taskLimits: TaskLimits;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface TaskLimits {
|
|
116
|
-
timeoutMs: number;
|
|
117
|
-
costCapUSD: number;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Thrown by an executor when a task breaches its limits. The orchestrator turns it
|
|
121
|
-
* into a failed outcome + halt instead of letting it escape as a crash. */
|
|
122
|
-
export class TaskLimitError extends Error {
|
|
123
|
-
constructor(
|
|
124
|
-
public readonly kind: "timeout" | "cost",
|
|
125
|
-
public readonly taskId: string,
|
|
126
|
-
/** Money already spent on the aborted attempt (must still be billed). */
|
|
127
|
-
public readonly costSoFar: number,
|
|
128
|
-
message: string,
|
|
129
|
-
) {
|
|
130
|
-
super(message);
|
|
131
|
-
this.name = "TaskLimitError";
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// Routing result.
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
export interface RouteDecision {
|
|
140
|
-
taskId: string;
|
|
141
|
-
backend: Backend;
|
|
142
|
-
provider: Provider;
|
|
143
|
-
model: Model;
|
|
144
|
-
tier: Tier;
|
|
145
|
-
/** Estimated USD for this task alone. */
|
|
146
|
-
cost: number;
|
|
147
|
-
/** Cumulative USD including this task. */
|
|
148
|
-
runningTotal: number;
|
|
149
|
-
/** True if runningTotal crossed the budget cap — caller should confirm with user. */
|
|
150
|
-
overCap: boolean;
|
|
151
|
-
/** Human-readable trail of how the decision was made. */
|
|
152
|
-
reasons: string[];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
// Orchestration (Phase 4).
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
|
|
159
|
-
export interface Bug {
|
|
160
|
-
severity: "low" | "medium" | "high";
|
|
161
|
-
description: string;
|
|
162
|
-
file?: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** A tester's structured judgement of a build. */
|
|
166
|
-
export interface Verdict {
|
|
167
|
-
passed: boolean;
|
|
168
|
-
bugs: Bug[];
|
|
169
|
-
/** True when the tester actually ran the app (headless Chromium). False = it could
|
|
170
|
-
* only read the code, so a PASS is weaker than it looks. */
|
|
171
|
-
runtimeChecked: boolean;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** What a role produces when it runs a task. */
|
|
175
|
-
export interface RoleResult {
|
|
176
|
-
/** The agent's final assistant text (design spec, notes, summary). */
|
|
177
|
-
finalText: string;
|
|
178
|
-
/** Files present in the workspace after the run (repo-relative). */
|
|
179
|
-
files: string[];
|
|
180
|
-
/** Pi's own measured dollar cost for this run. */
|
|
181
|
-
cost: number;
|
|
182
|
-
/** Only for test tasks — the pass/fail judgement. */
|
|
183
|
-
verdict?: Verdict;
|
|
184
|
-
/** Set when the task was aborted (limit breach). A failed outcome is billed but
|
|
185
|
-
* never counts as "done": resume rebuilds it. */
|
|
186
|
-
error?: string;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Executor injected into the orchestrator. Real impl runs Pi; tests pass a fake. */
|
|
190
|
-
export type RoleExecutor = (input: {
|
|
191
|
-
task: Task;
|
|
192
|
-
decision: RouteDecision;
|
|
193
|
-
/** Handoff text gathered from this task's dependencies. */
|
|
194
|
-
contextText: string;
|
|
195
|
-
/** 0 on first attempt, 1+ during a Tester→Developer feedback round. */
|
|
196
|
-
round: number;
|
|
197
|
-
/** Per-task limits the executor must enforce (throw TaskLimitError on breach). */
|
|
198
|
-
limits: TaskLimits;
|
|
199
|
-
}) => Promise<RoleResult>;
|
|
200
|
-
|
|
201
|
-
/** One recorded step of a build run. */
|
|
202
|
-
export interface TaskOutcome extends RoleResult {
|
|
203
|
-
taskId: string;
|
|
204
|
-
capability: Capability;
|
|
205
|
-
provider: Provider;
|
|
206
|
-
modelId: string;
|
|
207
|
-
round: number;
|
|
208
|
-
}
|