praana 0.5.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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
@@ -0,0 +1,4 @@
1
+ import type { ExtractedLearning, SessionEvent, SummarizerLLM } from "./types.js";
2
+ export declare function extractLearnings(llm: SummarizerLLM, events: SessionEvent[]): Promise<ExtractedLearning[]>;
3
+ /** Summarize a batch of old turns into episodic memories for history compression. */
4
+ export declare function summarizeTurns(llm: SummarizerLLM, events: SessionEvent[]): Promise<ExtractedLearning[]>;
@@ -0,0 +1,112 @@
1
+ // ============================================================
2
+ // ARIA Memory — Summarizer
3
+ //
4
+ // One LLM call per session end: "what did we learn?"
5
+ // No extract→consolidate→gate pipeline. Just ask.
6
+ // ============================================================
7
+ import { isMemoryKind } from "./types.js";
8
+ const SYSTEM_PROMPT = `You are a memory extractor for a coding agent.
9
+ Given a session transcript, extract 0-5 concise learnings.
10
+ Output ONLY a JSON array. No prose outside the array.
11
+
12
+ Each entry: { "kind": "fact" | "preference" | "decision" | "pattern" | "mistake" | "constraint", "content": "...", "certainty": "high" | "medium" | "low" }
13
+
14
+ Rules:
15
+ - "fact": verifiable project knowledge ("uses Vitest for testing")
16
+ - "preference": user or agent preference ("prefers dark mode UI")
17
+ - "decision": architectural choice ("chose JWT over session cookies")
18
+ - "pattern": recurring approach ("validates with Zod before DB writes")
19
+ - "mistake": failure + lesson learned ("forgot await on verify() → 401s")
20
+ - "constraint": hard rule ("never commits .env files")
21
+ - Be conservative. Skip vague or low-signal items.
22
+ - Content should be one sentence, max 120 characters.
23
+ - certainty reflects how strongly the transcript supports this.`;
24
+ function transcriptToPrompt(events) {
25
+ const lines = ["Session transcript:"];
26
+ for (const e of events) {
27
+ if (e.type === "user_message")
28
+ lines.push(`User: ${e.content}`);
29
+ else if (e.type === "agent_message")
30
+ lines.push(`Agent: ${e.content}`);
31
+ else if (e.type === "tool_use")
32
+ lines.push(`[tool] ${e.tool_name}(${JSON.stringify(e.args ?? {})})`);
33
+ else if (e.type === "tool_result")
34
+ lines.push(`[result] ${JSON.stringify(e.result).slice(0, 200)}`);
35
+ }
36
+ return lines.join("\n");
37
+ }
38
+ export async function extractLearnings(llm, events) {
39
+ if (events.length === 0)
40
+ return [];
41
+ if (!(await llm.available()))
42
+ return [];
43
+ const raw = await llm.complete({
44
+ system: SYSTEM_PROMPT,
45
+ prompt: transcriptToPrompt(events),
46
+ temperature: 0.3,
47
+ maxTokens: 1500,
48
+ json: true,
49
+ timeoutMs: 30_000,
50
+ });
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (!Array.isArray(parsed))
54
+ return [];
55
+ return parsed
56
+ .filter((p) => isMemoryKind(p.kind))
57
+ .map((p) => ({
58
+ kind: p.kind,
59
+ content: p.content.slice(0, 200),
60
+ certainty: (p.certainty === "high" || p.certainty === "medium" || p.certainty === "low")
61
+ ? p.certainty
62
+ : "medium",
63
+ scope_hints: p.scope_hints,
64
+ }));
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ const TURN_COMPRESSION_PROMPT = `You are a memory compressor for a coding agent.
71
+ Given a batch of old conversation turns, extract 1-5 concise episodic facts.
72
+ Output ONLY a JSON array. No prose outside the array.
73
+
74
+ Each entry: { "kind": "fact" | "pattern" | "decision", "content": "...", "certainty": "high" | "medium" | "low" }
75
+
76
+ Rules:
77
+ - Focus on verifiable facts, patterns, and decisions — not activity logs.
78
+ - Content should be one sentence, max 120 characters.
79
+ - Be conservative. Skip vague or low-signal items.
80
+ - These memories will replace the raw turns in the agent's context.`;
81
+ /** Summarize a batch of old turns into episodic memories for history compression. */
82
+ export async function summarizeTurns(llm, events) {
83
+ if (events.length === 0)
84
+ return [];
85
+ if (!(await llm.available()))
86
+ return [];
87
+ const raw = await llm.complete({
88
+ system: TURN_COMPRESSION_PROMPT,
89
+ prompt: transcriptToPrompt(events),
90
+ temperature: 0.3,
91
+ maxTokens: 1000,
92
+ json: true,
93
+ timeoutMs: 30_000,
94
+ });
95
+ try {
96
+ const parsed = JSON.parse(raw);
97
+ if (!Array.isArray(parsed))
98
+ return [];
99
+ return parsed
100
+ .filter((p) => isMemoryKind(p.kind))
101
+ .map((p) => ({
102
+ kind: p.kind,
103
+ content: p.content.slice(0, 200),
104
+ certainty: (p.certainty === "high" || p.certainty === "medium" || p.certainty === "low")
105
+ ? p.certainty
106
+ : "medium",
107
+ }));
108
+ }
109
+ catch {
110
+ return [];
111
+ }
112
+ }
@@ -0,0 +1,87 @@
1
+ export declare const MEMORY_KINDS: readonly ["fact", "preference", "decision", "pattern", "mistake", "constraint"];
2
+ export type MemoryKind = "fact" | "preference" | "decision" | "pattern" | "mistake" | "constraint";
3
+ export declare function isMemoryKind(value: string): value is MemoryKind;
4
+ export type Certainty = "high" | "medium" | "low";
5
+ export type MemoryLayer = 1 | 2;
6
+ export interface MemoryEntry {
7
+ id: string;
8
+ kind: MemoryKind;
9
+ content: string;
10
+ confidence: number;
11
+ pinned: boolean;
12
+ layer: MemoryLayer;
13
+ confirmation_count: number;
14
+ created_at: number;
15
+ last_seen_at: number;
16
+ session_id: string;
17
+ scopes: string[];
18
+ retracted: boolean;
19
+ embedding?: Buffer;
20
+ }
21
+ export interface SessionContext {
22
+ agent: string;
23
+ user_id: string;
24
+ time: number;
25
+ context_id: string;
26
+ context_label: string;
27
+ working_context?: Record<string, unknown>;
28
+ recall_min_score?: number;
29
+ }
30
+ export interface Digest {
31
+ markdown: string;
32
+ empty: boolean;
33
+ entriesIncluded: string[];
34
+ }
35
+ export interface RecallOptions {
36
+ limit?: number;
37
+ scope?: string[];
38
+ mode?: "standard" | "causal_chain";
39
+ kinds?: MemoryKind[];
40
+ }
41
+ export interface RecallResult {
42
+ entries: Array<{
43
+ id: string;
44
+ kind: MemoryKind;
45
+ content: string;
46
+ confidence: number;
47
+ match: number;
48
+ scopes: string[];
49
+ score: number;
50
+ }>;
51
+ }
52
+ export interface RememberOptions {
53
+ kind?: MemoryKind;
54
+ certainty?: Certainty;
55
+ pinned?: boolean;
56
+ scope?: string[];
57
+ }
58
+ export interface Embedder {
59
+ dim: number;
60
+ embed(text: string): Promise<Float32Array>;
61
+ }
62
+ export interface SummarizerLLM {
63
+ name: string;
64
+ available(): Promise<boolean>;
65
+ complete(opts: {
66
+ system?: string;
67
+ prompt: string;
68
+ temperature?: number;
69
+ maxTokens?: number;
70
+ json?: boolean;
71
+ timeoutMs?: number;
72
+ }): Promise<string>;
73
+ }
74
+ export interface SessionEvent {
75
+ type: "user_message" | "agent_message" | "tool_use" | "tool_result";
76
+ timestamp: number;
77
+ content?: string;
78
+ tool_name?: string;
79
+ args?: Record<string, unknown>;
80
+ result?: unknown;
81
+ }
82
+ export interface ExtractedLearning {
83
+ kind: MemoryKind;
84
+ content: string;
85
+ certainty: Certainty;
86
+ scope_hints?: string[];
87
+ }
@@ -0,0 +1,17 @@
1
+ // ============================================================
2
+ // ARIA Memory — Core Types
3
+ //
4
+ // A simplified, purpose-built cross-session memory layer.
5
+ // Replaces bodha dependency with ~600 lines of focused code.
6
+ // ============================================================
7
+ export const MEMORY_KINDS = [
8
+ "fact",
9
+ "preference",
10
+ "decision",
11
+ "pattern",
12
+ "mistake",
13
+ "constraint",
14
+ ];
15
+ export function isMemoryKind(value) {
16
+ return MEMORY_KINDS.includes(value);
17
+ }
@@ -0,0 +1,15 @@
1
+ export declare const DEFAULT_MODEL_CONTEXT_WINDOW = 128000;
2
+ export declare function mapAriaProviderToPiAi(provider: string): string | null;
3
+ export declare function lookupPiAiContextWindow(provider: string, modelId: string): number | null;
4
+ /**
5
+ * Synchronous best-effort resolution: override → cache → pi-ai catalog → default.
6
+ */
7
+ export declare function resolveContextWindowSync(provider: string, modelId: string, override?: number): number;
8
+ /**
9
+ * Fetch and cache context window for a model. Never throws — falls back to sync resolution.
10
+ */
11
+ export declare function fetchAndCacheContextWindow(provider: string, modelId: string, override?: number): Promise<number>;
12
+ /** @deprecated Use resolveContextWindowSync with provider, or session.getContextWindowTokens(). */
13
+ export declare function resolveContextWindow(modelId: string, override?: number): number;
14
+ /** Test helper — reset in-memory/disk cache state. */
15
+ export declare function resetModelContextCacheForTests(): void;
@@ -0,0 +1,212 @@
1
+ import { getModel, getProviders } from "@earendil-works/pi-ai";
2
+ import { dirname } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { resolveAppHomePath } from "./app-identity.js";
5
+ export const DEFAULT_MODEL_CONTEXT_WINDOW = 128_000;
6
+ const CACHE_VERSION = 1;
7
+ const OPENROUTER_CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
8
+ const CACHE_FILE = resolveAppHomePath("model-context-cache.json");
9
+ /** PRAANA config provider → pi-ai MODELS registry key (when available). */
10
+ const PI_AI_PROVIDER_MAP = {
11
+ openrouter: "openrouter",
12
+ openai: "openai",
13
+ anthropic: "anthropic",
14
+ google: "google",
15
+ deepseek: "deepseek",
16
+ groq: "groq",
17
+ xai: "xai",
18
+ fireworks: "fireworks",
19
+ together: "together",
20
+ mistral: "mistral",
21
+ "amazon-bedrock": "amazon-bedrock",
22
+ };
23
+ const memoryEntries = new Map();
24
+ let diskCache = null;
25
+ let openRouterFetchPromise = null;
26
+ function cacheKey(provider, modelId) {
27
+ return `${provider}:${modelId}`;
28
+ }
29
+ function isValidWindow(value) {
30
+ return typeof value === "number" && Number.isFinite(value) && value >= 1000;
31
+ }
32
+ function applyOverride(override) {
33
+ if (override !== undefined && isValidWindow(override))
34
+ return override;
35
+ return null;
36
+ }
37
+ function loadDiskCache() {
38
+ if (diskCache)
39
+ return diskCache;
40
+ diskCache = { version: CACHE_VERSION, entries: {} };
41
+ if (!existsSync(CACHE_FILE))
42
+ return diskCache;
43
+ try {
44
+ const raw = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
45
+ if (raw.version === CACHE_VERSION && raw.entries && typeof raw.entries === "object") {
46
+ diskCache = raw;
47
+ }
48
+ }
49
+ catch {
50
+ diskCache = { version: CACHE_VERSION, entries: {} };
51
+ }
52
+ return diskCache;
53
+ }
54
+ function persistDiskCache() {
55
+ const dir = dirname(CACHE_FILE);
56
+ if (!existsSync(dir))
57
+ mkdirSync(dir, { recursive: true });
58
+ writeFileSync(CACHE_FILE, JSON.stringify(loadDiskCache(), null, 2), "utf-8");
59
+ }
60
+ function rememberContextWindow(provider, modelId, contextWindow) {
61
+ const key = cacheKey(provider, modelId);
62
+ memoryEntries.set(key, contextWindow);
63
+ const file = loadDiskCache();
64
+ file.entries[key] = { contextWindow, fetchedAt: Date.now() };
65
+ persistDiskCache();
66
+ }
67
+ export function mapAriaProviderToPiAi(provider) {
68
+ const mapped = PI_AI_PROVIDER_MAP[provider];
69
+ if (mapped)
70
+ return mapped;
71
+ const known = getProviders();
72
+ return known.includes(provider) ? provider : null;
73
+ }
74
+ export function lookupPiAiContextWindow(provider, modelId) {
75
+ const piProvider = mapAriaProviderToPiAi(provider);
76
+ if (!piProvider)
77
+ return null;
78
+ const model = getModel(piProvider, modelId);
79
+ return isValidWindow(model?.contextWindow) ? model.contextWindow : null;
80
+ }
81
+ function readCachedContextWindow(provider, modelId) {
82
+ const key = cacheKey(provider, modelId);
83
+ const mem = memoryEntries.get(key);
84
+ if (isValidWindow(mem))
85
+ return mem;
86
+ const entry = loadDiskCache().entries[key];
87
+ if (entry && isValidWindow(entry.contextWindow)) {
88
+ memoryEntries.set(key, entry.contextWindow);
89
+ return entry.contextWindow;
90
+ }
91
+ return null;
92
+ }
93
+ function readOpenRouterCatalogEntry(modelId) {
94
+ const catalog = loadDiskCache().openRouterCatalog;
95
+ if (!catalog)
96
+ return null;
97
+ if (Date.now() - catalog.fetchedAt > OPENROUTER_CATALOG_TTL_MS)
98
+ return null;
99
+ const value = catalog.models[modelId];
100
+ return isValidWindow(value) ? value : null;
101
+ }
102
+ async function fetchOpenRouterCatalog() {
103
+ const file = loadDiskCache();
104
+ const existing = file.openRouterCatalog;
105
+ if (existing &&
106
+ Date.now() - existing.fetchedAt <= OPENROUTER_CATALOG_TTL_MS &&
107
+ Object.keys(existing.models).length > 0) {
108
+ return existing.models;
109
+ }
110
+ if (openRouterFetchPromise)
111
+ return openRouterFetchPromise;
112
+ openRouterFetchPromise = (async () => {
113
+ const headers = { Accept: "application/json" };
114
+ const apiKey = process.env.OPENROUTER_API_KEY;
115
+ if (apiKey)
116
+ headers.Authorization = `Bearer ${apiKey}`;
117
+ const response = await fetch("https://openrouter.ai/api/v1/models", { headers });
118
+ if (!response.ok) {
119
+ throw new Error(`OpenRouter models API returned ${response.status}`);
120
+ }
121
+ const body = (await response.json());
122
+ const models = {};
123
+ for (const item of body.data ?? []) {
124
+ if (item.id && isValidWindow(item.context_length)) {
125
+ models[item.id] = item.context_length;
126
+ }
127
+ }
128
+ file.openRouterCatalog = { fetchedAt: Date.now(), models };
129
+ persistDiskCache();
130
+ return models;
131
+ })().finally(() => {
132
+ openRouterFetchPromise = null;
133
+ });
134
+ return openRouterFetchPromise;
135
+ }
136
+ async function lookupOpenRouterContextWindow(modelId) {
137
+ const cached = readOpenRouterCatalogEntry(modelId);
138
+ if (cached !== null)
139
+ return cached;
140
+ try {
141
+ const catalog = await fetchOpenRouterCatalog();
142
+ const value = catalog[modelId];
143
+ return isValidWindow(value) ? value : null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Synchronous best-effort resolution: override → cache → pi-ai catalog → default.
151
+ */
152
+ export function resolveContextWindowSync(provider, modelId, override) {
153
+ const fromOverride = applyOverride(override);
154
+ if (fromOverride !== null)
155
+ return fromOverride;
156
+ const cached = readCachedContextWindow(provider, modelId);
157
+ if (cached !== null)
158
+ return cached;
159
+ const fromCatalog = readOpenRouterCatalogEntry(modelId);
160
+ if (fromCatalog !== null)
161
+ return fromCatalog;
162
+ const fromPiAi = lookupPiAiContextWindow(provider, modelId);
163
+ if (fromPiAi !== null)
164
+ return fromPiAi;
165
+ // OpenRouter-style ids may live under the openrouter provider in pi-ai.
166
+ if (provider !== "openrouter" && modelId.includes("/")) {
167
+ const fromOpenRouterPiAi = lookupPiAiContextWindow("openrouter", modelId);
168
+ if (fromOpenRouterPiAi !== null)
169
+ return fromOpenRouterPiAi;
170
+ }
171
+ return DEFAULT_MODEL_CONTEXT_WINDOW;
172
+ }
173
+ /**
174
+ * Fetch and cache context window for a model. Never throws — falls back to sync resolution.
175
+ */
176
+ export async function fetchAndCacheContextWindow(provider, modelId, override) {
177
+ const fromOverride = applyOverride(override);
178
+ if (fromOverride !== null)
179
+ return fromOverride;
180
+ const cached = readCachedContextWindow(provider, modelId);
181
+ if (cached !== null)
182
+ return cached;
183
+ const sources = [
184
+ lookupPiAiContextWindow(provider, modelId),
185
+ provider !== "openrouter" && modelId.includes("/")
186
+ ? lookupPiAiContextWindow("openrouter", modelId)
187
+ : null,
188
+ await lookupOpenRouterContextWindow(modelId),
189
+ ];
190
+ for (const value of sources) {
191
+ if (value !== null) {
192
+ rememberContextWindow(provider, modelId, value);
193
+ return value;
194
+ }
195
+ }
196
+ const fallback = DEFAULT_MODEL_CONTEXT_WINDOW;
197
+ rememberContextWindow(provider, modelId, fallback);
198
+ return fallback;
199
+ }
200
+ /** @deprecated Use resolveContextWindowSync with provider, or session.getContextWindowTokens(). */
201
+ export function resolveContextWindow(modelId, override) {
202
+ return resolveContextWindowSync("openrouter", modelId, override);
203
+ }
204
+ /** Test helper — reset in-memory/disk cache state. */
205
+ export function resetModelContextCacheForTests() {
206
+ memoryEntries.clear();
207
+ diskCache = null;
208
+ openRouterFetchPromise = null;
209
+ if (existsSync(CACHE_FILE)) {
210
+ unlinkSync(CACHE_FILE);
211
+ }
212
+ }
@@ -0,0 +1,37 @@
1
+ export interface ProjectDetectionResult {
2
+ projectName?: string;
3
+ description?: string;
4
+ languages: string[];
5
+ frameworks: string[];
6
+ packageManagers: string[];
7
+ scripts?: Record<string, string>;
8
+ dependencies?: string[];
9
+ monorepoTool?: string;
10
+ ciCd: string[];
11
+ hasDocker: boolean;
12
+ hasGitignore: boolean;
13
+ readme?: string;
14
+ }
15
+ export interface ProjectDetectionOptions {
16
+ languages?: string[];
17
+ frameworks?: string[];
18
+ }
19
+ export declare class ProjectDetector {
20
+ static detect(cwd: string, opts?: ProjectDetectionOptions): ProjectDetectionResult | null;
21
+ }
22
+ /**
23
+ * Convert a ProjectDetectionResult into the compact string used by the prompt
24
+ * compiler's ## Project Stack section. Returns null when result is null.
25
+ */
26
+ export declare function formatProjectContext(result: ProjectDetectionResult, opts?: {
27
+ maxChars?: number;
28
+ }): string;
29
+ /**
30
+ * Detect project context and return a formatted string summary.
31
+ * Returns null when no meaningful signals are found.
32
+ * This is the main entry point consumed by session.ts.
33
+ */
34
+ export declare function buildProjectContext(cwd: string, opts?: {
35
+ languages?: string[];
36
+ frameworks?: string[];
37
+ }): string | null;