litura-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/SPEC.md +376 -0
- package/index.js +530 -0
- package/markdown.js +63 -0
- package/package.json +48 -0
- package/pi.js +149 -0
- package/public/app.js +17835 -0
- package/public/fonts/iAWriterDuoS-Bold.woff2 +0 -0
- package/public/fonts/iAWriterDuoS-BoldItalic.woff2 +0 -0
- package/public/fonts/iAWriterDuoS-Italic.woff2 +0 -0
- package/public/fonts/iAWriterDuoS-Regular.woff2 +0 -0
- package/public/index.html +104 -0
- package/public/style.css +849 -0
- package/review-model.js +38 -0
- package/review-prompt.js +93 -0
- package/review.js +265 -0
- package/style.md +150 -0
package/pi.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ModelRuntime } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { clampThinkingLevel, getSupportedThinkingLevels } from '@earendil-works/pi-ai';
|
|
3
|
+
|
|
4
|
+
const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
5
|
+
// Named models first. A router picks a different model per request, so the same
|
|
6
|
+
// draft comes back rewritten wholesale on one call and empty on the next — the
|
|
7
|
+
// app cannot hold a prompt contract against it. Kept last so an OpenRouter-only
|
|
8
|
+
// setup still starts, never as the preferred default.
|
|
9
|
+
const FALLBACK_MODELS = [
|
|
10
|
+
['amazon-bedrock', 'eu.anthropic.claude-sonnet-4-6'],
|
|
11
|
+
['anthropic', 'claude-sonnet-4-6'],
|
|
12
|
+
['openrouter', 'anthropic/claude-sonnet-4.6'],
|
|
13
|
+
['openrouter', 'auto'],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
let runtimePromise;
|
|
17
|
+
const runtime = () => runtimePromise ??= ModelRuntime.create({ allowModelNetwork: false });
|
|
18
|
+
|
|
19
|
+
const requestedThinking = () => THINKING_LEVELS.includes(process.env.PI_THINKING_LEVEL)
|
|
20
|
+
? process.env.PI_THINKING_LEVEL
|
|
21
|
+
: 'medium';
|
|
22
|
+
|
|
23
|
+
const selectionFor = (model, thinkingLevel = requestedThinking()) => ({
|
|
24
|
+
provider: model.provider,
|
|
25
|
+
model: model.id,
|
|
26
|
+
thinkingLevel: clampThinkingLevel(model, thinkingLevel),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export async function getAgentStatus() {
|
|
30
|
+
try {
|
|
31
|
+
const rt = await runtime();
|
|
32
|
+
const available = await rt.getAvailable();
|
|
33
|
+
const unique = [...new Map(available.map(model => [`${model.provider}\0${model.id}`, model])).values()];
|
|
34
|
+
const models = unique.map(model => ({
|
|
35
|
+
provider: model.provider,
|
|
36
|
+
model: model.id,
|
|
37
|
+
name: model.name,
|
|
38
|
+
reasoning: model.reasoning,
|
|
39
|
+
thinkingLevels: getSupportedThinkingLevels(model),
|
|
40
|
+
}));
|
|
41
|
+
const providers = [...new Set(models.map(model => model.provider))].map(id => ({
|
|
42
|
+
id,
|
|
43
|
+
name: rt.getProvider(id)?.name ?? id,
|
|
44
|
+
models: models.filter(model => model.provider === id),
|
|
45
|
+
}));
|
|
46
|
+
const authProviders = rt.getProviders()
|
|
47
|
+
.filter(provider => provider.auth.apiKey?.login)
|
|
48
|
+
.map(provider => {
|
|
49
|
+
const status = rt.getProviderAuthStatus(provider.id);
|
|
50
|
+
return {
|
|
51
|
+
id: provider.id,
|
|
52
|
+
name: provider.name,
|
|
53
|
+
label: provider.auth.apiKey.name,
|
|
54
|
+
configured: status.configured,
|
|
55
|
+
...(status.source ? { source: status.source } : {}),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const configured = process.env.PI_PROVIDER && process.env.PI_MODEL
|
|
59
|
+
? unique.find(model => model.provider === process.env.PI_PROVIDER && model.id === process.env.PI_MODEL)
|
|
60
|
+
: undefined;
|
|
61
|
+
const selected = configured
|
|
62
|
+
?? FALLBACK_MODELS.map(([provider, id]) => unique.find(model => model.provider === provider && model.id === id)).find(Boolean)
|
|
63
|
+
?? unique[0];
|
|
64
|
+
return {
|
|
65
|
+
available: models.length > 0,
|
|
66
|
+
...(selected ? { defaultSelection: selectionFor(selected) } : {}),
|
|
67
|
+
providers,
|
|
68
|
+
models,
|
|
69
|
+
authProviders,
|
|
70
|
+
...(rt.getError() ? { error: rt.getError() } : {}),
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { available: false, providers: [], models: [], authProviders: [], error: error.message };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function saveProviderApiKey(providerId, apiKey) {
|
|
78
|
+
const rt = await runtime();
|
|
79
|
+
const provider = rt.getProvider(providerId);
|
|
80
|
+
if (!provider?.auth.apiKey?.login) throw new Error(`Provider ${providerId} does not support API-key login`);
|
|
81
|
+
if (!apiKey.trim() || apiKey.length > 10_000) throw new Error('API key is empty or too long');
|
|
82
|
+
await rt.login(providerId, 'api_key', { prompt: async () => apiKey.trim(), notify: () => {} });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function removeProviderApiKey(providerId) {
|
|
86
|
+
const rt = await runtime();
|
|
87
|
+
if (!rt.getProvider(providerId)) throw new Error(`Unknown provider ${providerId}`);
|
|
88
|
+
await rt.logout(providerId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function resolveModel(selection) {
|
|
92
|
+
const rt = await runtime();
|
|
93
|
+
if (!selection && process.env.PI_PROVIDER && process.env.PI_MODEL) {
|
|
94
|
+
selection = { provider: process.env.PI_PROVIDER, model: process.env.PI_MODEL, thinkingLevel: requestedThinking() };
|
|
95
|
+
}
|
|
96
|
+
if (selection) {
|
|
97
|
+
const model = rt.getModel(selection.provider, selection.model);
|
|
98
|
+
if (!model) throw new Error(`Unknown Pi model ${selection.provider}/${selection.model}`);
|
|
99
|
+
const available = await rt.getAvailable(selection.provider);
|
|
100
|
+
if (!available.some(candidate => candidate.id === selection.model)) {
|
|
101
|
+
throw new Error(`Pi model ${selection.provider}/${selection.model} is not authenticated or unavailable`);
|
|
102
|
+
}
|
|
103
|
+
return { rt, model, selection: selectionFor(model, selection.thinkingLevel) };
|
|
104
|
+
}
|
|
105
|
+
const available = await rt.getAvailable();
|
|
106
|
+
const model = FALLBACK_MODELS.map(([provider, id]) => available.find(candidate => candidate.provider === provider && candidate.id === id)).find(Boolean)
|
|
107
|
+
?? available[0];
|
|
108
|
+
if (!model) throw new Error('No authenticated Pi model is available; open Settings and add an API key');
|
|
109
|
+
return { rt, model, selection: selectionFor(model) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Either a single user turn or a whole transcript — the chat needs the latter.
|
|
113
|
+
const request = (systemPrompt, userPrompt, history) => ({
|
|
114
|
+
systemPrompt,
|
|
115
|
+
messages: (history ?? [{ role: 'user', content: userPrompt }])
|
|
116
|
+
.map(message => ({ role: message.role, content: message.content, timestamp: Date.now() })),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const options = (selection, maxTokens, signal) => ({
|
|
120
|
+
maxTokens,
|
|
121
|
+
signal,
|
|
122
|
+
...(selection.thinkingLevel === 'off' ? {} : { reasoning: selection.thinkingLevel }),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export async function completeText({ systemPrompt, userPrompt, selection, maxTokens = 1500, signal }) {
|
|
126
|
+
const resolved = await resolveModel(selection);
|
|
127
|
+
const response = await resolved.rt.completeSimple(
|
|
128
|
+
resolved.model,
|
|
129
|
+
request(systemPrompt, userPrompt),
|
|
130
|
+
options(resolved.selection, maxTokens, signal),
|
|
131
|
+
);
|
|
132
|
+
if (response.stopReason === 'error' || response.stopReason === 'aborted') {
|
|
133
|
+
throw new Error(response.errorMessage ?? 'Pi request failed');
|
|
134
|
+
}
|
|
135
|
+
return response.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('').trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function streamText({ systemPrompt, userPrompt, messages, selection, maxTokens = 2000, signal, onText }) {
|
|
139
|
+
const resolved = await resolveModel(selection);
|
|
140
|
+
const stream = resolved.rt.streamSimple(
|
|
141
|
+
resolved.model,
|
|
142
|
+
request(systemPrompt, userPrompt, messages),
|
|
143
|
+
options(resolved.selection, maxTokens, signal),
|
|
144
|
+
);
|
|
145
|
+
for await (const event of stream) {
|
|
146
|
+
if (event.type === 'text_delta') onText(event.delta);
|
|
147
|
+
if (event.type === 'error') throw new Error(event.error.errorMessage ?? 'Pi request failed');
|
|
148
|
+
}
|
|
149
|
+
}
|