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.
Files changed (171) hide show
  1. package/README.md +7 -6
  2. package/bin/projectinator.mjs +22 -15
  3. package/dist/bakeoff.js +163 -0
  4. package/dist/bakeoff.js.map +1 -0
  5. package/dist/build-state.js +26 -0
  6. package/dist/build-state.js.map +1 -0
  7. package/dist/burndown.js +19 -0
  8. package/dist/burndown.js.map +1 -0
  9. package/dist/calibration.js +86 -0
  10. package/dist/calibration.js.map +1 -0
  11. package/dist/cli.js +289 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/cost.js +35 -0
  14. package/dist/cost.js.map +1 -0
  15. package/dist/council.js +147 -0
  16. package/dist/council.js.map +1 -0
  17. package/dist/demo.js +93 -0
  18. package/dist/demo.js.map +1 -0
  19. package/dist/estimate.js +98 -0
  20. package/dist/estimate.js.map +1 -0
  21. package/dist/executor.js +118 -0
  22. package/dist/executor.js.map +1 -0
  23. package/dist/git.js +82 -0
  24. package/dist/git.js.map +1 -0
  25. package/dist/intake.js +98 -0
  26. package/dist/intake.js.map +1 -0
  27. package/dist/models.js +171 -0
  28. package/dist/models.js.map +1 -0
  29. package/dist/narrate.js +76 -0
  30. package/dist/narrate.js.map +1 -0
  31. package/dist/openrouter.js +112 -0
  32. package/dist/openrouter.js.map +1 -0
  33. package/dist/orchestrator.js +273 -0
  34. package/dist/orchestrator.js.map +1 -0
  35. package/dist/pm.js +245 -0
  36. package/dist/pm.js.map +1 -0
  37. package/dist/preview.js +194 -0
  38. package/dist/preview.js.map +1 -0
  39. package/dist/registry-store.js +38 -0
  40. package/dist/registry-store.js.map +1 -0
  41. package/dist/registry.js +115 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/research.js +98 -0
  44. package/dist/research.js.map +1 -0
  45. package/dist/retro.js +83 -0
  46. package/dist/retro.js.map +1 -0
  47. package/dist/roles.js +357 -0
  48. package/dist/roles.js.map +1 -0
  49. package/dist/router.js +95 -0
  50. package/dist/router.js.map +1 -0
  51. package/dist/run-bakeoff.js +68 -0
  52. package/dist/run-bakeoff.js.map +1 -0
  53. package/dist/run-build.js +193 -0
  54. package/dist/run-build.js.map +1 -0
  55. package/{src/run-dev.ts → dist/run-dev.js} +23 -39
  56. package/dist/run-dev.js.map +1 -0
  57. package/{src/run-pm.ts → dist/run-pm.js} +34 -46
  58. package/dist/run-pm.js.map +1 -0
  59. package/{src/run-research.ts → dist/run-research.js} +19 -30
  60. package/dist/run-research.js.map +1 -0
  61. package/{src/run-scout.ts → dist/run-scout.js} +17 -27
  62. package/dist/run-scout.js.map +1 -0
  63. package/dist/run-web.js +93 -0
  64. package/dist/run-web.js.map +1 -0
  65. package/dist/scout.js +81 -0
  66. package/dist/scout.js.map +1 -0
  67. package/dist/session-cost.js +15 -0
  68. package/dist/session-cost.js.map +1 -0
  69. package/dist/stack.js +34 -0
  70. package/dist/stack.js.map +1 -0
  71. package/dist/stuck.js +21 -0
  72. package/dist/stuck.js.map +1 -0
  73. package/dist/tui/App.js +1269 -0
  74. package/dist/tui/App.js.map +1 -0
  75. package/dist/tui/BakeOff.js +95 -0
  76. package/dist/tui/BakeOff.js.map +1 -0
  77. package/dist/tui/BoardEditor.js +183 -0
  78. package/dist/tui/BoardEditor.js.map +1 -0
  79. package/dist/tui/EditableBoard.js +140 -0
  80. package/dist/tui/EditableBoard.js.map +1 -0
  81. package/dist/tui/Frame.js +78 -0
  82. package/dist/tui/Frame.js.map +1 -0
  83. package/dist/tui/Intake.js +59 -0
  84. package/dist/tui/Intake.js.map +1 -0
  85. package/dist/tui/Kanban.js +75 -0
  86. package/dist/tui/Kanban.js.map +1 -0
  87. package/dist/tui/Settings.js +299 -0
  88. package/dist/tui/Settings.js.map +1 -0
  89. package/dist/tui/StackPick.js +44 -0
  90. package/dist/tui/StackPick.js.map +1 -0
  91. package/dist/tui/WebAccounts.js +131 -0
  92. package/dist/tui/WebAccounts.js.map +1 -0
  93. package/dist/tui/components.js +178 -0
  94. package/dist/tui/components.js.map +1 -0
  95. package/dist/tui/config.js +129 -0
  96. package/dist/tui/config.js.map +1 -0
  97. package/dist/tui/deploy.js +114 -0
  98. package/dist/tui/deploy.js.map +1 -0
  99. package/dist/tui/engine.js +710 -0
  100. package/dist/tui/engine.js.map +1 -0
  101. package/dist/tui/notify.js +47 -0
  102. package/dist/tui/notify.js.map +1 -0
  103. package/dist/tui/panels.js +42 -0
  104. package/dist/tui/panels.js.map +1 -0
  105. package/dist/tui/templates.js +106 -0
  106. package/dist/tui/templates.js.map +1 -0
  107. package/dist/tui/theme.js +42 -0
  108. package/dist/tui/theme.js.map +1 -0
  109. package/dist/tui/validate.js +51 -0
  110. package/dist/tui/validate.js.map +1 -0
  111. package/{src/tui.tsx → dist/tui.js} +23 -33
  112. package/dist/tui.js.map +1 -0
  113. package/dist/types.js +19 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/web/oauth-anthropic.js +179 -0
  116. package/dist/web/oauth-anthropic.js.map +1 -0
  117. package/dist/web/session.js +274 -0
  118. package/dist/web/session.js.map +1 -0
  119. package/package.json +5 -4
  120. package/src/bakeoff.ts +0 -214
  121. package/src/build-state.ts +0 -47
  122. package/src/burndown.ts +0 -35
  123. package/src/calibration.ts +0 -88
  124. package/src/cli.ts +0 -263
  125. package/src/cost.ts +0 -43
  126. package/src/council.ts +0 -175
  127. package/src/demo.ts +0 -106
  128. package/src/estimate.ts +0 -111
  129. package/src/executor.ts +0 -169
  130. package/src/git.ts +0 -72
  131. package/src/intake.ts +0 -125
  132. package/src/models.ts +0 -175
  133. package/src/narrate.ts +0 -87
  134. package/src/openrouter.ts +0 -119
  135. package/src/orchestrator.ts +0 -310
  136. package/src/pm.ts +0 -302
  137. package/src/preview.ts +0 -206
  138. package/src/registry-store.ts +0 -41
  139. package/src/registry.ts +0 -132
  140. package/src/research.ts +0 -123
  141. package/src/retro.ts +0 -99
  142. package/src/roles.ts +0 -401
  143. package/src/router.ts +0 -120
  144. package/src/run-bakeoff.ts +0 -77
  145. package/src/run-build.ts +0 -203
  146. package/src/run-web.ts +0 -87
  147. package/src/scout.ts +0 -121
  148. package/src/session-cost.ts +0 -17
  149. package/src/stack.ts +0 -46
  150. package/src/tui/App.tsx +0 -1760
  151. package/src/tui/BakeOff.tsx +0 -190
  152. package/src/tui/BoardEditor.tsx +0 -260
  153. package/src/tui/EditableBoard.tsx +0 -179
  154. package/src/tui/Frame.tsx +0 -142
  155. package/src/tui/Intake.tsx +0 -111
  156. package/src/tui/Kanban.tsx +0 -158
  157. package/src/tui/Settings.tsx +0 -513
  158. package/src/tui/StackPick.tsx +0 -79
  159. package/src/tui/WebAccounts.tsx +0 -197
  160. package/src/tui/components.tsx +0 -340
  161. package/src/tui/config.ts +0 -150
  162. package/src/tui/deploy.ts +0 -137
  163. package/src/tui/engine.ts +0 -749
  164. package/src/tui/notify.ts +0 -21
  165. package/src/tui/panels.tsx +0 -89
  166. package/src/tui/templates.ts +0 -119
  167. package/src/tui/theme.ts +0 -44
  168. package/src/tui/validate.ts +0 -53
  169. package/src/types.ts +0 -208
  170. package/src/web/oauth-anthropic.ts +0 -206
  171. package/src/web/session.ts +0 -299
package/src/intake.ts DELETED
@@ -1,125 +0,0 @@
1
- // Intake — the PM's clarifying-questions step. Before planning a vague request,
2
- // the PM asks 2-4 short questions (each with suggested options) to pin down what
3
- // to build. A clear request returns no questions and skips straight to planning.
4
- //
5
- // Forced structured output, same discipline as pm.ts: one tool, permissive
6
- // schema, coerced/validated in code.
7
-
8
- import {
9
- createAgentSession,
10
- defineTool,
11
- } from "@earendil-works/pi-coding-agent";
12
- import { Type, type Static } from "typebox";
13
- import type { Backend, Provider } from "./types.js";
14
- import { findEntry } from "./registry.js";
15
- import { piRuntime, resolvePiModel } from "./executor.js"
16
- import { addSessionCost } from "./session-cost.js";
17
-
18
- const IntakeSchema = Type.Object(
19
- {
20
- needsClarification: Type.Boolean({ description: "true only if the request is too vague to plan well" }),
21
- questions: Type.Array(
22
- Type.Object(
23
- {
24
- question: Type.String({ description: "one short clarifying question" }),
25
- options: Type.Array(Type.String(), { description: "2-4 concrete pickable answers; may be empty for free text" }),
26
- multi: Type.Boolean({ description: "true if several options can apply at once" }),
27
- },
28
- { additionalProperties: true },
29
- ),
30
- { description: "empty when needsClarification is false; at most 4" },
31
- ),
32
- },
33
- { additionalProperties: true },
34
- );
35
- type IntakeRaw = Static<typeof IntakeSchema>;
36
-
37
- export interface IntakeQuestion {
38
- question: string;
39
- options: string[];
40
- multi: boolean;
41
- }
42
-
43
- function buildIntakeTool() {
44
- let captured: IntakeRaw | undefined;
45
- const tool = defineTool({
46
- name: "submit_intake",
47
- label: "Submit Intake",
48
- description: "Submit whether clarification is needed and, if so, the clarifying questions. Call exactly once.",
49
- parameters: IntakeSchema,
50
- execute: async (_id, params) => {
51
- captured = params as IntakeRaw;
52
- return { content: [{ type: "text", text: `Intake: ${params.needsClarification ? `${params.questions.length} questions` : "clear"}.` }], details: {} };
53
- },
54
- });
55
- return { tool, get: () => captured };
56
- }
57
-
58
- const SYSTEM = [
59
- "You are the PROJECT MANAGER doing intake for a build request.",
60
- "If the request already has enough detail to plan and build — its purpose and the",
61
- "must-have features are clear or reasonably inferable — set needsClarification=false",
62
- "and questions=[].",
63
- "If it is vague (a bare template like 'landing page', a one-liner missing the",
64
- "essentials), set needsClarification=true and produce 2-4 SHORT questions that most",
65
- "reduce ambiguity: what the product/business actually is, a name, must-have",
66
- "sections/features, and style. For each question give 2-4 concrete pickable options;",
67
- "set multi=true when several can apply (e.g. which sections). Never more than 4",
68
- "questions. Do not ask what you can reasonably assume. Call submit_intake exactly once.",
69
- ].join("\n");
70
-
71
- export interface AssessOptions {
72
- backend: Backend;
73
- modelOverride?: { provider: Provider; model: string };
74
- }
75
-
76
- /** Ask the PM whether the request needs clarification; returns up to 4 questions
77
- * (empty = clear enough to plan directly). Never throws — returns [] on trouble. */
78
- export async function assessIntake(idea: string, opts: AssessOptions): Promise<IntakeQuestion[]> {
79
- const runtime = await piRuntime();
80
- const { entry } = findEntry("plan", "mid");
81
- const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
82
-
83
- try {
84
- const model = resolvePiModel(runtime, pick.provider, pick.model);
85
- const { tool, get } = buildIntakeTool();
86
- const { session } = await createAgentSession({
87
- model,
88
- modelRuntime: runtime,
89
- thinkingLevel: "low",
90
- noTools: "all",
91
- customTools: [tool],
92
- tools: ["submit_intake"],
93
- });
94
- try {
95
- await session.prompt(`${SYSTEM}\n\n--- REQUEST ---\n${idea}`);
96
- let raw = get();
97
- if (!raw) {
98
- await session.prompt("Call submit_intake now.");
99
- raw = get();
100
- }
101
- addSessionCost(session.getSessionStats().cost);
102
- if (!raw || !raw.needsClarification) return [];
103
- return (raw.questions ?? [])
104
- .slice(0, 4)
105
- .map((q) => ({
106
- question: String(q.question ?? "").trim(),
107
- options: (q.options ?? []).map((o) => String(o).trim()).filter(Boolean).slice(0, 6),
108
- multi: !!q.multi,
109
- }))
110
- .filter((q) => q.question);
111
- } finally {
112
- session.dispose();
113
- }
114
- } catch {
115
- return []; // never block a build on intake — fall through to planning
116
- }
117
- }
118
-
119
- /** Fold the collected answers into the brief the PM will plan from. */
120
- export function enrichBrief(idea: string, answers: { question: string; answer: string }[]): string {
121
- const kept = answers.filter((a) => a.answer.trim());
122
- if (!kept.length) return idea;
123
- const lines = kept.map((a) => `- ${a.question} ${a.answer.trim()}`);
124
- return `${idea}\n\nClarifications from the requester:\n${lines.join("\n")}`;
125
- }
package/src/models.ts DELETED
@@ -1,175 +0,0 @@
1
- // Model pricing table. Rates USD per 1,000,000 tokens — copied verbatim from Pi's
2
- // bundled catalog (pi-coding-agent 0.85.1, Sept 2026); test/executor.test.ts pins every
3
- // entry against it. Shape mirrors Pi's models.json `cost` block.
4
-
5
- import type { Model } from "./types.js";
6
- import { findOpenRouterModel } from "./openrouter.js";
7
-
8
- export const MODELS: Record<string, Model> = {
9
- // ---- OpenAI: GPT-5.6 family (prices cut Sept 2026; past 272k input costs 2x) ----
10
- "gpt-5.6-sol": {
11
- id: "gpt-5.6-sol",
12
- provider: "openai",
13
- name: "GPT-5.6 Sol",
14
- contextWindow: 272_000,
15
- cost: { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5, tiers: [{ inputTokensAbove: 272_000, input: 8, output: 30, cacheRead: 0.8, cacheWrite: 10 }] },
16
- },
17
- "gpt-5.6-terra": {
18
- id: "gpt-5.6-terra",
19
- provider: "openai",
20
- name: "GPT-5.6 Terra",
21
- contextWindow: 272_000,
22
- cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5, tiers: [{ inputTokensAbove: 272_000, input: 4, output: 18, cacheRead: 0.4, cacheWrite: 5 }] },
23
- },
24
- "gpt-5.6-luna": {
25
- id: "gpt-5.6-luna",
26
- provider: "openai",
27
- name: "GPT-5.6 Luna",
28
- contextWindow: 272_000,
29
- cost: { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25, tiers: [{ inputTokensAbove: 272_000, input: 0.4, output: 1.8, cacheRead: 0.04, cacheWrite: 0.5 }] },
30
- },
31
-
32
- // ---- Anthropic: Claude ----
33
- "claude-fable-5-1": {
34
- id: "claude-fable-5-1",
35
- provider: "anthropic",
36
- name: "Claude Fable 5.1",
37
- contextWindow: 1_000_000,
38
- cost: { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
39
- },
40
- "claude-fable-5": {
41
- id: "claude-fable-5",
42
- provider: "anthropic",
43
- name: "Claude Fable 5",
44
- contextWindow: 1_000_000,
45
- cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
46
- },
47
- "claude-opus-5": {
48
- id: "claude-opus-5",
49
- provider: "anthropic",
50
- name: "Claude Opus 5",
51
- contextWindow: 1_000_000,
52
- cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
53
- },
54
- "claude-opus-4-8": {
55
- id: "claude-opus-4-8",
56
- provider: "anthropic",
57
- name: "Claude Opus 4.8",
58
- contextWindow: 1_000_000,
59
- cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
60
- },
61
- "claude-sonnet-5": {
62
- id: "claude-sonnet-5",
63
- provider: "anthropic",
64
- name: "Claude Sonnet 5",
65
- contextWindow: 1_000_000,
66
- cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
67
- },
68
- "claude-sonnet-4-6": {
69
- id: "claude-sonnet-4-6",
70
- provider: "anthropic",
71
- name: "Claude Sonnet 4.6",
72
- contextWindow: 1_000_000,
73
- cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
74
- },
75
- "claude-haiku-4-5": {
76
- id: "claude-haiku-4-5",
77
- provider: "anthropic",
78
- name: "Claude Haiku 4.5",
79
- contextWindow: 200_000,
80
- cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
81
- },
82
-
83
- // ---- Google: Gemini ----
84
- // NOTE: ids match Pi's built-in registry exactly (provider "google"), so they
85
- // resolve directly via ModelRegistry.find() with no alias layer.
86
- "gemini-3.1-pro-preview": {
87
- id: "gemini-3.1-pro-preview",
88
- provider: "google",
89
- name: "Gemini 3.1 Pro",
90
- contextWindow: 1_000_000,
91
- cost: {
92
- input: 2,
93
- output: 12,
94
- cacheRead: 0.2,
95
- cacheWrite: 2.5,
96
- // Google charges more past 200k input tokens.
97
- tiers: [{ inputTokensAbove: 200_000, input: 4, output: 18, cacheRead: 0.4, cacheWrite: 5 }],
98
- },
99
- },
100
- "gemini-3.8-flash": {
101
- id: "gemini-3.8-flash",
102
- provider: "google",
103
- name: "Gemini 3.8 Flash",
104
- contextWindow: 1_000_000,
105
- cost: { input: 0.75, output: 3.75, cacheRead: 0.075 },
106
- },
107
- "gemini-3-flash-preview": {
108
- id: "gemini-3-flash-preview",
109
- provider: "google",
110
- name: "Gemini 3 Flash",
111
- contextWindow: 1_000_000,
112
- cost: { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.625 },
113
- },
114
-
115
- // ---- OpenRouter (one key → frontier models). ids are Pi's OpenRouter-catalog
116
- // slugs (vendor/model). Pricing mirrors the underlying model (OpenRouter passes
117
- // it through, ~small margin); ACTUAL cost still comes from Pi per run.
118
- "anthropic/claude-opus-5": {
119
- id: "anthropic/claude-opus-5",
120
- provider: "openrouter",
121
- name: "Claude Opus 5 (OpenRouter)",
122
- contextWindow: 1_000_000,
123
- cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
124
- },
125
- "anthropic/claude-opus-4.8": {
126
- id: "anthropic/claude-opus-4.8",
127
- provider: "openrouter",
128
- name: "Claude Opus 4.8 (OpenRouter)",
129
- contextWindow: 1_000_000,
130
- cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
131
- },
132
- "anthropic/claude-sonnet-5": {
133
- id: "anthropic/claude-sonnet-5",
134
- provider: "openrouter",
135
- name: "Claude Sonnet 5 (OpenRouter)",
136
- contextWindow: 1_000_000,
137
- cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
138
- },
139
- "anthropic/claude-sonnet-4.6": {
140
- id: "anthropic/claude-sonnet-4.6",
141
- provider: "openrouter",
142
- name: "Claude Sonnet 4.6 (OpenRouter)",
143
- contextWindow: 1_000_000,
144
- cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
145
- },
146
- "google/gemini-3.8-flash": {
147
- id: "google/gemini-3.8-flash",
148
- provider: "openrouter",
149
- name: "Gemini 3.8 Flash (OpenRouter)",
150
- contextWindow: 1_000_000,
151
- cost: { input: 0.75, output: 3.75, cacheRead: 0.075 },
152
- },
153
- "openai/gpt-5.6-luna": {
154
- id: "openai/gpt-5.6-luna",
155
- provider: "openrouter",
156
- name: "GPT-5.6 Luna (OpenRouter)",
157
- contextWindow: 1_050_000,
158
- cost: { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
159
- },
160
- };
161
-
162
- export function getModel(id: string): Model {
163
- const m = MODELS[id];
164
- if (m) return m;
165
- // OpenRouter slugs (vendor/model) aren't in the static table — price them from
166
- // the OpenRouter catalog (live cache or Pi's built-in list). Fall back to a
167
- // rough estimate so a build never crashes on an unpriced model (actual cost
168
- // still comes from Pi per run).
169
- if (id.includes("/")) {
170
- const or = findOpenRouterModel(id);
171
- if (or) return { ...or, provider: "openrouter" };
172
- return { id, provider: "openrouter", name: id, contextWindow: 200_000, cost: { input: 1, output: 3, cacheRead: 0.1, cacheWrite: 1.25 } };
173
- }
174
- throw new Error(`Unknown model id: "${id}". Add it to src/models.ts.`);
175
- }
package/src/narrate.ts DELETED
@@ -1,87 +0,0 @@
1
- // AI narrative for a build retro. Feeds the data-driven RetroReport to a model
2
- // and gets back a short "what went well / what to improve" write-up. On-demand
3
- // (costs a small call) and cached on the build state by the caller.
4
-
5
- import {
6
- createAgentSession,
7
- type AgentSession,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { Backend, Provider } from "./types.js";
10
- import type { RetroReport } from "./retro.js";
11
- import { findEntry } from "./registry.js";
12
- import { piRuntime, resolvePiModel } from "./executor.js"
13
- import { addSessionCost } from "./session-cost.js";
14
-
15
- function lastAssistantText(session: AgentSession): string {
16
- const msgs = session.messages as Array<{ role?: string; content?: unknown }>;
17
- for (let i = msgs.length - 1; i >= 0; i--) {
18
- const m = msgs[i];
19
- if (m?.role !== "assistant") continue;
20
- const c = m.content;
21
- if (typeof c === "string") return c;
22
- if (Array.isArray(c)) {
23
- return c
24
- .map((p: unknown) => (typeof p === "string" ? p : p && typeof p === "object" && "text" in p ? String((p as { text: unknown }).text) : ""))
25
- .join("")
26
- .trim();
27
- }
28
- }
29
- return "";
30
- }
31
-
32
- /** Compact fact sheet the model reasons over — numbers, not prose. */
33
- function facts(r: RetroReport): string {
34
- const lines = [
35
- `Idea: ${r.idea}`,
36
- `Status: ${r.status}. Tasks done: ${r.doneCount}/${r.taskCount}.`,
37
- `Cost: predicted $${r.estCost.toFixed(2)}, actual $${r.totalCost.toFixed(2)}.`,
38
- `Tests: ${r.tests.passed} passed, ${r.tests.failed} failed.`,
39
- r.retries.length ? `Rebuilds: ${r.retries.map((x) => `${x.taskId}×${x.rounds}`).join(", ")}.` : "Rebuilds: none.",
40
- `Cost by epic: ${r.byEpic.map((e) => `${e.epic} $${e.cost}`).join(", ") || "n/a"}.`,
41
- `Cost by model: ${r.byModel.map((m) => `${m.model} $${m.cost} (${m.tasks})`).join(", ") || "n/a"}.`,
42
- `Priciest tasks: ${r.topCost.map((t) => `${t.taskId} $${t.cost}`).join(", ") || "n/a"}.`,
43
- r.bugs.length
44
- ? `Tester flags: ${r.bugs.map((b) => `[${b.severity}] ${b.description}`).slice(0, 6).join("; ")}.`
45
- : "Tester flags: none.",
46
- ];
47
- return lines.join("\n");
48
- }
49
-
50
- const PROMPT = [
51
- "You are a pragmatic engineering lead writing a SHORT retro for this build.",
52
- "Use the facts below — reference the actual numbers. Do not invent anything.",
53
- "Write exactly three sections, terse bullets, under 140 words total:",
54
- "**What went well** (2-3 bullets)",
55
- "**What to improve** (2-3 bullets)",
56
- "**Next time** (one line)",
57
- "No preamble, no closing remarks.",
58
- ].join("\n");
59
-
60
- export interface NarrateOptions {
61
- backend: Backend;
62
- modelOverride?: { provider: Provider; model: string };
63
- }
64
-
65
- /** Generate the narrative. Throws on failure (caller shows the error). */
66
- export async function narrateRetro(report: RetroReport, opts: NarrateOptions): Promise<string> {
67
- const runtime = await piRuntime();
68
- const { entry } = findEntry("plan", "mid");
69
- const pick = opts.modelOverride ?? entry.byBackend[opts.backend];
70
- const model = resolvePiModel(runtime, pick.provider, pick.model);
71
-
72
- const { session } = await createAgentSession({
73
- model,
74
- modelRuntime: runtime,
75
- thinkingLevel: "low",
76
- noTools: "all",
77
- });
78
- try {
79
- await session.prompt(`${PROMPT}\n\n--- FACTS ---\n${facts(report)}`);
80
- const text = lastAssistantText(session);
81
- if (!text) throw new Error("The model returned no narrative.");
82
- return text;
83
- } finally {
84
- addSessionCost(session.getSessionStats().cost);
85
- session.dispose();
86
- }
87
- }
package/src/openrouter.ts DELETED
@@ -1,119 +0,0 @@
1
- // The OpenRouter model catalog — so users can pick ANY OpenRouter model by name
2
- // (Kimi, DeepSeek, Qwen, …) instead of typing slugs.
3
- //
4
- // - refreshOpenRouterModels(): live-fetch openrouter.ai/api/v1/models (freshest
5
- // names + pricing), cache to disk. Falls back to Pi's built-in OR catalog.
6
- // - openRouterModels() / findOpenRouterModel(): SYNC reads (disk cache, else Pi's
7
- // built-in list) so cost estimation (getModel) can price any picked model.
8
-
9
- import { piRuntime } from "./executor.js";
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
- import type { Model, ModelCost } from "./types.js";
14
-
15
- /** A pickable OpenRouter model: our Model shape minus the provider (always "openrouter"). */
16
- export type ORModel = Omit<Model, "provider">;
17
-
18
- const CACHE = join(homedir(), ".projectinator", "openrouter-models.json");
19
-
20
- let builtinMemo: ORModel[] | null = null;
21
- let diskMemo: ORModel[] | null | undefined; // undefined = not read yet, null = no cache
22
-
23
- /** Pi's built-in OpenRouter catalog — offline, names + pricing. Sync read of the memo;
24
- * call warmBuiltinOpenRouterModels() once at startup to fill it (Pi's runtime is async). */
25
- export function builtinOpenRouterModels(): ORModel[] {
26
- return builtinMemo ?? [];
27
- }
28
-
29
- /** Load Pi's built-in OpenRouter list into the memo. Safe to call repeatedly. */
30
- export async function warmBuiltinOpenRouterModels(): Promise<ORModel[]> {
31
- if (builtinMemo) return builtinMemo;
32
- try {
33
- const runtime = await piRuntime();
34
- builtinMemo = runtime
35
- .getModels("openrouter")
36
- .filter((m) => !!m.cost)
37
- .map((m) => ({
38
- id: m.id,
39
- name: m.name ?? m.id,
40
- contextWindow: m.contextWindow ?? 200_000,
41
- cost: m.cost,
42
- }))
43
- .sort((a, b) => a.name.localeCompare(b.name));
44
- } catch {
45
- builtinMemo = [];
46
- }
47
- return builtinMemo;
48
- }
49
-
50
- function readDiskCache(): ORModel[] | null {
51
- if (diskMemo !== undefined) return diskMemo;
52
- try {
53
- if (existsSync(CACHE)) {
54
- const parsed = JSON.parse(readFileSync(CACHE, "utf8")) as { models?: ORModel[] };
55
- diskMemo = Array.isArray(parsed.models) && parsed.models.length ? parsed.models : null;
56
- } else diskMemo = null;
57
- } catch {
58
- diskMemo = null;
59
- }
60
- return diskMemo;
61
- }
62
-
63
- /** The list to show / price against: live-fetched disk cache if present, else Pi's built-in. */
64
- export function openRouterModels(): ORModel[] {
65
- const cached = readDiskCache();
66
- return cached && cached.length ? cached : builtinOpenRouterModels();
67
- }
68
-
69
- /** Look up one model's pricing/name by slug (cache first, then built-in). Sync. */
70
- export function findOpenRouterModel(id: string): ORModel | undefined {
71
- return openRouterModels().find((m) => m.id === id) ?? builtinOpenRouterModels().find((m) => m.id === id);
72
- }
73
-
74
- /** Map OpenRouter's API pricing (USD per token, strings) to our per-1M-token cost. */
75
- function mapApiModel(m: {
76
- id: string;
77
- name?: string;
78
- context_length?: number;
79
- pricing?: { prompt?: string; completion?: string; input_cache_read?: string; input_cache_write?: string };
80
- }): ORModel | null {
81
- const p = m.pricing ?? {};
82
- const input = Number(p.prompt) * 1e6;
83
- const output = Number(p.completion) * 1e6;
84
- if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
85
- const cacheRead = p.input_cache_read != null ? Number(p.input_cache_read) * 1e6 : input * 0.1;
86
- const cacheWrite = p.input_cache_write != null ? Number(p.input_cache_write) * 1e6 : input * 1.25;
87
- return {
88
- id: m.id,
89
- name: m.name ?? m.id,
90
- contextWindow: m.context_length ?? 200_000,
91
- cost: { input, output, cacheRead, cacheWrite },
92
- };
93
- }
94
-
95
- /** Live-fetch the catalog and cache it. Falls back to the current list on any failure. */
96
- export async function refreshOpenRouterModels(timeoutMs = 8000): Promise<ORModel[]> {
97
- const ctrl = new AbortController();
98
- const timer = setTimeout(() => ctrl.abort(), timeoutMs);
99
- try {
100
- const res = await fetch("https://openrouter.ai/api/v1/models", { signal: ctrl.signal });
101
- if (!res.ok) return openRouterModels();
102
- const json = (await res.json()) as { data?: Array<Parameters<typeof mapApiModel>[0]> };
103
- const list = (json.data ?? []).map(mapApiModel).filter((m): m is ORModel => m !== null)
104
- .sort((a, b) => a.name.localeCompare(b.name));
105
- if (list.length) {
106
- try {
107
- mkdirSync(join(homedir(), ".projectinator"), { recursive: true });
108
- writeFileSync(CACHE, JSON.stringify({ fetchedAt: Date.now(), models: list }));
109
- diskMemo = list;
110
- } catch { /* cache write is best-effort */ }
111
- return list;
112
- }
113
- return openRouterModels();
114
- } catch {
115
- return openRouterModels();
116
- } finally {
117
- clearTimeout(timer);
118
- }
119
- }