pi-spark 0.12.0 → 0.13.1
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 +86 -61
- package/assets/screenshot-credits.png +0 -0
- package/assets/screenshot-overview.png +0 -0
- package/assets/screenshot-presets.png +0 -0
- package/assets/screenshot-recap.png +0 -0
- package/assets/screenshot-tools.png +0 -0
- package/assets/screenshot-tui.png +0 -0
- package/index.ts +31 -0
- package/package.json +5 -4
- package/{extensions/shared → src}/components/inline-text.ts +1 -1
- package/{extensions/shared → src}/config/index.ts +37 -12
- package/src/config/schema.ts +30 -0
- package/{extensions/shared → src}/events/auto-collect-events.ts +1 -1
- package/src/events/index.ts +3 -0
- package/{extensions → src/features}/credits/index.ts +5 -7
- package/{extensions → src/features}/credits/providers/deepseek.ts +1 -1
- package/{extensions → src/features}/credits/providers/fireworks.ts +1 -1
- package/{extensions → src/features}/credits/providers/moonshot.ts +1 -1
- package/{extensions → src/features}/credits/providers/openai-codex.ts +1 -1
- package/{extensions → src/features}/credits/providers/openrouter.ts +1 -1
- package/{extensions → src/features}/credits/providers/vercel-ai-gateway.ts +1 -1
- package/{extensions → src/features}/editor/index.ts +8 -11
- package/{extensions → src/features}/footer/index.ts +7 -9
- package/{extensions → src/features}/fullscreen/index.ts +6 -9
- package/src/features/model/config.ts +3 -0
- package/{extensions/models → src/features/model}/index.ts +5 -5
- package/{extensions → src/features}/name/index.ts +6 -8
- package/{extensions → src/features}/presets/config.ts +1 -1
- package/{extensions → src/features}/presets/index.ts +4 -4
- package/{extensions → src/features}/presets/manager.ts +2 -2
- package/{extensions → src/features}/recap/config.ts +1 -1
- package/{extensions → src/features}/recap/index.ts +4 -6
- package/{extensions → src/features}/recap/manager.ts +5 -5
- package/{extensions → src/features}/recap/model.ts +2 -2
- package/{extensions/shared/format/index.ts → src/utils/format.ts} +29 -0
- package/{extensions/shared/usage/index.ts → src/utils/usage.ts} +0 -18
- package/assets/credits.png +0 -0
- package/assets/screenshot.png +0 -0
- package/extensions/credits/utils.ts +0 -52
- package/extensions/models/config.ts +0 -3
- package/extensions/shared/config/schema.ts +0 -24
- package/extensions/shared/events/index.ts +0 -3
- /package/{extensions/shared → src}/components/split-line.ts +0 -0
- /package/{extensions/shared → src}/config/model.ts +0 -0
- /package/{extensions/shared → src}/events/preset.ts +0 -0
- /package/{extensions → src/features}/credits/config.ts +0 -0
- /package/{extensions → src/features}/credits/manager.ts +0 -0
- /package/{extensions → src/features}/credits/providers/fireworks.proto +0 -0
- /package/{extensions → src/features}/credits/providers/index.ts +0 -0
- /package/{extensions → src/features}/credits/status.ts +0 -0
- /package/{extensions → src/features}/credits/types.ts +0 -0
- /package/{extensions → src/features}/editor/config.ts +0 -0
- /package/{extensions → src/features}/editor/spinner.ts +0 -0
- /package/{extensions → src/features}/footer/config.ts +0 -0
- /package/{extensions → src/features}/fullscreen/config.ts +0 -0
- /package/{extensions → src/features}/fullscreen/filler.ts +0 -0
- /package/{extensions → src/features}/name/config.ts +0 -0
- /package/{extensions → src/features}/presets/selector.ts +0 -0
- /package/{extensions → src/features}/recap/idle.ts +0 -0
- /package/{extensions → src/features}/recap/widget.ts +0 -0
package/README.md
CHANGED
|
@@ -2,46 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
# pi-spark
|
|
4
4
|
|
|
5
|
-
[Pi](https://pi.dev/)
|
|
5
|
+
[Pi](https://pi.dev/) package that polishes your daily experience and keeps you at the frontier of agentic workflows.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+

|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- **Editor:** replaces the default editor with a compact working indicator (inspired by [Amp](https://ampcode.com/)) and current model info.
|
|
11
|
-
- **Footer:** shows session information, extension statuses, cost, and context usage on one line.
|
|
12
|
-
- **Fullscreen:** clears the screen and scrollback on session start, pins the editor and footer to the bottom for a full-screen session, and clears again on exit.
|
|
13
|
-
- **Models:** exposes a `model` tool so the agent can list the models the user can use currently and inspect the provider, model, and thinking level in use.
|
|
14
|
-
- **Name:** exposes a `name` tool so the agent can give the current session a concise, recognizable name in the session selector.
|
|
15
|
-
- **Presets:** switches named model presets with `/preset`, `--preset`, and quick cycle shortcuts.
|
|
16
|
-
- **Recap:** generates a short idle-session recap and exposes a `/recap` command for manual generation, inspired by [Claude Code's session recap](https://code.claude.com/docs/en/interactive-mode#session-recap).
|
|
9
|
+
## Features
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
### Compact TUI: editor, footer, and fullscreen
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
pi-spark ships with custom editor, footer, and fullscreen rendering, replacing the default ones. The compact TUI gives you a calm, immersive experience without distraction.
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
- The editor shows a working indicator inspired by [Amp](https://ampcode.com/) and the current model on the top border. If you use [presets](#presets), the active preset appears there too.
|
|
16
|
+
- The footer shows session information, extension statuses, cost, and context usage on one line.
|
|
17
|
+
- The fullscreen rendering clears the terminal screen and scrollback on session start and exit, and pins the editor and footer to the bottom.
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
### Credits
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
pi-spark shows the active provider's credit balance or rate-limit usage in the status line, so you can keep an eye on what's left without leaving the terminal.
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
- Supported providers: DeepSeek, Fireworks, Moonshot, OpenAI Codex, OpenRouter, and Vercel AI Gateway.
|
|
26
|
+
- Most provider fetching follows [CodexBar](https://github.com/steipete/codexbar). Fireworks is the exception: its balance sits behind an internal gRPC API, reverse-engineered from the `firectl` binary (see [docs/fireworks.md](./docs/fireworks.md)).
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
### Presets
|
|
31
|
+
|
|
32
|
+
pi-spark lets you define named model presets in `spark.json` (see [Configuration](#configuration)), so you can switch between models and thinking levels without retyping provider details. The active preset is shown on the editor's top border.
|
|
33
|
+
|
|
34
|
+
- Switch interactively with `/preset`, or jump straight to one with `/preset <name>`.
|
|
35
|
+
- Start pi on a given preset with `pi --preset <name>`.
|
|
36
|
+
- Cycle presets with `ctrl+super+p` (forward) and `ctrl+shift+super+p` (backward); `super` is `command` on macOS and needs a terminal that forwards it.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
### Recap
|
|
41
|
+
|
|
42
|
+
pi-spark generates a short recap of the current session after it goes idle, or on demand, inspired by [Claude Code's session recap](https://code.claude.com/docs/en/interactive-mode#session-recap).
|
|
43
|
+
|
|
44
|
+
- A recap is generated automatically once the session stays idle past `recap.idle` in `spark.json`.
|
|
45
|
+
- Run `/recap` to generate one manually at any time.
|
|
46
|
+
- The recap can use its own model, configured separately from your working model.
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+

|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
### Agent tools: `name` and `model`
|
|
37
51
|
|
|
38
|
-
|
|
52
|
+
pi-spark provides the pi coding agent with tools to inspect and manipulate itself. Currently, `name` and `model` tools are included.
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
- `name` sets or updates the current session's name.
|
|
55
|
+
- `model` shows the active model or lists pi models.
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
pi-spark reads config from `~/.pi/agent/spark.json` and from the current project's `.pi/spark.json`. Project config overrides matching global fields.
|
|
62
|
+
|
|
63
|
+
For example:
|
|
41
64
|
|
|
42
65
|
```json
|
|
43
66
|
{
|
|
44
|
-
"credits": false,
|
|
45
67
|
"editor": {
|
|
46
68
|
"spinner": "dots"
|
|
47
69
|
},
|
|
@@ -67,58 +89,61 @@ Example:
|
|
|
67
89
|
}
|
|
68
90
|
```
|
|
69
91
|
|
|
70
|
-
###
|
|
71
|
-
|
|
72
|
-
- pi-spark shows the active provider's credit balance or rate-limit usage in the status line.
|
|
73
|
-
- Supported providers: DeepSeek, Fireworks, Moonshot, OpenAI Codex, OpenRouter, and Vercel AI Gateway.
|
|
74
|
-
- Most provider-specific fetching approaches reference [CodexBar](https://github.com/steipete/codexbar). Fireworks is an exception: its balance lives behind an internal gRPC API, so the approach was reverse-engineered from the `firectl` binary — see [docs/fireworks.md](./docs/fireworks.md).
|
|
75
|
-
|
|
76
|
-
### Editor
|
|
77
|
-
|
|
78
|
-
- `editor.spinner` controls the working indicator style and can be `dots`, `lights`, `tildes`, or `pulse`.
|
|
92
|
+
### References
|
|
79
93
|
|
|
80
|
-
|
|
94
|
+
All fields are optional. Each top-level feature runs with the defaults below unless you [turn it off](#turn-off-the-features-you-dont-like).
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
| Field | Value (or `false`) | Description |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `credits` | `{}` | Shows the active provider's credit balance or rate-limit usage in the status line. |
|
|
99
|
+
| `editor` | `EditorConfig` | Shows a working indicator and the current model on the editor's top border. |
|
|
100
|
+
| `footer` | `{}` | Shows session info, extension statuses, cost, and context usage on one line. |
|
|
101
|
+
| `fullscreen` | `{}` | Clears the screen and scrollback on start and exit, and pins the editor and footer to the bottom. |
|
|
102
|
+
| `model` | `{}` | Exposes the `model` agent tool. |
|
|
103
|
+
| `name` | `{}` | Exposes the `name` agent tool. |
|
|
104
|
+
| `presets` | `{ [name]: Preset }` | Defines named model presets, keyed by name. |
|
|
105
|
+
| `recap` | `RecapConfig` | Generates a session recap when idle or on demand. |
|
|
83
106
|
|
|
84
|
-
|
|
107
|
+
#### `EditorConfig`
|
|
85
108
|
|
|
86
|
-
|
|
109
|
+
The `spinner` field is optional and defaults to `tildes`.
|
|
87
110
|
|
|
88
|
-
|
|
111
|
+
| Field | Value | Description |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `spinner` | `dots` | `⠋, ⠙, ⠹, ⠸, ⠼, ⠴, ⠦, ⠧, ⠇, ⠏` |
|
|
114
|
+
| | `lights` | `○, ●` |
|
|
115
|
+
| | `tildes` (default) | `∼, ≈, ≋, ≈, ∼` |
|
|
116
|
+
| | `pulse` | `·, •, ●, •, ·` |
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
- `active`: gets the active provider, model, and thinking level.
|
|
92
|
-
- `list`: lists models with their metadata, with an optional [Liqe](https://github.com/gajus/liqe) (Lucene-like) `query` over model fields and `offset`/`limit` paging.
|
|
118
|
+
#### `Preset`
|
|
93
119
|
|
|
94
|
-
|
|
120
|
+
Each preset must set all three fields.
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
| Field | Value | Description |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `provider` | string | Provider id, e.g. `anthropic`. |
|
|
125
|
+
| `model` | string | Model id, e.g. `claude-opus-4-8`. |
|
|
126
|
+
| `thinkingLevel` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | Thinking level for the preset. |
|
|
97
127
|
|
|
98
|
-
|
|
128
|
+
#### `RecapConfig`
|
|
99
129
|
|
|
100
|
-
|
|
130
|
+
All fields are optional, including `thinkingLevel`.
|
|
101
131
|
|
|
102
|
-
|
|
132
|
+
| Field | Value | Description |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `idle` | number (ms) or duration string | How long the session must stay idle before a recap is generated. Accepts a millisecond number or a [vercel/ms](https://github.com/vercel/ms) string (e.g. `"3m"`); minimum 5000 ms, defaults to 3 minutes. |
|
|
135
|
+
| `provider` | string | Provider id for the recap model. |
|
|
136
|
+
| `model` | string | Model id for the recap model. |
|
|
137
|
+
| `thinkingLevel` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | Thinking level for the recap model. |
|
|
103
138
|
|
|
104
|
-
|
|
105
|
-
- Start pi with a preset using `pi --preset <key>`
|
|
106
|
-
- Cycle presets with `ctrl+super+p` and `ctrl+shift+super+p` (`super` is `command` on macOS)
|
|
139
|
+
### Turn off the features you don't like
|
|
107
140
|
|
|
108
|
-
|
|
141
|
+
All features are enabled by default. Set a specific feature to `false` in `spark.json` to disable it.
|
|
109
142
|
|
|
110
|
-
|
|
111
|
-
- The `recap.idle` value sets how long the session must stay idle before a recap is generated. It accepts either a millisecond number or a human-readable duration string parsed by [vercel/ms](https://github.com/vercel/ms) (e.g., `"3m"`, `"30s"`, `"2 minutes"`), and must resolve to at least 5000ms.
|
|
112
|
-
- The recap model can be customized with `provider`, `model`, and `thinkingLevel`.
|
|
113
|
-
|
|
114
|
-
## Recommended pi settings
|
|
115
|
-
|
|
116
|
-
[Pi 0.79.0](https://pi.dev/news/releases/0.79.0) added a project trust dialog that asks before loading project-local resources ([earendil-works/pi#5514](https://github.com/earendil-works/pi/issues/5514)), and [pi 0.79.1](https://pi.dev/news/releases/0.79.1) made the default behavior configurable via `defaultProjectTrust`. To keep startup minimal, set it to `"always"` in `~/.pi/agent/settings.json` (or change it with `/settings`):
|
|
143
|
+
For example, to disable the customized footer:
|
|
117
144
|
|
|
118
145
|
```json
|
|
119
146
|
{
|
|
120
|
-
"
|
|
147
|
+
"footer": false
|
|
121
148
|
}
|
|
122
149
|
```
|
|
123
|
-
|
|
124
|
-
Project trust is an [input-loading guard](https://pi.dev/docs/latest/security#project-trust), so use `"always"` only if you trust the projects you open.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { autoCollectEvents } from "./src/events";
|
|
2
|
+
import { registerCredits } from "./src/features/credits";
|
|
3
|
+
import { registerEditor } from "./src/features/editor";
|
|
4
|
+
import { registerFooter } from "./src/features/footer";
|
|
5
|
+
import { registerFullscreen } from "./src/features/fullscreen";
|
|
6
|
+
import { registerModel } from "./src/features/model";
|
|
7
|
+
import { registerName } from "./src/features/name";
|
|
8
|
+
import { registerPresets } from "./src/features/presets";
|
|
9
|
+
import { registerRecap } from "./src/features/recap";
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* pi-spark is a single extension that bundles several features. Each feature registers itself
|
|
15
|
+
* like a standalone extension and reads its config via the shared, cached `loadConfig`. This file
|
|
16
|
+
* is just the registry that wires them together.
|
|
17
|
+
*/
|
|
18
|
+
export default function (pi: ExtensionAPI) {
|
|
19
|
+
// Own the event-bus subscription lifecycle here so features never manage cleanup themselves;
|
|
20
|
+
// the collector disposes every subscription on session_shutdown.
|
|
21
|
+
const events = autoCollectEvents(pi);
|
|
22
|
+
|
|
23
|
+
registerCredits(pi);
|
|
24
|
+
registerEditor(pi, events);
|
|
25
|
+
registerFooter(pi);
|
|
26
|
+
registerFullscreen(pi);
|
|
27
|
+
registerModel(pi);
|
|
28
|
+
registerName(pi);
|
|
29
|
+
registerPresets(pi);
|
|
30
|
+
registerRecap(pi);
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi
|
|
3
|
+
"version": "0.13.1",
|
|
4
|
+
"description": "Pi package that polishes your daily experience and keeps you at the frontier of agentic workflows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
7
7
|
"pi-package"
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"files": [
|
|
16
|
-
"
|
|
16
|
+
"index.ts",
|
|
17
|
+
"src",
|
|
17
18
|
"assets",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
20
21
|
],
|
|
21
22
|
"pi": {
|
|
22
23
|
"extensions": [
|
|
23
|
-
"./
|
|
24
|
+
"./index.ts"
|
|
24
25
|
],
|
|
25
26
|
"image": "https://raw.githubusercontent.com/zlliang/pi-spark/main/assets/cover.png"
|
|
26
27
|
},
|
|
@@ -2,26 +2,51 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { sparkConfigSchema } from "./schema";
|
|
6
6
|
|
|
7
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import type {
|
|
8
|
+
import type { SparkConfig } from "./schema";
|
|
9
9
|
|
|
10
10
|
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
11
11
|
type JsonObject = { [key: string]: JsonValue };
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
/** All features disabled; used as the fallback when `spark.json` fails validation. */
|
|
14
|
+
const DISABLED_CONFIG: SparkConfig = Object.freeze({
|
|
15
|
+
credits: false,
|
|
16
|
+
editor: false,
|
|
17
|
+
footer: false,
|
|
18
|
+
fullscreen: false,
|
|
19
|
+
model: false,
|
|
20
|
+
name: false,
|
|
21
|
+
presets: false,
|
|
22
|
+
recap: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const cache = new Map<string, SparkConfig>();
|
|
26
|
+
|
|
27
|
+
/** Load and validate spark.json once per session lifecycle; later calls return the cached result. */
|
|
28
|
+
export function loadConfig(ctx: ExtensionContext, fileName: string = "spark.json"): SparkConfig {
|
|
29
|
+
const key = `${ctx.cwd}\u0000${fileName}`;
|
|
30
|
+
const cached = cache.get(key);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
|
|
33
|
+
const rawConfig = loadMergedJson(getConfigPaths(ctx.cwd, fileName)) ?? {};
|
|
34
|
+
const result = sparkConfigSchema.safeParse(rawConfig);
|
|
35
|
+
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
const message = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
38
|
+
ctx.ui.notify(`Invalid spark config: ${message}`, "error");
|
|
39
|
+
|
|
40
|
+
const config = DISABLED_CONFIG;
|
|
41
|
+
cache.set(key, config);
|
|
42
|
+
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
20
45
|
|
|
21
|
-
const
|
|
22
|
-
|
|
46
|
+
const config = result.data;
|
|
47
|
+
cache.set(key, config);
|
|
23
48
|
|
|
24
|
-
return
|
|
49
|
+
return config;
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
function getConfigPaths(cwd: string, fileName: string): [globalPath: string, projectPath: string] {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
import { creditsConfigSchema } from "../features/credits/config";
|
|
4
|
+
import { editorConfigSchema } from "../features/editor/config";
|
|
5
|
+
import { footerConfigSchema } from "../features/footer/config";
|
|
6
|
+
import { fullscreenConfigSchema } from "../features/fullscreen/config";
|
|
7
|
+
import { modelConfigSchema } from "../features/model/config";
|
|
8
|
+
import { nameConfigSchema } from "../features/name/config";
|
|
9
|
+
import { presetsConfigSchema } from "../features/presets/config";
|
|
10
|
+
import { recapConfigSchema } from "../features/recap/config";
|
|
11
|
+
|
|
12
|
+
const disabled = z.literal(false);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Each feature field is `{ ... } | false`: `false` disables the feature, an object configures
|
|
16
|
+
* it, and an omitted field falls back to defaults (`{}`, enabled).
|
|
17
|
+
*/
|
|
18
|
+
export const sparkConfigSchema = z.object({
|
|
19
|
+
credits: creditsConfigSchema.or(disabled).default({}),
|
|
20
|
+
editor: editorConfigSchema.or(disabled).default({}),
|
|
21
|
+
footer: footerConfigSchema.or(disabled).default({}),
|
|
22
|
+
fullscreen: fullscreenConfigSchema.or(disabled).default({}),
|
|
23
|
+
model: modelConfigSchema.or(disabled).default({}),
|
|
24
|
+
name: nameConfigSchema.or(disabled).default({}),
|
|
25
|
+
presets: presetsConfigSchema.or(disabled).default({}),
|
|
26
|
+
recap: recapConfigSchema.or(disabled).default({}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Resolved config for every feature; `false` means the feature is disabled. */
|
|
30
|
+
export type SparkConfig = z.infer<typeof sparkConfigSchema>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreditsManager } from "./manager";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { loadConfig } from "../../config";
|
|
3
|
+
import { isUsage } from "../../utils/usage";
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
@@ -12,14 +12,12 @@ function hasCost(message: AgentMessage): boolean {
|
|
|
12
12
|
return usage.cost.total > 0 || usage.input > 0 || usage.output > 0;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export function registerCredits(pi: ExtensionAPI): void {
|
|
16
16
|
let creditsManager: CreditsManager | undefined = undefined;
|
|
17
17
|
|
|
18
18
|
pi.on("session_start", (_event, ctx) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const config = loadConfig(ctx, "credits");
|
|
22
|
-
if (!config) return;
|
|
19
|
+
const config = loadConfig(ctx).credits;
|
|
20
|
+
if (!ctx.hasUI || !config) return;
|
|
23
21
|
|
|
24
22
|
creditsManager = new CreditsManager();
|
|
25
23
|
creditsManager.refresh(ctx);
|
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { credentials, loadPackageDefinition, Metadata } from "@grpc/grpc-js";
|
|
4
4
|
import { loadSync } from "@grpc/proto-loader";
|
|
5
5
|
|
|
6
|
-
import { toNumber } from "
|
|
6
|
+
import { toNumber } from "../../../utils/format";
|
|
7
7
|
|
|
8
8
|
import type { ClientUnaryCall, ServiceClientConstructor, ServiceError } from "@grpc/grpc-js";
|
|
9
9
|
import type { Credits, CreditsProvider } from "../types";
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { Spinner } from "./spinner";
|
|
4
|
-
import { SplitLine } from "
|
|
5
|
-
import { loadConfig } from "
|
|
6
|
-
import {
|
|
7
|
-
import { formatModel } from "
|
|
4
|
+
import { SplitLine } from "../../components/split-line";
|
|
5
|
+
import { loadConfig } from "../../config";
|
|
6
|
+
import { PRESET_CHANGE, parsePresetChange } from "../../events";
|
|
7
|
+
import { formatModel } from "../../utils/format";
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { TUI, EditorTheme } from "@earendil-works/pi-tui";
|
|
11
|
+
import type { EventCollector } from "../../events";
|
|
11
12
|
|
|
12
13
|
class Editor extends CustomEditor {
|
|
13
14
|
private pi: ExtensionAPI;
|
|
@@ -81,18 +82,14 @@ class Editor extends CustomEditor {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
export
|
|
85
|
-
const events = autoCollectEvents(pi);
|
|
86
|
-
|
|
85
|
+
export function registerEditor(pi: ExtensionAPI, events: EventCollector): void {
|
|
87
86
|
let editor: Editor | undefined = undefined;
|
|
88
87
|
let spinner: Spinner | undefined = undefined;
|
|
89
88
|
let runningToolCallIds = new Set<string>();
|
|
90
89
|
|
|
91
90
|
pi.on("session_start", (_event, ctx) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const config = loadConfig(ctx, "editor");
|
|
95
|
-
if (!config) return;
|
|
91
|
+
const config = loadConfig(ctx).editor;
|
|
92
|
+
if (!ctx.hasUI || !config) return;
|
|
96
93
|
|
|
97
94
|
spinner = new Spinner(config.spinner);
|
|
98
95
|
|
|
@@ -2,10 +2,10 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
|
|
5
|
-
import { SplitLine } from "
|
|
6
|
-
import { loadConfig } from "
|
|
7
|
-
import { formatContextUsage, formatCost, formatCwd, linkText, sanitizeText } from "
|
|
8
|
-
import { getEntryUsage } from "
|
|
5
|
+
import { SplitLine } from "../../components/split-line";
|
|
6
|
+
import { loadConfig } from "../../config";
|
|
7
|
+
import { formatContextUsage, formatCost, formatCwd, linkText, sanitizeText } from "../../utils/format";
|
|
8
|
+
import { getEntryUsage } from "../../utils/usage";
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext, ExtensionAPI, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import type { Component } from "@earendil-works/pi-tui";
|
|
@@ -80,12 +80,10 @@ class FooterComponent implements Component {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export
|
|
83
|
+
export function registerFooter(pi: ExtensionAPI): void {
|
|
84
84
|
pi.on("session_start", (_event, ctx) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const config = loadConfig(ctx, "footer");
|
|
88
|
-
if (!config) return;
|
|
85
|
+
const config = loadConfig(ctx).footer;
|
|
86
|
+
if (!ctx.hasUI || !config) return;
|
|
89
87
|
|
|
90
88
|
ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData));
|
|
91
89
|
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
4
|
import { BottomFiller } from "./filler";
|
|
4
|
-
import { loadConfig } from "
|
|
5
|
-
import { sanitizeText } from "
|
|
5
|
+
import { loadConfig } from "../../config";
|
|
6
|
+
import { sanitizeText } from "../../utils/format";
|
|
6
7
|
|
|
7
8
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
|
|
10
9
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
11
10
|
import type { UserMessage } from "@earendil-works/pi-ai";
|
|
12
11
|
|
|
@@ -33,16 +32,14 @@ function extractText(message: UserMessage): string {
|
|
|
33
32
|
return content.filter((block) => block.type === "text").map((block) => block.text).join(" ");
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export
|
|
35
|
+
export function registerFullscreen(pi: ExtensionAPI): void {
|
|
37
36
|
let tui: TUI | undefined;
|
|
38
37
|
let enabled = false;
|
|
39
38
|
let pendingClear = false;
|
|
40
39
|
|
|
41
40
|
pi.on("session_start", (_event, ctx) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const config = loadConfig(ctx, "fullscreen");
|
|
45
|
-
if (!config) return;
|
|
41
|
+
const config = loadConfig(ctx).fullscreen;
|
|
42
|
+
if (!ctx.hasUI || !config) return;
|
|
46
43
|
|
|
47
44
|
enabled = true;
|
|
48
45
|
pendingClear = true;
|
|
@@ -4,8 +4,8 @@ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
|
4
4
|
import { filter, parse } from "liqe";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
|
|
7
|
-
import { loadConfig } from "
|
|
8
|
-
import { formatModel, formatTokens } from "
|
|
7
|
+
import { loadConfig } from "../../config";
|
|
8
|
+
import { formatModel, formatTokens } from "../../utils/format";
|
|
9
9
|
|
|
10
10
|
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { ExtensionAPI, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
@@ -72,9 +72,9 @@ function formatListNotice(truncated: boolean, startIndex: number, endDisplay: nu
|
|
|
72
72
|
return undefined;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export
|
|
75
|
+
export function registerModel(pi: ExtensionAPI): void {
|
|
76
76
|
pi.on("session_start", (_event, ctx) => {
|
|
77
|
-
const config = loadConfig(ctx
|
|
77
|
+
const config = loadConfig(ctx).model;
|
|
78
78
|
if (!config) return;
|
|
79
79
|
|
|
80
80
|
pi.registerTool({
|
|
@@ -89,7 +89,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
89
|
promptSnippet: "Show the active model or list pi models",
|
|
90
90
|
promptGuidelines: [
|
|
91
91
|
"Use the model tool when you need pi model metadata, the active model, or the thinking level, rather than guessing.",
|
|
92
|
-
"
|
|
92
|
+
"Use the model tool with `available:true` in the query unless unavailable models are explicitly needed.",
|
|
93
93
|
],
|
|
94
94
|
parameters: Type.Object({
|
|
95
95
|
action: StringEnum(["active", "list"] as const, {
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
-
import { loadConfig } from "
|
|
5
|
-
import { sanitizeText } from "
|
|
4
|
+
import { loadConfig } from "../../config";
|
|
5
|
+
import { sanitizeText } from "../../utils/format";
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function registerName(pi: ExtensionAPI): void {
|
|
10
10
|
pi.on("session_start", (_event, ctx) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const config = loadConfig(ctx, "name");
|
|
11
|
+
const config = loadConfig(ctx).name;
|
|
14
12
|
if (!config) return;
|
|
15
13
|
|
|
16
14
|
pi.registerTool({
|
|
17
15
|
name: "name",
|
|
18
16
|
label: "name",
|
|
19
17
|
description:
|
|
20
|
-
"
|
|
18
|
+
"Set or update the current session's name. The name is a concise label shown in the session " +
|
|
21
19
|
"selector instead of the first-message preview.",
|
|
22
|
-
promptSnippet: "
|
|
20
|
+
promptSnippet: "Set or update the current session's name",
|
|
23
21
|
promptGuidelines: [
|
|
24
22
|
"Use the name tool when the session needs a concise, recognizable label, especially after a long, vague, or pasted opening prompt.",
|
|
25
23
|
"Use the name tool to rename the session only after a substantial shift in the conversation's focus, not for minor follow-ups.",
|
|
@@ -2,11 +2,11 @@ import { Key } from "@earendil-works/pi-tui";
|
|
|
2
2
|
|
|
3
3
|
import { PresetManager } from "./manager";
|
|
4
4
|
import { showPresetSelector } from "./selector";
|
|
5
|
-
import { loadConfig } from "
|
|
5
|
+
import { loadConfig } from "../../config";
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function registerPresets(pi: ExtensionAPI): void {
|
|
10
10
|
let presetManager: PresetManager | undefined = undefined;
|
|
11
11
|
|
|
12
12
|
pi.registerFlag("preset", {
|
|
@@ -14,10 +14,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
14
14
|
type: "string",
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
pi.on("session_start", async (
|
|
17
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
18
|
+
const config = loadConfig(ctx).presets;
|
|
18
19
|
const presetFlag = pi.getFlag("preset");
|
|
19
20
|
|
|
20
|
-
const config = loadConfig(ctx, "presets");
|
|
21
21
|
if (!config || Object.keys(config).length === 0) {
|
|
22
22
|
if (presetFlag) ctx.ui.notify("No presets defined in spark.json", "warning");
|
|
23
23
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PRESET_CHANGE } from "
|
|
2
|
-
import { formatModel } from "
|
|
1
|
+
import { PRESET_CHANGE } from "../../events";
|
|
2
|
+
import { formatModel } from "../../utils/format";
|
|
3
3
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { PresetsConfig, PresetConfig } from "./config";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
3
|
import { idleTimeoutSchema } from "./idle";
|
|
4
|
-
import { optionalModelSchema } from "
|
|
4
|
+
import { optionalModelSchema } from "../../config/model";
|
|
5
5
|
|
|
6
6
|
export const recapConfigSchema = optionalModelSchema.extend({
|
|
7
7
|
idle: idleTimeoutSchema.optional(),
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { IdleListener } from "./idle";
|
|
2
2
|
import { RecapManager } from "./manager";
|
|
3
|
-
import { loadConfig } from "
|
|
3
|
+
import { loadConfig } from "../../config";
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export function registerRecap(pi: ExtensionAPI): void {
|
|
8
8
|
let idleListener: IdleListener<ExtensionContext> | undefined = undefined;
|
|
9
9
|
let recapManager: RecapManager | undefined = undefined;
|
|
10
10
|
|
|
11
11
|
pi.on("session_start", (event, ctx) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const config = loadConfig(ctx, "recap");
|
|
15
|
-
if (!config) return;
|
|
12
|
+
const config = loadConfig(ctx).recap;
|
|
13
|
+
if (!ctx.hasUI || !config) return;
|
|
16
14
|
|
|
17
15
|
recapManager = new RecapManager(pi, config);
|
|
18
16
|
|
|
@@ -3,22 +3,22 @@ import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
|
|
4
4
|
import { resolveRecapModelSettings } from "./model";
|
|
5
5
|
import { clearRecapWidget, setRecapLoadingWidget, setRecapTextWidget } from "./widget";
|
|
6
|
-
import { sanitizeText } from "
|
|
6
|
+
import { sanitizeText } from "../../utils/format";
|
|
7
7
|
|
|
8
8
|
import type { Api, Model, ModelThinkingLevel, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { RecapConfig } from "./config";
|
|
11
11
|
|
|
12
12
|
const SYSTEM_PROMPT = [
|
|
13
|
-
"You write concise idle
|
|
13
|
+
"You write concise idle session recaps for a terminal coding agent.",
|
|
14
14
|
"Use only transcript-supported facts; do not invent progress, intent, files, or next steps.",
|
|
15
15
|
"Prefer the latest active task if the session changed direction.",
|
|
16
16
|
"Summarize the user's goal, what was done, current state, and any clearly supported next step.",
|
|
17
|
-
"Respond in the conversation's primary language.",
|
|
18
|
-
"Output 1-2 plain-text sentences under
|
|
17
|
+
"Respond in the conversation's primary language. Address the user in the second person.",
|
|
18
|
+
"Output 1-2 plain-text sentences under 40 words. No heading, markdown, bullets, or quotes.",
|
|
19
19
|
].join("\n");
|
|
20
20
|
|
|
21
|
-
const MAX_TOKENS =
|
|
21
|
+
const MAX_TOKENS = 80;
|
|
22
22
|
const MAX_CONVERSATION_CHARS = 8_000;
|
|
23
23
|
|
|
24
24
|
export class RecapManager {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
|
-
import { formatModel } from "
|
|
3
|
+
import { formatModel } from "../../utils/format";
|
|
4
4
|
|
|
5
5
|
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import type { OptionalModelConfig } from "
|
|
7
|
+
import type { OptionalModelConfig } from "../../config/model";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_THINKING_LEVEL: ModelThinkingLevel = "off";
|
|
10
10
|
|
|
@@ -61,3 +61,32 @@ export function sanitizeText(text: string): string {
|
|
|
61
61
|
.replace(/ +/g, " ")
|
|
62
62
|
.trim();
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/** Coerce a possibly-stringified numeric value to a finite number, or `undefined`. */
|
|
66
|
+
export function toNumber(value?: string | number | null): number | undefined {
|
|
67
|
+
if (value === undefined || value === null) return undefined;
|
|
68
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const FRANKFURTER_API = "https://api.frankfurter.dev/v2/rate";
|
|
73
|
+
|
|
74
|
+
interface FrankfurterRateResponse {
|
|
75
|
+
rate?: string | number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Convert an amount in the given currency to USD via the Frankfurter API. */
|
|
79
|
+
export async function convertToUSD(amount: number | undefined, currency: string | undefined, signal: AbortSignal): Promise<number | undefined> {
|
|
80
|
+
if (amount === undefined) return undefined;
|
|
81
|
+
if (!currency || currency === "USD") return amount;
|
|
82
|
+
|
|
83
|
+
const url = `${FRANKFURTER_API}/${encodeURIComponent(currency)}/USD`;
|
|
84
|
+
const response = await fetch(url, { headers: { Accept: "application/json" }, signal });
|
|
85
|
+
if (!response.ok) throw new Error("currency conversion failed");
|
|
86
|
+
|
|
87
|
+
const payload = (await response.json()) as FrankfurterRateResponse;
|
|
88
|
+
const rate = toNumber(payload.rate);
|
|
89
|
+
if (rate === undefined) throw new Error("currency conversion failed");
|
|
90
|
+
|
|
91
|
+
return amount * rate;
|
|
92
|
+
}
|
|
@@ -25,24 +25,6 @@ export function isUsage(value: unknown): value is Usage {
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** Sum two `Usage` values into a new one. */
|
|
29
|
-
export function addUsage(a: Usage, b: Usage): Usage {
|
|
30
|
-
return {
|
|
31
|
-
input: a.input + b.input,
|
|
32
|
-
output: a.output + b.output,
|
|
33
|
-
cacheRead: a.cacheRead + b.cacheRead,
|
|
34
|
-
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
35
|
-
totalTokens: a.totalTokens + b.totalTokens,
|
|
36
|
-
cost: {
|
|
37
|
-
input: a.cost.input + b.cost.input,
|
|
38
|
-
output: a.cost.output + b.cost.output,
|
|
39
|
-
cacheRead: a.cost.cacheRead + b.cost.cacheRead,
|
|
40
|
-
cacheWrite: a.cost.cacheWrite + b.cost.cacheWrite,
|
|
41
|
-
total: a.cost.total + b.cost.total,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
28
|
/**
|
|
47
29
|
* Extract usage from a session entry.
|
|
48
30
|
*
|
package/assets/credits.png
DELETED
|
Binary file
|
package/assets/screenshot.png
DELETED
|
Binary file
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
-
|
|
3
|
-
/** Structural type guard for the pi `Usage` shape. */
|
|
4
|
-
export function isUsage(value: unknown): value is Usage {
|
|
5
|
-
if (typeof value !== "object" || value === null) return false;
|
|
6
|
-
|
|
7
|
-
const usage = value as Record<string, unknown>;
|
|
8
|
-
const hasTokenFields =
|
|
9
|
-
typeof usage.input === "number" &&
|
|
10
|
-
typeof usage.output === "number" &&
|
|
11
|
-
typeof usage.cacheRead === "number" &&
|
|
12
|
-
typeof usage.cacheWrite === "number" &&
|
|
13
|
-
typeof usage.totalTokens === "number";
|
|
14
|
-
if (!hasTokenFields) return false;
|
|
15
|
-
|
|
16
|
-
if (typeof usage.cost !== "object" || usage.cost === null) return false;
|
|
17
|
-
const cost = usage.cost as Record<string, unknown>;
|
|
18
|
-
return (
|
|
19
|
-
typeof cost.input === "number" &&
|
|
20
|
-
typeof cost.output === "number" &&
|
|
21
|
-
typeof cost.cacheRead === "number" &&
|
|
22
|
-
typeof cost.cacheWrite === "number" &&
|
|
23
|
-
typeof cost.total === "number"
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function toNumber(value?: string | number | null): number | undefined {
|
|
28
|
-
if (value === undefined || value === null) return undefined;
|
|
29
|
-
const parsed = typeof value === "number" ? value : Number(value);
|
|
30
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const FRANKFURTER_API = "https://api.frankfurter.dev/v2/rate";
|
|
34
|
-
|
|
35
|
-
interface FrankfurterRateResponse {
|
|
36
|
-
rate?: string | number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function convertToUSD(amount: number | undefined, currency: string | undefined, signal: AbortSignal): Promise<number | undefined> {
|
|
40
|
-
if (amount === undefined) return undefined;
|
|
41
|
-
if (!currency || currency === "USD") return amount;
|
|
42
|
-
|
|
43
|
-
const url = `${FRANKFURTER_API}/${encodeURIComponent(currency)}/USD`;
|
|
44
|
-
const response = await fetch(url, { headers: { Accept: "application/json" }, signal });
|
|
45
|
-
if (!response.ok) throw new Error("currency conversion failed");
|
|
46
|
-
|
|
47
|
-
const payload = (await response.json()) as FrankfurterRateResponse;
|
|
48
|
-
const rate = toNumber(payload.rate);
|
|
49
|
-
if (rate === undefined) throw new Error("currency conversion failed");
|
|
50
|
-
|
|
51
|
-
return amount * rate;
|
|
52
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
|
-
import { creditsConfigSchema } from "../../credits/config";
|
|
4
|
-
import { editorConfigSchema } from "../../editor/config";
|
|
5
|
-
import { footerConfigSchema } from "../../footer/config";
|
|
6
|
-
import { fullscreenConfigSchema } from "../../fullscreen/config";
|
|
7
|
-
import { modelsConfigSchema } from "../../models/config";
|
|
8
|
-
import { nameConfigSchema } from "../../name/config";
|
|
9
|
-
import { presetsConfigSchema } from "../../presets/config";
|
|
10
|
-
import { recapConfigSchema } from "../../recap/config";
|
|
11
|
-
|
|
12
|
-
export const configSchemas = {
|
|
13
|
-
credits: creditsConfigSchema,
|
|
14
|
-
editor: editorConfigSchema,
|
|
15
|
-
footer: footerConfigSchema,
|
|
16
|
-
fullscreen: fullscreenConfigSchema,
|
|
17
|
-
models: modelsConfigSchema,
|
|
18
|
-
name: nameConfigSchema,
|
|
19
|
-
presets: presetsConfigSchema,
|
|
20
|
-
recap: recapConfigSchema,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type ConfigField = keyof typeof configSchemas;
|
|
24
|
-
export type ConfigValue<Field extends ConfigField> = z.infer<(typeof configSchemas)[Field]>;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|