@nmzpy/pi-ember-stack 0.1.6 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nmzpy/pi-ember-stack",
3
- "version": "0.1.6",
4
- "description": "Ember's configurable pi plugin stack with modes, questionnaire, compact edits, subagents, and Devin auth",
3
+ "version": "0.2.1",
4
+ "description": "Ember's configurable pi plugin stack with modes, questionnaire, compact edits, subagents, and Devin auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -25,11 +25,19 @@
25
25
  "tsconfig.json",
26
26
  "plugins"
27
27
  ],
28
+ "exports": {
29
+ ".": "./plugins/index.ts",
30
+ "./renderer": "./plugins/pi-compact-tools/renderer.ts"
31
+ },
28
32
  "pi": {
29
33
  "extensions": [
30
34
  "./plugins/index.ts"
31
35
  ]
32
36
  },
37
+ "dependencies": {
38
+ "@ff-labs/fff-node": "0.9.6",
39
+ "@sinclair/typebox": "^0.34.0"
40
+ },
33
41
  "peerDependencies": {
34
42
  "@earendil-works/pi-ai": "*",
35
43
  "@earendil-works/pi-agent-core": "*",
@@ -42,6 +50,8 @@
42
50
  "@earendil-works/pi-agent-core": "^0.80.6",
43
51
  "@earendil-works/pi-coding-agent": "^0.80.6",
44
52
  "@earendil-works/pi-tui": "^0.80.6",
53
+ "@ff-labs/fff-node": "0.9.6",
54
+ "@sinclair/typebox": "^0.34.0",
45
55
  "@types/node": "^20.19.43",
46
56
  "typebox": "^1.3.1",
47
57
  "typescript": "^5.9.3"
@@ -5,7 +5,7 @@
5
5
  * - OAuth login via Windsurf's browser sign-in flow (`loginDevin`)
6
6
  * - A no-op token refresh (Windsurf api_keys are long-lived)
7
7
  * - Live model catalog fetch from Cognition's GetCascadeModelConfigs
8
- * after login, filtered to 11 wanted model families
8
+ * after login, surfacing every model the account has access to
9
9
  * - `/devin-refresh` command to manually re-fetch the catalog
10
10
  * - `/devin-status` command to check auth state
11
11
  * - `session_start` auto-fetch when already logged in
@@ -15,7 +15,8 @@
15
15
  * and auth are handled internally via the OAuth-issued api_key.
16
16
  */
17
17
 
18
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
18
+ import type { ExtensionAPI, ProviderModelConfig } from '@earendil-works/pi-coding-agent';
19
+ import { AuthStorage } from '@earendil-works/pi-coding-agent';
19
20
  import type {
20
21
  Api,
21
22
  Model,
@@ -24,7 +25,7 @@ import type {
24
25
  } from '@earendil-works/pi-ai';
25
26
  import { streamDevin } from '../src/stream.js';
26
27
  import { loginDevin } from '../src/oauth/login.js';
27
- import { buildLiveModels, FALLBACK_MODELS, DEFAULT_HOST } from '../src/models.js';
28
+ import { buildLiveModels, DEFAULT_HOST } from '../src/models.js';
28
29
  import { getCachedCatalog, clearCachedCatalog } from '../src/cloud-direct/catalog.js';
29
30
  import { DEFAULT_REGION } from '../src/oauth/types.js';
30
31
 
@@ -38,7 +39,34 @@ const PLACEHOLDER_BASE_URL = DEFAULT_HOST;
38
39
 
39
40
  let _pi: ExtensionAPI | null = null;
40
41
 
41
- function registerDevinProvider(pi: ExtensionAPI, models: typeof FALLBACK_MODELS): void {
42
+ /**
43
+ * Fetch the live catalog using credentials already in auth.json and register
44
+ * the resulting models. Called during the awaited factory load so devin
45
+ * models exist before pi flushes pending provider registrations and restores
46
+ * the session model.
47
+ *
48
+ * Reads auth.json directly via AuthStorage (the model registry is not bound
49
+ * yet during factory load). Silently no-ops when not signed in or the fetch
50
+ * fails — the session_start handler re-attempts once the registry is live.
51
+ */
52
+ async function primeCatalogFromStoredAuth(): Promise<void> {
53
+ if (!_pi) return;
54
+ try {
55
+ const authStorage = AuthStorage.create();
56
+ const apiKey = await authStorage.getApiKey(PROVIDER_ID, { includeFallback: false });
57
+ if (!apiKey) return;
58
+ const catalog = await getCachedCatalog(apiKey, DEFAULT_HOST);
59
+ const liveModels = buildLiveModels(catalog);
60
+ if (liveModels.length > 0) {
61
+ registerDevinProvider(_pi, liveModels);
62
+ }
63
+ } catch {
64
+ // Not signed in, network failure, or schema drift — keep the empty
65
+ // model list and let session_start retry once the registry is bound.
66
+ }
67
+ }
68
+
69
+ function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]): void {
42
70
  pi.registerProvider(PROVIDER_ID, {
43
71
  name: PROVIDER_NAME,
44
72
  api: API_IDENTIFIER,
@@ -58,7 +86,7 @@ function registerDevinProvider(pi: ExtensionAPI, models: typeof FALLBACK_MODELS)
58
86
  const liveModels = buildLiveModels(catalog);
59
87
  registerDevinProvider(_pi, liveModels);
60
88
  } catch {
61
- // keep static models if catalog fetch fails
89
+ // keep current models if catalog fetch fails
62
90
  }
63
91
  }
64
92
  return credentials;
@@ -80,9 +108,19 @@ function registerDevinProvider(pi: ExtensionAPI, models: typeof FALLBACK_MODELS)
80
108
  export default async function (pi: ExtensionAPI): Promise<void> {
81
109
  _pi = pi;
82
110
 
83
- registerDevinProvider(pi, FALLBACK_MODELS);
111
+ // Register with an empty model list first so the provider (and OAuth
112
+ // login support) is known even before the catalog arrives. Then, if we
113
+ // already have credentials in auth.json, fetch the live catalog now —
114
+ // during the awaited factory load, before pi flushes pending provider
115
+ // registrations and restores the session model. This is what makes
116
+ // `devin/glm-5-2` resolvable on resume instead of falling back to the
117
+ // default provider with a "Could not restore model" warning.
118
+ registerDevinProvider(pi, []);
119
+ await primeCatalogFromStoredAuth();
84
120
 
85
121
  pi.on('session_start', async (_event, ctx) => {
122
+ // Re-prime in case credentials were added via /login since load, or
123
+ // the catalog TTL expired during a long-lived session.
86
124
  try {
87
125
  const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
88
126
  if (apiKey && _pi) {
@@ -91,7 +129,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
91
129
  registerDevinProvider(_pi, liveModels);
92
130
  }
93
131
  } catch {
94
- // keep static models
132
+ // keep current models
95
133
  }
96
134
  });
97
135
 
@@ -1,18 +1,13 @@
1
1
  /**
2
2
  * Model list for the Devin (Cognition) provider.
3
3
  *
4
- * Two paths:
5
- * 1. {@link FALLBACK_MODELS} 11 static entries shown before login or
6
- * when the live catalog fetch fails. Carries real pricing.
7
- * 2. {@link buildLiveModels} filters the live `GetCascadeModelConfigs`
8
- * catalog to the 11 model families we care about, stamping each entry
9
- * with pricing/metadata from {@link MODEL_OVERRIDES}.
4
+ * Fully live-catalog driven. {@link buildLiveModels} pulls every model from
5
+ * Cognition's `GetCascadeModelConfigs` catalog and surfaces it directly
6
+ * no hardcoded family list, no hardcoded pricing/metadata. The catalog's
7
+ * `label` is used as the display name (with optional overrides below).
10
8
  *
11
- * The catalog only carries `modelUid`, `label`, and `disabled` it does
12
- * NOT include context window, max output tokens, reasoning capability, or
13
- * pricing. We supply all of those from {@link MODEL_OVERRIDES}, keyed by
14
- * prefix so variants (e.g. `claude-opus-4-8-medium`, `gpt-5-6-sol-low`)
15
- * inherit the correct metadata from their parent family.
9
+ * Before login (or if the catalog fetch fails) we return an empty list so
10
+ * pi shows no models until the live catalog arrives.
16
11
  */
17
12
 
18
13
  import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent';
@@ -22,212 +17,63 @@ import type { CacheEntry } from './cloud-direct/index.js';
22
17
  const DEFAULT_HOST = 'https://server.codeium.com';
23
18
 
24
19
  /**
25
- * Prefixes of the 11 model families we want from the live catalog.
26
- * Prefix matching catches variants (e.g. `swe-1-7-lightning`,
27
- * `gpt-5-6-sol-low`, `claude-opus-4-8-high`).
28
- */
29
- export const WANTED_PREFIXES: readonly string[] = [
30
- 'swe-1-7',
31
- 'gpt-5-6-sol',
32
- 'gpt-5-6-luna',
33
- 'gpt-5-6-terra',
34
- 'claude-opus-4-8',
35
- 'claude-fable-5',
36
- 'claude-sonnet-5',
37
- 'glm-5-2',
38
- 'kimi-k2-7',
39
- 'grok-4-5',
40
- ];
41
-
42
- function matchesWantedPrefix(uid: string): boolean {
43
- for (const prefix of WANTED_PREFIXES) {
44
- if (uid === prefix || uid.startsWith(prefix + '-') || uid.startsWith(prefix + '_')) {
45
- return true;
46
- }
47
- }
48
- return false;
49
- }
50
-
51
- /**
52
- * Per-model metadata + pricing, keyed by prefix.
20
+ * Per-family variant allow-list. If a family prefix is present, only UIDs
21
+ * whose suffix (the part after `prefix-`) is in the set are kept. A UID that
22
+ * exactly equals the prefix is always allowed (suffix = empty string).
53
23
  *
54
- * All prices are per million tokens (USD). Used by both
55
- * {@link FALLBACK_MODELS} and {@link buildLiveModels} so there is a single
56
- * source of truth for pricing.
24
+ * Families not listed here keep all variants.
57
25
  */
58
- interface ModelMeta {
59
- contextWindow: number;
60
- maxTokens: number;
61
- reasoning: boolean;
62
- input: ('text' | 'image')[];
63
- cost: {
64
- input: number;
65
- output: number;
66
- cacheRead: number;
67
- cacheWrite: number;
68
- };
69
- }
70
-
71
- const MODEL_META: Map<string, ModelMeta> = new Map([
72
- ['swe-1-7', {
73
- contextWindow: 256_000,
74
- maxTokens: 128_000,
75
- reasoning: true,
76
- input: ['text', 'image'],
77
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
78
- }],
79
- ['swe-1-7-lightning', {
80
- contextWindow: 256_000,
81
- maxTokens: 128_000,
82
- reasoning: true,
83
- input: ['text', 'image'],
84
- cost: { input: 2.50, output: 12.50, cacheRead: 0.25, cacheWrite: 3.13 },
85
- }],
86
- ['gpt-5-6-sol', {
87
- contextWindow: 1_050_000,
88
- maxTokens: 128_000,
89
- reasoning: true,
90
- input: ['text', 'image'],
91
- cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
92
- }],
93
- ['gpt-5-6-luna', {
94
- contextWindow: 1_050_000,
95
- maxTokens: 128_000,
96
- reasoning: true,
97
- input: ['text', 'image'],
98
- cost: { input: 1.00, output: 6.00, cacheRead: 0.10, cacheWrite: 1.25 },
99
- }],
100
- ['gpt-5-6-terra', {
101
- contextWindow: 1_050_000,
102
- maxTokens: 128_000,
103
- reasoning: true,
104
- input: ['text', 'image'],
105
- cost: { input: 2.50, output: 15.00, cacheRead: 0.25, cacheWrite: 3.13 },
106
- }],
107
- ['claude-opus-4-8', {
108
- contextWindow: 200_000,
109
- maxTokens: 128_000,
110
- reasoning: true,
111
- input: ['text', 'image'],
112
- cost: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
113
- }],
114
- ['claude-fable-5', {
115
- contextWindow: 1_000_000,
116
- maxTokens: 128_000,
117
- reasoning: true,
118
- input: ['text', 'image'],
119
- cost: { input: 10.00, output: 50.00, cacheRead: 1.00, cacheWrite: 12.50 },
120
- }],
121
- ['claude-sonnet-5', {
122
- contextWindow: 200_000,
123
- maxTokens: 64_000,
124
- reasoning: true,
125
- input: ['text', 'image'],
126
- cost: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
127
- }],
128
- ['glm-5-2', {
129
- contextWindow: 1_000_000,
130
- maxTokens: 131_000,
131
- reasoning: true,
132
- input: ['text', 'image'],
133
- cost: { input: 0.70, output: 2.20, cacheRead: 0.26, cacheWrite: 0.88 },
134
- }],
135
- ['kimi-k2-7', {
136
- contextWindow: 256_000,
137
- maxTokens: 256_000,
138
- reasoning: true,
139
- input: ['text', 'image'],
140
- cost: { input: 0.95, output: 4.00, cacheRead: 0.19, cacheWrite: 1.19 },
141
- }],
142
- ['grok-4-5', {
143
- contextWindow: 500_000,
144
- maxTokens: 128_000,
145
- reasoning: true,
146
- input: ['text', 'image'],
147
- cost: { input: 2.00, output: 6.00, cacheRead: 0.50, cacheWrite: 2.50 },
148
- }],
26
+ const VARIANT_ALLOW: Map<string, Set<string>> = new Map([
27
+ ['glm-5-2', new Set(['high', 'max'])],
149
28
  ]);
150
29
 
151
- /**
152
- * Find the best-matching metadata for a UID by longest-prefix match
153
- * against {@link MODEL_META}. This lets `swe-1-7-lightning` match the
154
- * `swe-1-7-lightning` entry (specific) and `gpt-5-6-sol-low` match the
155
- * `gpt-5-6-sol` entry (parent family).
156
- */
157
- function findMeta(uid: string): ModelMeta | undefined {
158
- let best: { key: string; meta: ModelMeta } | null = null;
159
- for (const [key, meta] of MODEL_META) {
160
- if (uid === key || uid.startsWith(key + '-') || uid.startsWith(key + '_')) {
161
- if (!best || key.length > best.key.length) {
162
- best = { key, meta };
30
+ function matchesVariantFilter(uid: string): boolean {
31
+ let bestPrefix: string | null = null;
32
+ for (const prefix of VARIANT_ALLOW.keys()) {
33
+ if (uid === prefix || uid.startsWith(prefix + '-') || uid.startsWith(prefix + '_')) {
34
+ if (bestPrefix === null || prefix.length > bestPrefix.length) {
35
+ bestPrefix = prefix;
163
36
  }
164
37
  }
165
38
  }
166
- return best?.meta;
167
- }
168
-
169
- /** Conservative defaults for catalog UIDs not in the override table. */
170
- const DEFAULT_META: ModelMeta = {
171
- contextWindow: 256_000,
172
- maxTokens: 128_000,
173
- reasoning: true,
174
- input: ['text', 'image'],
175
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
176
- };
177
-
178
- function makeModel(id: string, name: string, meta: ModelMeta): ProviderModelConfig {
179
- return {
180
- id,
181
- name,
182
- reasoning: meta.reasoning,
183
- input: meta.input,
184
- cost: meta.cost,
185
- contextWindow: meta.contextWindow,
186
- maxTokens: meta.maxTokens,
187
- };
39
+ if (bestPrefix === null) return true;
40
+ if (uid === bestPrefix) return true;
41
+ const suffix = uid.slice(bestPrefix.length + 1);
42
+ return VARIANT_ALLOW.get(bestPrefix)!.has(suffix);
188
43
  }
189
44
 
190
- /**
191
- * Static fallback list — 11 models with real pricing.
192
- * Shown before login or when the live catalog fetch fails.
193
- */
194
- export const FALLBACK_MODELS: ProviderModelConfig[] = [
195
- makeModel('swe-1-7', 'SWE-1.7', MODEL_META.get('swe-1-7')!),
196
- makeModel('swe-1-7-lightning', 'SWE-1.7 Lightning', MODEL_META.get('swe-1-7-lightning')!),
197
- makeModel('gpt-5-6-sol', 'GPT-5.6 Sol', MODEL_META.get('gpt-5-6-sol')!),
198
- makeModel('gpt-5-6-luna', 'GPT-5.6 Luna', MODEL_META.get('gpt-5-6-luna')!),
199
- makeModel('gpt-5-6-terra', 'GPT-5.6 Terra', MODEL_META.get('gpt-5-6-terra')!),
200
- makeModel('claude-opus-4-8', 'Claude Opus 4.8', MODEL_META.get('claude-opus-4-8')!),
201
- makeModel('claude-fable-5', 'Claude Fable 5', MODEL_META.get('claude-fable-5')!),
202
- makeModel('claude-sonnet-5', 'Claude Sonnet 5', MODEL_META.get('claude-sonnet-5')!),
203
- makeModel('glm-5-2', 'GLM-5.2', MODEL_META.get('glm-5-2')!),
204
- makeModel('kimi-k2-7', 'Kimi K2.7', MODEL_META.get('kimi-k2-7')!),
205
- makeModel('grok-4-5', 'Grok 4.5', MODEL_META.get('grok-4-5')!),
206
- ];
45
+ /** Display-name overrides for catalog UIDs whose live label is undesired. */
46
+ const NAME_OVERRIDES = new Map<string, string>([
47
+ ['glm-5-2', 'GLM-5.2'],
48
+ ]);
207
49
 
208
50
  /**
209
51
  * Build the model list from a live catalog response.
210
52
  *
211
- * Filters to the 11 wanted families via {@link WANTED_PREFIXES}, skips
212
- * disabled entries, and stamps each with pricing/metadata from
213
- * {@link MODEL_META}. Falls back to {@link FALLBACK_MODELS} when the
214
- * catalog is null, empty, or contains none of our wanted models.
53
+ * Surfaces every non-disabled model the catalog returns, applying the variant
54
+ * filter and name overrides above. Returns an empty array before login or
55
+ * when the catalog fetch fails pi will show no models until the live list
56
+ * arrives.
215
57
  */
216
58
  export function buildLiveModels(catalog: CacheEntry | null): ProviderModelConfig[] {
217
59
  if (!catalog || catalog.byUid.size === 0) {
218
- return FALLBACK_MODELS;
60
+ return [];
219
61
  }
220
62
 
221
63
  const models: ProviderModelConfig[] = [];
222
64
  for (const entry of catalog.byUid.values()) {
223
65
  if (entry.disabled) continue;
224
- if (!matchesWantedPrefix(entry.modelUid)) continue;
225
- const meta = findMeta(entry.modelUid) ?? DEFAULT_META;
226
- models.push(makeModel(entry.modelUid, entry.label || entry.modelUid, meta));
227
- }
228
-
229
- if (models.length === 0) {
230
- return FALLBACK_MODELS;
66
+ if (!matchesVariantFilter(entry.modelUid)) continue;
67
+ const name = NAME_OVERRIDES.get(entry.modelUid) ?? entry.label ?? entry.modelUid;
68
+ models.push({
69
+ id: entry.modelUid,
70
+ name,
71
+ reasoning: true,
72
+ input: ['text', 'image'],
73
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
74
+ contextWindow: 256_000,
75
+ maxTokens: 128_000,
76
+ });
231
77
  }
232
78
 
233
79
  return models;
package/plugins/index.ts CHANGED
@@ -3,10 +3,15 @@ import * as path from "node:path";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  import devinAuthPlugin from "./devin-auth/extensions/index.ts";
6
- import piCompactToolsPlugin from "./pi-compact-tools/index.ts";
6
+ import piCompactToolsPlugin, { getSharedRenderer } from "./pi-compact-tools/index.ts";
7
7
  import piCustomAgentsPlugin from "./pi-custom-agents/index.ts";
8
+ import piEmberFffPlugin from "./pi-ember-fff/index.ts";
9
+ import piEmberTpsPlugin from "./pi-ember-tps/index.ts";
10
+ import piEmberUiPlugin from "./pi-ember-ui/index.ts";
8
11
 
9
- type PluginId = "pi-compact-tools" | "pi-custom-agents" | "devin-auth";
12
+ export { getSharedRenderer };
13
+
14
+ type PluginId = "pi-compact-tools" | "pi-custom-agents" | "devin-auth" | "pi-ember-fff" | "pi-ember-ui" | "pi-ember-tps";
10
15
  type StackPlugin = {
11
16
  id: PluginId;
12
17
  description: string;
@@ -20,8 +25,11 @@ type StackPluginConfig = {
20
25
  const CONFIG_RELATIVE_PATH = path.join(".pi", "ember-stack.json");
21
26
  const DEFAULT_PLUGIN_IDS: readonly PluginId[] = [
22
27
  "pi-compact-tools",
23
- "pi-custom-agents",
24
28
  "devin-auth",
29
+ "pi-custom-agents",
30
+ "pi-ember-fff",
31
+ "pi-ember-ui",
32
+ "pi-ember-tps",
25
33
  ];
26
34
 
27
35
  const PLUGINS: readonly StackPlugin[] = [
@@ -30,15 +38,30 @@ const PLUGINS: readonly StackPlugin[] = [
30
38
  description: "Collapsed native edit rendering",
31
39
  extension: piCompactToolsPlugin,
32
40
  },
41
+ {
42
+ id: "devin-auth",
43
+ description: "Devin OAuth provider, model catalog, and streaming transport",
44
+ extension: devinAuthPlugin,
45
+ },
33
46
  {
34
47
  id: "pi-custom-agents",
35
48
  description: "Questionnaire, primary modes, plans, subagents, and bundled agent definitions",
36
49
  extension: piCustomAgentsPlugin,
37
50
  },
38
51
  {
39
- id: "devin-auth",
40
- description: "Devin OAuth provider, model catalog, and streaming transport",
41
- extension: devinAuthPlugin,
52
+ id: "pi-ember-fff",
53
+ description: "FFF-powered grep and find with compact rendering",
54
+ extension: piEmberFffPlugin,
55
+ },
56
+ {
57
+ id: "pi-ember-ui",
58
+ description: "Ember accent theme — orange reasoning colors, accent borders",
59
+ extension: piEmberUiPlugin,
60
+ },
61
+ {
62
+ id: "pi-ember-tps",
63
+ description: "Tokens-per-second meter with sparkline trend and live gauge",
64
+ extension: piEmberTpsPlugin,
42
65
  },
43
66
  ];
44
67