decorated-pi 0.1.0 → 0.2.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 CHANGED
@@ -1,70 +1,46 @@
1
1
  # decorated-pi
2
2
 
3
- `decorated-pi` is a Pi extension bundle that adds safety gates, LSP tools, image/compaction model helpers, smarter `@` file completion, dynamic subdirectory `AGENTS.md` loading, and a few workflow quality-of-life improvements.
4
-
5
- ## Status
6
-
7
- Current scope is **functionally complete for local use**.
8
-
9
- Recent audit highlights:
10
- - Fixed stale LSP diagnostics caused by returning cached diagnostics after `didChange`
11
- - Fixed `subdir-agents` path capture on `tool_call`
12
- - Fixed a `smart-at` multi-token search bug
13
- - Updated safety checks so shell overwrite attempts on existing files are treated as dangerous
3
+ `decorated-pi` is a Pi extension that adds safety gates, LSP tools, image/compaction model helpers, smarter `@` file search, dynamic subdirectory `AGENTS.md` loading, and a few workflow quality-of-life improvements.
14
4
 
15
5
  ## Features
16
6
 
17
- ### 1. Decorated Pi Guidance
18
- Adds global system-prompt guidance via `before_agent_start.systemPrompt`:
19
- - Restate understanding before acting
20
- - Break medium/large tasks into discrete steps
21
-
22
- ### 2. Safety Layer
23
- Implemented in `extensions/safety.ts`.
7
+ ### 1. Safety Layer
24
8
 
25
9
  - **Dangerous bash guard**
26
- - Blocks or asks for confirmation on destructive commands such as:
10
+ - asks for confirmation on destructive commands such as:
27
11
  - `rm`
28
12
  - `sudo`
29
- - `svn commit`
30
- - `svn revert`
31
- - `git reset`
32
- - `git restore`
33
- - `git clean`
34
- - `git push`
35
- - `git revert`
36
- - **Shell overwrite detection**
37
- - Detects `bash` commands that would overwrite an **existing regular file**, including:
38
- - `>` / `>>`
39
- - `1>` / `1>>`
40
- - `2>` / `2>>`
41
- - `&>` / `&>>`
42
- - `tee`
43
- - Aggregates **all** dangerous reasons found in one command
13
+ - `svn commit/revert`
14
+ - `git reset/restore/clean/push/revert`
15
+ - `npm publish`
16
+ - `>` / `1>` / `2>` / `&>` / `tee` overwrite existing files
17
+ - Hints the agent to use `edit` instead of `write` on non-empty files
44
18
  - **Protected paths**
45
- - Blocks `write` / `edit` to sensitive locations such as `.env`, `.git/`, `.ssh/`, `node_modules/`, `*.pem`, `*.key`, etc.
46
- - **Write guard**
47
- - Blocks the `write` tool when it would overwrite a non-empty file
48
- - Instructs the model to use `edit` instead
19
+ - Blocks read/write access (via `write`/`edit`/`read` tools or `cat`/`head`/`tail`/`grep`/`rg` etc. bash commands) to sensitive locations such as `.env`, `.git/`, `.ssh/`, `*.pem`, `*.key`, etc.
49
20
  - **Secret redaction**
50
- - Uses `secretlint` rules to redact secrets from tool output before they are fed back into context
21
+ - Dual-layer detection: 40+ known-format patterns (AWS, GitHub, OpenAI, etc.) + Adjusted Shannon Entropy analysis for unknown formats. Based on [opencode-secrets-protect](https://github.com/jscheel/opencode-secrets-protect) (MIT)
22
+
23
+ ### 2. Smart `@` File Search
51
24
 
52
- ### 3. Smart `@` Completion
53
- Implemented in `extensions/smart-at.ts`.
25
+ Replaces Pi's default file search with a faster project-aware search strategy:
54
26
 
55
- Replaces Pi's default file autocomplete behavior with a faster project-aware search strategy:
56
27
  - Uses `git ls-files` in git repos
57
28
  - Falls back to `fd` outside git repos
58
29
  - Caches results for 10 seconds
59
30
  - Scores primarily on **filename match quality**, not full-path fuzziness
60
31
  - Penalizes hidden/cache/build directories
61
32
  - Hides hidden paths from empty-query results
62
- - Keeps Pi's original `applyCompletion` / `shouldTriggerFileCompletion` binding behavior intact
63
33
 
64
- ### 4. LSP Tool Suite
65
- Implemented in `extensions/lsp/`.
34
+ ### 3. LSP Tool Suite
35
+
36
+ Based on [@spences10/pi-lsp](https://github.com/spences10/my-pi/tree/main/packages/pi-lsp) by Scott Spence (MIT License), with additions:
37
+
38
+ - C/C++ (clangd) and Lua support
39
+ - `lsp_find_symbol`, `lsp_rename`, multi-file support merged into `lsp_diagnostics`
40
+ - Force-sync on `didChange` (no stale diagnostics)
66
41
 
67
42
  Registered tools:
43
+
68
44
  - `lsp_diagnostics`
69
45
  - `lsp_find_symbol`
70
46
  - `lsp_hover`
@@ -74,8 +50,8 @@ Registered tools:
74
50
  - `lsp_rename`
75
51
 
76
52
  Supported languages:
77
- - c
78
- - cpp
53
+
54
+ - c/cpp
79
55
  - go
80
56
  - java
81
57
  - lua
@@ -85,134 +61,49 @@ Supported languages:
85
61
  - svelte
86
62
  - typescript
87
63
 
88
- LSP integration includes:
89
- - prompt snippets for `Available tools`
90
- - tool-specific prompt guidelines
91
- - parameter descriptions for the JSON schema
92
- - trust checks for project-local LSP binaries
93
- - an LSP-specific system-prompt section injected only when LSP tools are active
64
+ ### 4. Auxiliary Models (Image + Compact)
94
65
 
95
- ### 5. Image Read Fallback
96
- Implemented in `extensions/extend-model.ts`.
66
+ Uses cheaper models for auxiliary tasks, configured via `/extend-model`:
97
67
 
98
- When the model reads an image file and an image model is configured:
99
- - Detects supported image types via magic bytes
100
- - Calls a configured vision-capable model
101
- - Replaces the read result with image analysis text
68
+ - **Image read fallback** — when the model reads an image file, detects type via magic bytes, calls a configured vision-capable model, and replaces the read result with image analysis text (jpeg, png, gif, webp)
69
+ - **Compact model** uses a configured model for context compaction (instead of the main model), auto-resumes after compaction.
102
70
 
103
- Supported image types:
104
- - jpeg
105
- - png
106
- - gif
107
- - webp
108
-
109
- ### 6. Custom Compact Model + Auto Resume
110
- Also implemented in `extensions/extend-model.ts`.
111
-
112
- - Supports a configured **compact model** through `session_before_compact`
113
- - Preserves auto-resume behavior through `session_compact`
114
- - Appends read/modified file summaries to compaction output
115
-
116
- ### 7. `/extend-model`
117
- Implemented in `extensions/slash.ts`.
118
-
119
- Interactive command for configuring:
120
- - image model
121
- - compact model
122
-
123
- ### 8. `/retry`
124
- Implemented in `extensions/slash.ts`.
125
-
126
- Allows continuing after interruption by:
127
- - aborting the current run if needed
128
- - sending a hidden continuation trigger
129
- - injecting a one-turn retry note into the system prompt
130
-
131
- ### 9. Dynamic Subdirectory `AGENTS.md` / `CLAUDE.md`
132
- Implemented in `extensions/subdir-agents.ts`.
71
+ ### 5. Dynamic Subdirectory `AGENTS.md` / `CLAUDE.md`
133
72
 
134
73
  When the agent reads or edits a file:
74
+
135
75
  - discovers `AGENTS.md` / `CLAUDE.md` in the file's directory and ancestor directories
136
76
  - injects newly discovered guidance into tool results
137
77
  - persists discovered files into the session so they are restored on resume
138
78
 
139
- ### 10. Automatic Session Title
140
- Implemented in `extensions/session-title.ts`.
141
-
142
- - Derives the session name from the first user message
143
- - Avoids overriding a manually assigned session name
79
+ ### 6. Extend Providers
144
80
 
145
- ## Install
81
+ Extend providers are registered via `/login` → "Use a subscription":
146
82
 
147
- ### Local install
83
+ | Provider | Base URL |
84
+ | ---------- | ----------- |
85
+ | Ollama Cloud | `ollama.com/v1` |
86
+ | Baidu Qianfan | `qianfan.baidubce.com/v2/coding` |
87
+ | ARK Coding | `ark.cn-beijing.volces.com/api/coding/v3` |
148
88
 
149
- ```bash
150
- pi install /path/to/decorated-pi
151
- ```
152
-
153
- Then reload Pi:
89
+ ## Install
154
90
 
155
91
  ```bash
156
- /reload
92
+ pi install /path/to/decorated-pi #local
93
+ pi install npm:decorated-pi #npm
94
+ pi install git:github.com/lcwecker/decorated-pi #github
157
95
  ```
158
96
 
159
- ### npm publish
160
- Not published yet.
97
+ Then reload Pi
161
98
 
162
99
  ## Configuration
163
100
 
164
101
  Runtime settings are stored in:
165
102
 
166
103
  ```text
167
- ~/.pi/agent/extensions/decorated-pi.json
168
- ```
169
-
170
- Current keys:
171
- - `imageModelKey`
172
- - `compactModelKey`
173
-
174
- Design rule:
175
- - **only** `extensions/settings.ts` writes this file
176
- - all other modules read through exported getters/setters
177
-
178
- ## Architecture
179
-
180
- Entry point:
181
- - `extensions/index.ts`
182
-
183
- Main modules:
184
- - `extensions/guidance.ts` — global system prompt guidance
185
- - `extensions/safety.ts` — command guard, protected paths, write guard, secret redaction
186
- - `extensions/smart-at.ts` — smart `@` autocomplete
187
- - `extensions/extend-model.ts` — image fallback, compact model, auto-resume
188
- - `extensions/slash.ts` — `/extend-model`, `/retry`
189
- - `extensions/subdir-agents.ts` — dynamic `AGENTS.md` / `CLAUDE.md`
190
- - `extensions/session-title.ts` — session naming
191
- - `extensions/lsp/*` — LSP client, server manager, prompt wiring, tools
192
- - `extensions/settings.ts` — config I/O
193
-
194
- ## Current Limitations
195
-
196
- - `write` protection applies to the **LLM's `write` tool**; agent attempts to overwrite files via `bash` are handled separately by the dangerous-command gate
197
- - shell overwrite detection only escalates when the target resolves to an **existing regular file**
198
- - compact behavior still relies on Pi's normal compaction flow; this extension customizes the model and post-compact continuation, not the global compaction threshold UI
199
- - npm distribution is not set up yet
200
- - there is no dedicated automated test suite yet; current validation is based on Pi runtime checks, LSP diagnostics, and manual smoke tests
201
-
202
- ## Development Notes
203
-
204
- Helpful checks during development:
205
-
206
- ```bash
207
- pi install /path/to/decorated-pi
208
- /reload
104
+ ~/.pi/agent/decorated-pi.json
209
105
  ```
210
106
 
211
- For focused validation, use:
212
- - `lsp_diagnostics` on edited source files
213
- - real `pi "..."` smoke tests for safety behavior
214
- - manual autocomplete checks for `smart-at`
215
-
216
107
  ## License
217
108
 
218
109
  MIT
@@ -387,12 +387,7 @@ export function setupExtendModel(pi: ExtensionAPI) {
387
387
  auth.apiKey ?? "", auth.headers, signal, customInstructions, previousSummary);
388
388
  }
389
389
 
390
- const readFiles = [...fileOps.read].filter(f => !fileOps.edited.has(f) && !fileOps.written.has(f)).sort();
391
- const modifiedFiles = [...new Set([...fileOps.edited, ...fileOps.written])].sort();
392
- if (readFiles.length > 0) summary += `\n\n<read-files>\n${readFiles.join("\n")}\n</read-files>`;
393
- if (modifiedFiles.length > 0) summary += `\n\n<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`;
394
-
395
- return { compaction: { summary, firstKeptEntryId, tokensBefore, details: { readFiles, modifiedFiles } } };
390
+ return { compaction: { summary, firstKeptEntryId, tokensBefore } };
396
391
  } catch (err) {
397
392
  if (signal.aborted) return;
398
393
  ctx.ui.notify(`Compact failed: ${err instanceof Error ? err.message : err}`, "error");
@@ -10,6 +10,7 @@ import { setupSubdirAgents } from "./subdir-agents";
10
10
  import { setupSessionTitle } from "./session-title";
11
11
  import { setupGuidance } from "./guidance";
12
12
  import { setupLsp } from "./lsp/index";
13
+ import { setupProviders } from "./providers/index";
13
14
  import { setupSmartAt } from "./smart-at";
14
15
 
15
16
  export default function (pi: ExtensionAPI) {
@@ -20,5 +21,6 @@ export default function (pi: ExtensionAPI) {
20
21
  setupSessionTitle(pi);
21
22
  setupGuidance(pi);
22
23
  setupLsp(pi);
24
+ setupProviders(pi);
23
25
  setupSmartAt(pi);
24
26
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * LSP Client — JSON-RPC over stdio transport
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ *
7
+ * Modifications: added rename() method, simplified type exports
8
+ */
1
9
  import { spawn, ChildProcess } from "node:child_process";
2
10
  import { EventEmitter } from "node:events";
3
11
  import { pathToFileURL } from "node:url";
@@ -204,7 +212,10 @@ export class LspClient extends EventEmitter {
204
212
  async ensure_document_open(uri: string, text: string): Promise<void> {
205
213
  const existing = this.#open_docs.get(uri);
206
214
  if (existing) {
207
- if (existing.text === text) return;
215
+ // Always force-sync: the file may have been modified externally
216
+ // (e.g. by pi's edit/write tools), and even if text matches the
217
+ // cached version, diagnostics may be stale due to changes in
218
+ // other files. Never skip the sync based on text comparison.
208
219
  const next_version = existing.version + 1;
209
220
  this.#open_docs.set(uri, { version: next_version, text });
210
221
  this.#diagnostics_by_uri.delete(uri);
@@ -1,3 +1,9 @@
1
+ /**
2
+ * LSP Child Process Environment — restricted env for LSP server processes
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ */
1
7
  import { create_child_process_env as create_shared_child_process_env } from "@spences10/pi-child-env";
2
8
 
3
9
  export function create_child_process_env(
@@ -1,3 +1,9 @@
1
+ /**
2
+ * LSP Output Formatting — diagnostics, hover, locations, symbols
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ */
1
7
  import { fileURLToPath } from "node:url";
2
8
  import type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./client.js";
3
9
  import { LspClientStartError } from "./client.js";
@@ -1,3 +1,9 @@
1
+ /**
2
+ * LSP Extension Entry Point
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ */
1
7
  import { LspServerManager } from "./server-manager.js";
2
8
  import { register_lsp_tools } from "./tools.js";
3
9
  import { setup_lsp_prompt } from "./prompt.js";
@@ -1,3 +1,9 @@
1
+ /**
2
+ * LSP System Prompt — injects LSP guidance into agent system prompt
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ */
1
7
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
8
  import { list_supported_languages } from "./servers.js";
3
9
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * LSP Server Manager — lifecycle, per-language instances, project-local trust
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ */
1
7
  import { resolve_project_trust } from "@spences10/pi-project-trust";
2
8
  import { readFile } from "node:fs/promises";
3
9
  import { isAbsolute, resolve } from "node:path";
@@ -1,3 +1,11 @@
1
+ /**
2
+ * LSP Server Config — language detection, server commands, workspace roots
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ *
7
+ * Modifications: added C/C++ (clangd) and Lua support
8
+ */
1
9
  import { existsSync } from "node:fs";
2
10
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
3
11
 
@@ -31,7 +39,7 @@ const EXTENSION_LANGUAGES: Record<string, string> = {
31
39
  ".svelte": "svelte",
32
40
  };
33
41
 
34
- type LanguageConfig = {
42
+ export type LanguageConfig = {
35
43
  language: string;
36
44
  command: string;
37
45
  args: string[];
@@ -1,3 +1,11 @@
1
+ /**
2
+ * LSP Tool Definitions — diagnostics, hover, definition, references, symbols, rename
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ *
7
+ * Modifications: added lsp_find_symbol, lsp_rename, multi-file lsp_diagnostics
8
+ */
1
9
  import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
10
  import { Type } from "typebox";
3
11
  import { list_supported_languages } from "./servers.js";
@@ -1,3 +1,9 @@
1
+ /**
2
+ * LSP Binary Trust Store — project-local binary trust management
3
+ *
4
+ * Based on @spences10/pi-lsp by Scott Spence
5
+ * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
+ */
1
7
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
8
  import {
3
9
  is_project_subject_trusted,
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ARK Coding Plan — OAuth/subscription provider with hardcoded models
3
+ *
4
+ * Provider: "ark-coding"
5
+ * Base URL: https://ark.cn-beijing.volces.com/api/coding/v3 (OpenAI-compatible)
6
+ * Auth: OAuth/subscription login → prompt for API key
7
+ *
8
+ * All models hardcoded. No dynamic fetching, no config file caching.
9
+ * - No auth → no models in /model (clean UX, via hasConfiguredAuth)
10
+ * - Login → models become available immediately
11
+ * - Startup → models registered unconditionally (hardcoded)
12
+ */
13
+
14
+ import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
15
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
16
+
17
+ const PROVIDER_ID = "ark-coding";
18
+ const PROVIDER_DISPLAY_NAME = "ARK Coding Plan";
19
+ const BASE_URL = "https://ark.cn-beijing.volces.com/api/coding/v3";
20
+
21
+ // ── Hardcoded models (parameters from models.dev) ─────────────────────────
22
+
23
+ const MODELS: ProviderModelConfig[] = [
24
+ { id: "deepseek-v3.2", name: "DeepSeek V3.2", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 163_840, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
25
+ { id: "glm-4.7", name: "GLM 4.7", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
26
+ { id: "glm-5.1", name: "GLM 5.1", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
27
+ { id: "kimi-k2.5", name: "Kimi K2.5", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
28
+ { id: "kimi-k2.6", name: "Kimi K2.6", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
29
+ { id: "minimax-m2.5", name: "MiniMax M2.5", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204_800, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
30
+ { id: "minimax-m2.7", name: "MiniMax M2.7", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 196_608, maxTokens: 196_608, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
31
+ { id: "doubao-seed-2.0-code", name: "Doubao Seed 2.0 Code", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
32
+ { id: "doubao-seed-2.0-pro", name: "Doubao Seed 2.0 Pro", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
33
+ { id: "doubao-seed-2.0-lite", name: "Doubao Seed 2.0 Lite", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 32_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
34
+ { id: "doubao-seed-code", name: "Doubao Seed Code", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 256_000, maxTokens: 16_384, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
35
+ ];
36
+
37
+ // ── Entry ──────────────────────────────────────────────────────────────────
38
+
39
+ export function setupArkCoding(pi: ExtensionAPI) {
40
+ pi.registerProvider(PROVIDER_ID, {
41
+ name: PROVIDER_DISPLAY_NAME,
42
+ baseUrl: BASE_URL,
43
+ api: "openai-completions",
44
+ authHeader: true,
45
+ models: MODELS,
46
+ oauth: {
47
+ name: PROVIDER_DISPLAY_NAME,
48
+
49
+ async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
50
+ const apiKey = (await callbacks.onPrompt({
51
+ message: "Enter ARK Coding Plan API key:",
52
+ placeholder: "your-api-key",
53
+ })).trim();
54
+
55
+ if (!apiKey) throw new Error("API key cannot be empty.");
56
+
57
+ return {
58
+ refresh: apiKey,
59
+ access: apiKey,
60
+ expires: Date.now() + 1000 * 365.24 * 24 * 3600 * 1000, // ~1000 years
61
+ };
62
+ },
63
+
64
+ refreshToken(cred: OAuthCredentials): Promise<OAuthCredentials> {
65
+ return Promise.resolve(cred); // API key never expires
66
+ },
67
+
68
+ getApiKey(cred: OAuthCredentials): string {
69
+ return cred.access;
70
+ },
71
+ },
72
+ });
73
+ }
@@ -0,0 +1,9 @@
1
+ import { setupArkCoding } from "./ark-coding.js";
2
+ import { setupOllamaCloud } from "./ollama-cloud.js";
3
+ import { setupQianfanCoding } from "./qianfan-coding.js";
4
+
5
+ export function setupProviders(pi: any) {
6
+ setupArkCoding(pi);
7
+ setupOllamaCloud(pi);
8
+ setupQianfanCoding(pi);
9
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Ollama Cloud — OAuth/subscription provider with hardcoded models
3
+ *
4
+ * Provider: "ollama-cloud"
5
+ * Base URL: https://ollama.com/v1 (OpenAI-compatible)
6
+ * Auth: OAuth/subscription login → prompt for API key
7
+ *
8
+ * All models hardcoded from models.dev. No dynamic fetching, no config file caching.
9
+ * - No auth → no models in /model (clean UX, via hasConfiguredAuth)
10
+ * - Login → models become available immediately
11
+ * - Startup → models registered unconditionally (hardcoded)
12
+ */
13
+
14
+ import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
15
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
16
+
17
+ const PROVIDER_ID = "ollama-cloud";
18
+ const PROVIDER_DISPLAY_NAME = "Ollama Cloud";
19
+ const BASE_URL = "https://ollama.com/v1";
20
+
21
+ // ── Hardcoded models (from models.dev) ────────────────────────────────────
22
+
23
+ const MODELS: ProviderModelConfig[] = [
24
+ { id: "cogito-2.1:671b", name: "cogito-2.1:671b", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 163_840, maxTokens: 32_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
25
+ { id: "deepseek-v3.1:671b", name: "deepseek-v3.1:671b", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 163_840, maxTokens: 163_840, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
26
+ { id: "deepseek-v3.2", name: "deepseek-v3.2", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 163_840, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
27
+ { id: "deepseek-v4-flash", name: "deepseek-v4-flash", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 1_048_576, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
28
+ { id: "deepseek-v4-pro", name: "deepseek-v4-pro", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 1_048_576, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
29
+ { id: "devstral-2:123b", name: "devstral-2:123b", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
30
+ { id: "devstral-small-2:24b", name: "devstral-small-2:24b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
31
+ { id: "gemini-3-flash-preview", name: "gemini-3-flash-preview", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
32
+ { id: "gemma3:12b", name: "gemma3:12b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 131_072, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
33
+ { id: "gemma3:27b", name: "gemma3:27b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 131_072, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
34
+ { id: "gemma3:4b", name: "gemma3:4b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 131_072, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
35
+ { id: "gemma4:31b", name: "gemma4:31b", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
36
+ { id: "glm-4.6", name: "glm-4.6", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
37
+ { id: "glm-4.7", name: "glm-4.7", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
38
+ { id: "glm-5", name: "glm-5", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
39
+ { id: "glm-5.1", name: "glm-5.1", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202_752, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
40
+ { id: "gpt-oss:120b", name: "gpt-oss:120b", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 131_072, maxTokens: 32_768, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
41
+ { id: "gpt-oss:20b", name: "gpt-oss:20b", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 131_072, maxTokens: 32_768, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
42
+ { id: "kimi-k2-thinking", name: "kimi-k2-thinking", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
43
+ { id: "kimi-k2.5", name: "kimi-k2.5", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
44
+ { id: "kimi-k2.6", name: "kimi-k2.6", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
45
+ { id: "kimi-k2:1t", name: "kimi-k2:1t", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
46
+ { id: "minimax-m2", name: "minimax-m2", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204_800, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
47
+ { id: "minimax-m2.1", name: "minimax-m2.1", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204_800, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
48
+ { id: "minimax-m2.5", name: "minimax-m2.5", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204_800, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
49
+ { id: "minimax-m2.7", name: "minimax-m2.7", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 196_608, maxTokens: 196_608, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
50
+ { id: "ministral-3:14b", name: "ministral-3:14b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
51
+ { id: "ministral-3:3b", name: "ministral-3:3b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
52
+ { id: "ministral-3:8b", name: "ministral-3:8b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 128_000, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
53
+ { id: "mistral-large-3:675b", name: "mistral-large-3:675b", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 262_144, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
54
+ { id: "nemotron-3-nano:30b", name: "nemotron-3-nano:30b", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
55
+ { id: "nemotron-3-super", name: "nemotron-3-super", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
56
+ { id: "qwen3-coder-next", name: "qwen3-coder-next", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
57
+ { id: "qwen3-coder:480b", name: "qwen3-coder:480b", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
58
+ { id: "qwen3-next:80b", name: "qwen3-next:80b", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 32_768, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
59
+ { id: "qwen3-vl:235b", name: "qwen3-vl:235b", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 32_768, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
60
+ { id: "qwen3-vl:235b-instruct", name: "qwen3-vl:235b-instruct", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 131_072, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
61
+ { id: "qwen3.5:397b", name: "qwen3.5:397b", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262_144, maxTokens: 65_536, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
62
+ { id: "rnj-1:8b", name: "rnj-1:8b", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32_768, maxTokens: 4_096, compat: { supportsDeveloperRole: false, supportsReasoningEffort: true } as any },
63
+ ];
64
+
65
+ // ── Entry ──────────────────────────────────────────────────────────────────
66
+
67
+ export function setupOllamaCloud(pi: ExtensionAPI) {
68
+ pi.registerProvider(PROVIDER_ID, {
69
+ name: PROVIDER_DISPLAY_NAME,
70
+ baseUrl: BASE_URL,
71
+ api: "openai-completions",
72
+ authHeader: true,
73
+ models: MODELS,
74
+ oauth: {
75
+ name: PROVIDER_DISPLAY_NAME,
76
+
77
+ async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
78
+ const apiKey = (await callbacks.onPrompt({
79
+ message: "Enter Ollama Cloud API key:",
80
+ placeholder: "your-api-key",
81
+ })).trim();
82
+
83
+ if (!apiKey) throw new Error("API key cannot be empty.");
84
+
85
+ return {
86
+ refresh: apiKey,
87
+ access: apiKey,
88
+ expires: Date.now() + 1000 * 365.24 * 24 * 3600 * 1000, // ~1000 years
89
+ };
90
+ },
91
+
92
+ refreshToken(cred: OAuthCredentials): Promise<OAuthCredentials> {
93
+ return Promise.resolve(cred); // API key never expires
94
+ },
95
+
96
+ getApiKey(cred: OAuthCredentials): string {
97
+ return cred.access;
98
+ },
99
+ },
100
+ });
101
+ }