@crouton-kit/crouter 0.3.70 → 0.3.79
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 +1 -1
- package/dist/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
|
@@ -1,516 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
5
|
-
import { FileAuthStorageBackend } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
|
|
8
|
-
export const ANTHROPIC_PROVIDER_ID = "anthropic";
|
|
9
|
-
export const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
|
10
|
-
|
|
11
|
-
export type ManagedProviderId = typeof ANTHROPIC_PROVIDER_ID | typeof OPENAI_CODEX_PROVIDER_ID;
|
|
12
|
-
export type LadderProviderId = "anthropic" | "openai";
|
|
13
|
-
export type ModelStrength = "ultra" | "strong" | "medium" | "light";
|
|
14
|
-
|
|
15
|
-
// The default label's slot (`label === providerId`) is the OAuth-login-linked credential --
|
|
16
|
-
// its live value (`refresh`/`access`/`expires`) is owned exclusively by pi's auth.json (see
|
|
17
|
-
// provider-rotation.ts's `refreshDefaultSlotCredential`/`reauthenticateCredential`), so those
|
|
18
|
-
// fields are absent on that entry. Any OTHER explicitly-added account (`/provider-sub add`,
|
|
19
|
-
// label !== providerId) keeps the full value here, unchanged.
|
|
20
|
-
export type SubscriptionCredential = {
|
|
21
|
-
label: string;
|
|
22
|
-
refresh?: string;
|
|
23
|
-
access?: string;
|
|
24
|
-
expires?: number;
|
|
25
|
-
rateLimitedUntil: number;
|
|
26
|
-
lastAttemptAt: number;
|
|
27
|
-
lastRateLimitedAt: number;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type ModelRef = {
|
|
31
|
-
providerId: ManagedProviderId;
|
|
32
|
-
modelId: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type RotationConfig = {
|
|
36
|
-
defaultFallbackStrength?: ModelStrength;
|
|
37
|
-
revertWhenAvailable?: boolean;
|
|
38
|
-
preferredModel?: ModelRef;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type RotationConfigUpdate = {
|
|
42
|
-
defaultFallbackStrength?: ModelStrength;
|
|
43
|
-
revertWhenAvailable?: boolean;
|
|
44
|
-
preferredModel?: ModelRef | null;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type FallbackTarget = {
|
|
48
|
-
providerId: ManagedProviderId;
|
|
49
|
-
modelId: string;
|
|
50
|
-
label: string;
|
|
51
|
-
strength: ModelStrength;
|
|
52
|
-
thinkingLevel?: ThinkingLevel;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
type ModelLadderConfig = Partial<Record<ModelStrength, string>>;
|
|
56
|
-
type ModelLaddersConfig = Partial<Record<LadderProviderId, ModelLadderConfig>>;
|
|
57
|
-
const AGENT_DIR = join(homedir(), ".pi", "agent");
|
|
58
|
-
const ROTATION_CONFIG_FILE = join(AGENT_DIR, "provider-rotation.json");
|
|
59
|
-
const CROUTER_CONFIG_FILE = join(homedir(), ".crouter", "config.json");
|
|
60
|
-
const DEFAULT_FALLBACK_STRENGTH: ModelStrength = "strong";
|
|
61
|
-
|
|
62
|
-
const PROVIDERS: Record<
|
|
63
|
-
ManagedProviderId,
|
|
64
|
-
{
|
|
65
|
-
label: string;
|
|
66
|
-
ladderProviderId: LadderProviderId;
|
|
67
|
-
poolFile: string;
|
|
68
|
-
}
|
|
69
|
-
> = {
|
|
70
|
-
[ANTHROPIC_PROVIDER_ID]: {
|
|
71
|
-
label: "Claude",
|
|
72
|
-
ladderProviderId: "anthropic",
|
|
73
|
-
poolFile: join(AGENT_DIR, "anthropic-subscriptions.json"),
|
|
74
|
-
},
|
|
75
|
-
[OPENAI_CODEX_PROVIDER_ID]: {
|
|
76
|
-
label: "OpenAI Codex",
|
|
77
|
-
ladderProviderId: "openai",
|
|
78
|
-
poolFile: join(AGENT_DIR, "openai-codex-subscriptions.json"),
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
function readJsonFile<T>(path: string): T | undefined {
|
|
83
|
-
if (!existsSync(path)) return undefined;
|
|
84
|
-
try {
|
|
85
|
-
const raw = readFileSync(path, "utf8").trim();
|
|
86
|
-
if (!raw) return undefined;
|
|
87
|
-
return JSON.parse(raw) as T;
|
|
88
|
-
} catch {
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function writeJsonFile(path: string, value: unknown): void {
|
|
94
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
95
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
96
|
-
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
97
|
-
renameSync(tmp, path);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function normalizeStrength(value: unknown): ModelStrength | undefined {
|
|
101
|
-
return value === "ultra" || value === "strong" || value === "medium" || value === "light" ? value : undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function normalizeTiming(value: unknown): number {
|
|
105
|
-
const numeric = Number(value);
|
|
106
|
-
return Number.isFinite(numeric) ? numeric : 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeCredential(value: Partial<SubscriptionCredential> & { label?: string }): SubscriptionCredential | undefined {
|
|
110
|
-
const label = typeof value.label === "string" ? value.label.trim() : "";
|
|
111
|
-
if (!label) return undefined;
|
|
112
|
-
const refresh = typeof value.refresh === "string" ? value.refresh.trim() : "";
|
|
113
|
-
const access = typeof value.access === "string" ? value.access.trim() : "";
|
|
114
|
-
const expires = Number(value.expires);
|
|
115
|
-
const rateLimitedUntil = Number(value.rateLimitedUntil);
|
|
116
|
-
return {
|
|
117
|
-
label,
|
|
118
|
-
...(refresh && access ? { refresh, access, expires: Number.isFinite(expires) ? expires : 0 } : {}),
|
|
119
|
-
rateLimitedUntil: Number.isFinite(rateLimitedUntil) ? rateLimitedUntil : 0,
|
|
120
|
-
lastAttemptAt: normalizeTiming(value.lastAttemptAt),
|
|
121
|
-
lastRateLimitedAt: normalizeTiming(value.lastRateLimitedAt),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizeCredentials(values: unknown): SubscriptionCredential[] {
|
|
126
|
-
if (!Array.isArray(values)) return [];
|
|
127
|
-
return values.map((entry) => normalizeCredential(entry as Partial<SubscriptionCredential> & { label?: string })).filter((entry): entry is SubscriptionCredential => Boolean(entry));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getCurrentModelRef(model?: { provider: string; id: string }): ModelRef | undefined {
|
|
131
|
-
return isManagedProvider(model?.provider) ? { providerId: model.provider, modelId: model.id } : undefined;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
type AuthFileData = Record<string, { type?: string }>;
|
|
135
|
-
|
|
136
|
-
function readAuthCredential(providerId: ManagedProviderId): { type?: string } | undefined {
|
|
137
|
-
return new FileAuthStorageBackend().withLock((current) => {
|
|
138
|
-
if (!current) return { result: undefined };
|
|
139
|
-
const data = JSON.parse(current) as AuthFileData;
|
|
140
|
-
return { result: data[providerId] };
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Seeds a METADATA-ONLY pool entry for the default label (`label === providerId`)
|
|
145
|
-
// whenever an OAuth-login-linked credential exists for it in auth.json, regardless of
|
|
146
|
-
// whether the pool already holds OTHER explicit accounts. The auth.json seed read goes
|
|
147
|
-
// through FileAuthStorageBackend's real lock so a held auth.json lock or parse failure is
|
|
148
|
-
// surfaced instead of silently pretending the default credential is absent. Gated
|
|
149
|
-
// specifically on an OAUTH auth.json record (`type === "oauth"`) rather than the presence
|
|
150
|
-
// of any credential, because an api_key credential has no refresh token -- a
|
|
151
|
-
// metadata-only "default" slot seeded for one would be a slot `refreshDefaultSlotCredential`
|
|
152
|
-
// can never actually refresh. No token is ever copied into the pool: the value lives
|
|
153
|
-
// exclusively in auth.json.
|
|
154
|
-
function ensureDefaultSubscriptionMetadata(providerId: ManagedProviderId, pool: SubscriptionCredential[]): SubscriptionCredential[] {
|
|
155
|
-
if (pool.some((entry) => entry.label === providerId)) return pool;
|
|
156
|
-
const authCred = readAuthCredential(providerId);
|
|
157
|
-
if (authCred?.type !== "oauth") return pool;
|
|
158
|
-
const seeded = normalizeCredential({ label: providerId, rateLimitedUntil: 0, lastAttemptAt: 0, lastRateLimitedAt: 0 });
|
|
159
|
-
return seeded ? [...pool, seeded] : pool;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// The default label's value lives exclusively in auth.json, so its pool entry must never
|
|
163
|
-
// carry `refresh`/`access`/`expires`; this strips those fields on every read and write,
|
|
164
|
-
// self-healing any pool file that has them physically present on disk. `changed` tells
|
|
165
|
-
// the caller whether anything was actually stripped, so `readSubscriptionPool` can
|
|
166
|
-
// persist the metadata-only result immediately rather than leaving a stale copy on disk
|
|
167
|
-
// until some unrelated write.
|
|
168
|
-
function scrubDefaultValue(providerId: ManagedProviderId, pool: SubscriptionCredential[]): { pool: SubscriptionCredential[]; changed: boolean } {
|
|
169
|
-
let changed = false;
|
|
170
|
-
const next = pool.map((entry) => {
|
|
171
|
-
if (entry.label !== providerId || (entry.refresh === undefined && entry.access === undefined && entry.expires === undefined)) return entry;
|
|
172
|
-
changed = true;
|
|
173
|
-
const { refresh: _refresh, access: _access, expires: _expires, ...meta } = entry;
|
|
174
|
-
return meta;
|
|
175
|
-
});
|
|
176
|
-
return { pool: next, changed };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function isManagedProvider(providerId: string | undefined): providerId is ManagedProviderId {
|
|
180
|
-
return providerId === ANTHROPIC_PROVIDER_ID || providerId === OPENAI_CODEX_PROVIDER_ID;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function getProviderLabel(providerId: ManagedProviderId): string {
|
|
184
|
-
return PROVIDERS[providerId].label;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function readSubscriptionPool(providerId: ManagedProviderId): SubscriptionCredential[] {
|
|
188
|
-
const poolFile = PROVIDERS[providerId].poolFile;
|
|
189
|
-
const raw = normalizeCredentials(readJsonFile<unknown>(poolFile));
|
|
190
|
-
const { pool: scrubbed, changed: wasScrubbed } = scrubDefaultValue(providerId, raw);
|
|
191
|
-
const withDefaultSlot = ensureDefaultSubscriptionMetadata(providerId, scrubbed);
|
|
192
|
-
if (wasScrubbed || withDefaultSlot !== scrubbed) return writeSubscriptionPool(providerId, withDefaultSlot);
|
|
193
|
-
return withDefaultSlot;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function writeSubscriptionPool(providerId: ManagedProviderId, next: SubscriptionCredential[]): SubscriptionCredential[] {
|
|
197
|
-
const normalized = scrubDefaultValue(providerId, normalizeCredentials(next)).pool;
|
|
198
|
-
writeJsonFile(PROVIDERS[providerId].poolFile, normalized);
|
|
199
|
-
return normalized;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function resolveCredentialIndex(pool: SubscriptionCredential[], ref: string): number {
|
|
203
|
-
const needle = ref.trim().toLowerCase();
|
|
204
|
-
const numeric = Number.parseInt(needle, 10);
|
|
205
|
-
if (Number.isFinite(numeric) && String(numeric) === needle && numeric >= 1 && numeric <= pool.length) {
|
|
206
|
-
return numeric - 1;
|
|
207
|
-
}
|
|
208
|
-
return pool.findIndex((entry) => entry.label.toLowerCase() === needle);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function upsertSubscription(providerId: ManagedProviderId, next: SubscriptionCredential): SubscriptionCredential[] {
|
|
212
|
-
const pool = readSubscriptionPool(providerId);
|
|
213
|
-
const index = pool.findIndex((entry) => entry.label.toLowerCase() === next.label.toLowerCase());
|
|
214
|
-
if (index === -1) pool.push(next);
|
|
215
|
-
else pool[index] = next;
|
|
216
|
-
return writeSubscriptionPool(providerId, pool);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function removeSubscription(providerId: ManagedProviderId, ref: string): SubscriptionCredential[] {
|
|
220
|
-
const pool = readSubscriptionPool(providerId);
|
|
221
|
-
const index = resolveCredentialIndex(pool, ref);
|
|
222
|
-
if (index === -1) throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${ref}`);
|
|
223
|
-
pool.splice(index, 1);
|
|
224
|
-
return writeSubscriptionPool(providerId, pool);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function promoteSubscription(providerId: ManagedProviderId, ref: string): SubscriptionCredential[] {
|
|
228
|
-
const pool = readSubscriptionPool(providerId);
|
|
229
|
-
const index = resolveCredentialIndex(pool, ref);
|
|
230
|
-
if (index === -1) throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${ref}`);
|
|
231
|
-
if (index > 0) pool.unshift(pool.splice(index, 1)[0]);
|
|
232
|
-
return writeSubscriptionPool(providerId, pool);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function markSubscriptionAttempt(providerId: ManagedProviderId, ref: string, attemptAt = Date.now()): SubscriptionCredential[] {
|
|
236
|
-
const pool = readSubscriptionPool(providerId);
|
|
237
|
-
const index = resolveCredentialIndex(pool, ref);
|
|
238
|
-
if (index === -1) throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${ref}`);
|
|
239
|
-
pool[index] = {
|
|
240
|
-
...pool[index],
|
|
241
|
-
lastAttemptAt: Math.max(pool[index].lastAttemptAt, attemptAt),
|
|
242
|
-
};
|
|
243
|
-
return writeSubscriptionPool(providerId, pool);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function markSubscriptionRateLimited(
|
|
247
|
-
providerId: ManagedProviderId,
|
|
248
|
-
ref: string,
|
|
249
|
-
retryAfterMs: number,
|
|
250
|
-
attemptAt = Date.now(),
|
|
251
|
-
rateLimitedAt = Date.now(),
|
|
252
|
-
): SubscriptionCredential[] {
|
|
253
|
-
const pool = readSubscriptionPool(providerId);
|
|
254
|
-
const index = resolveCredentialIndex(pool, ref);
|
|
255
|
-
if (index === -1) throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${ref}`);
|
|
256
|
-
if (rateLimitedAt < pool[index].lastRateLimitedAt) return pool;
|
|
257
|
-
pool[index] = {
|
|
258
|
-
...pool[index],
|
|
259
|
-
lastAttemptAt: Math.max(pool[index].lastAttemptAt, attemptAt),
|
|
260
|
-
lastRateLimitedAt: Math.max(pool[index].lastRateLimitedAt, rateLimitedAt),
|
|
261
|
-
rateLimitedUntil: rateLimitedAt + Math.max(0, retryAfterMs),
|
|
262
|
-
};
|
|
263
|
-
return writeSubscriptionPool(providerId, pool);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function markSubscriptionSuccess(providerId: ManagedProviderId, ref: string, attemptAt = Date.now()): SubscriptionCredential[] {
|
|
267
|
-
const pool = readSubscriptionPool(providerId);
|
|
268
|
-
const index = resolveCredentialIndex(pool, ref);
|
|
269
|
-
if (index === -1) throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${ref}`);
|
|
270
|
-
if (attemptAt < pool[index].lastRateLimitedAt) return pool;
|
|
271
|
-
pool[index] = {
|
|
272
|
-
...pool[index],
|
|
273
|
-
lastAttemptAt: Math.max(pool[index].lastAttemptAt, attemptAt),
|
|
274
|
-
rateLimitedUntil: 0,
|
|
275
|
-
};
|
|
276
|
-
return writeSubscriptionPool(providerId, pool);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function findAvailableSubscription(providerId: ManagedProviderId, now = Date.now()): SubscriptionCredential | undefined {
|
|
280
|
-
return readSubscriptionPool(providerId).find((entry) => !entry.rateLimitedUntil || entry.rateLimitedUntil <= now);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function parseRetryAfterHeader(value: string | undefined, now = Date.now()): number | undefined {
|
|
284
|
-
if (!value) return undefined;
|
|
285
|
-
const trimmed = value.trim();
|
|
286
|
-
if (!trimmed) return undefined;
|
|
287
|
-
const seconds = Number(trimmed);
|
|
288
|
-
if (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000));
|
|
289
|
-
const parsed = Date.parse(trimmed);
|
|
290
|
-
return Number.isFinite(parsed) ? Math.max(0, parsed - now) : undefined;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function thinkingLevelOfRef(ref: string): ThinkingLevel | undefined {
|
|
294
|
-
const match = ref.trim().match(/:(thinking|off|minimal|low|medium|high|xhigh|max)$/i);
|
|
295
|
-
if (!match) return undefined;
|
|
296
|
-
const level = match[1].toLowerCase();
|
|
297
|
-
return level === "off" || level === "thinking" ? undefined : (level as ThinkingLevel);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function normalizeModelRef(ref: string): string {
|
|
301
|
-
return ref
|
|
302
|
-
.trim()
|
|
303
|
-
.replace(/^[^/]+\//, "")
|
|
304
|
-
.replace(/:(?:thinking|off|minimal|low|medium|high|xhigh|max)$/i, "")
|
|
305
|
-
.toLowerCase();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function splitModelRef(ref: string): FallbackTarget {
|
|
309
|
-
const trimmed = ref.trim();
|
|
310
|
-
const slash = trimmed.indexOf("/");
|
|
311
|
-
const providerId = slash === -1 ? OPENAI_CODEX_PROVIDER_ID : trimmed.slice(0, slash).trim();
|
|
312
|
-
if (!isManagedProvider(providerId)) {
|
|
313
|
-
throw new Error(`Unsupported fallback provider: ${providerId}`);
|
|
314
|
-
}
|
|
315
|
-
const rawModelId = slash === -1 ? trimmed : trimmed.slice(slash + 1).trim();
|
|
316
|
-
const thinkingMatch = rawModelId.match(/^(.*?)(?::(thinking|off|minimal|low|medium|high|xhigh|max))$/i);
|
|
317
|
-
return {
|
|
318
|
-
providerId,
|
|
319
|
-
modelId: thinkingMatch ? thinkingMatch[1] : rawModelId,
|
|
320
|
-
label: `${providerId}/${thinkingMatch ? thinkingMatch[1] : rawModelId}${thinkingMatch?.[2] ? `:${thinkingMatch[2].toLowerCase()}` : ""}`,
|
|
321
|
-
strength: DEFAULT_FALLBACK_STRENGTH,
|
|
322
|
-
thinkingLevel: thinkingMatch?.[2] && thinkingMatch[2].toLowerCase() !== "off" && thinkingMatch[2].toLowerCase() !== "thinking"
|
|
323
|
-
? (thinkingMatch[2].toLowerCase() as ThinkingLevel)
|
|
324
|
-
: undefined,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Vendored copy of crouter's `defaultModelLaddersConfig()` (src/types.ts).
|
|
329
|
-
// This package is synced from a standalone repo (pi-personal-extensions) and
|
|
330
|
-
// has no runtime import path into crouter's compiled internals, so the
|
|
331
|
-
// builtin rungs are duplicated here rather than imported; keep the two in
|
|
332
|
-
// sync by hand if either changes.
|
|
333
|
-
const BUILTIN_MODEL_LADDERS: Record<LadderProviderId, ModelLadderConfig> = {
|
|
334
|
-
anthropic: {
|
|
335
|
-
ultra: "anthropic/claude-fable-5:high",
|
|
336
|
-
strong: "anthropic/claude-fable-5:medium",
|
|
337
|
-
medium: "anthropic/claude-opus-4-8:high",
|
|
338
|
-
light: "anthropic/claude-sonnet-5:high",
|
|
339
|
-
},
|
|
340
|
-
openai: {
|
|
341
|
-
ultra: "openai-codex/gpt-5.6-sol:max",
|
|
342
|
-
strong: "openai-codex/gpt-5.6-sol:high",
|
|
343
|
-
medium: "openai-codex/gpt-5.6-terra:medium",
|
|
344
|
-
light: "openai-codex/gpt-5.6-luna:low",
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
// Layers ~/.crouter/config.json's `modelLadders` (if any) OVER the builtin
|
|
349
|
-
// defaults above, per provider/strength — mirroring crouter's own
|
|
350
|
-
// `mergeModelLadders` (src/core/config.ts), which is what a node's launcher
|
|
351
|
-
// actually resolves models from. Without this fallback, a config-less user
|
|
352
|
-
// (the common case: fresh install, no ~/.crouter/config.json) saw an EMPTY
|
|
353
|
-
// ladder here while crtr itself launched them on the builtin defaults —
|
|
354
|
-
// rotation could then never resolve a strength tier or a cross-provider
|
|
355
|
-
// fallback for a model it was already running. Always returns a fully
|
|
356
|
-
// populated ladder for both providers; a config.json override only replaces
|
|
357
|
-
// the specific strengths it sets.
|
|
358
|
-
function readModelLadders(): ModelLaddersConfig {
|
|
359
|
-
const config = readJsonFile<{ modelLadders?: Partial<Record<LadderProviderId, Partial<ModelLadderConfig>>> }>(CROUTER_CONFIG_FILE);
|
|
360
|
-
const overrides = config?.modelLadders;
|
|
361
|
-
const result: ModelLaddersConfig = {};
|
|
362
|
-
for (const provider of ["anthropic", "openai"] as const) {
|
|
363
|
-
const normalized: ModelLadderConfig = { ...BUILTIN_MODEL_LADDERS[provider] };
|
|
364
|
-
const override = overrides?.[provider];
|
|
365
|
-
if (override) {
|
|
366
|
-
for (const strength of ["ultra", "strong", "medium", "light"] as const) {
|
|
367
|
-
const value = typeof override[strength] === "string" ? override[strength]?.trim() : "";
|
|
368
|
-
if (value) normalized[strength] = value;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
result[provider] = normalized;
|
|
372
|
-
}
|
|
373
|
-
return result;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function findStrengthForModelId(
|
|
377
|
-
providerId: ManagedProviderId,
|
|
378
|
-
modelId: string | undefined,
|
|
379
|
-
defaultStrength: ModelStrength,
|
|
380
|
-
ladders = readModelLadders(),
|
|
381
|
-
currentThinkingLevel?: string,
|
|
382
|
-
): ModelStrength | undefined {
|
|
383
|
-
const source = ladders?.[PROVIDERS[providerId].ladderProviderId];
|
|
384
|
-
const bareId = modelId ? normalizeModelRef(modelId) : undefined;
|
|
385
|
-
if (!source || !bareId) return undefined;
|
|
386
|
-
const matches: ModelStrength[] = [];
|
|
387
|
-
for (const strength of ["ultra", "strong", "medium", "light"] as const) {
|
|
388
|
-
const ref = source[strength];
|
|
389
|
-
if (ref && normalizeModelRef(ref) === bareId) matches.push(strength);
|
|
390
|
-
}
|
|
391
|
-
if (matches.length === 0) return undefined;
|
|
392
|
-
// Rungs that share a bare model id (e.g. gpt-5.6-sol:max vs gpt-5.6-sol:high) are
|
|
393
|
-
// disambiguated by the live reasoning level so an ultra run doesn't collapse to strong.
|
|
394
|
-
if (matches.length > 1 && currentThinkingLevel) {
|
|
395
|
-
const exact = matches.find((strength) => thinkingLevelOfRef(source[strength]!) === currentThinkingLevel);
|
|
396
|
-
if (exact) return exact;
|
|
397
|
-
}
|
|
398
|
-
return matches.includes(defaultStrength) ? defaultStrength : matches[0];
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export function readRotationConfig(): RotationConfig {
|
|
402
|
-
const raw = readJsonFile<RotationConfig>(ROTATION_CONFIG_FILE) ?? {};
|
|
403
|
-
const preferredModel =
|
|
404
|
-
raw.preferredModel && isManagedProvider(raw.preferredModel.providerId) && typeof raw.preferredModel.modelId === "string"
|
|
405
|
-
? { providerId: raw.preferredModel.providerId, modelId: raw.preferredModel.modelId.trim() }
|
|
406
|
-
: undefined;
|
|
407
|
-
return {
|
|
408
|
-
defaultFallbackStrength: normalizeStrength(raw.defaultFallbackStrength) ?? DEFAULT_FALLBACK_STRENGTH,
|
|
409
|
-
revertWhenAvailable: raw.revertWhenAvailable ?? true,
|
|
410
|
-
preferredModel,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export function writeRotationConfig(next: RotationConfigUpdate): RotationConfig {
|
|
415
|
-
const current = readRotationConfig();
|
|
416
|
-
const merged: RotationConfig = {
|
|
417
|
-
defaultFallbackStrength: normalizeStrength(next.defaultFallbackStrength) ?? current.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH,
|
|
418
|
-
revertWhenAvailable: next.revertWhenAvailable ?? current.revertWhenAvailable ?? true,
|
|
419
|
-
preferredModel: Object.prototype.hasOwnProperty.call(next, "preferredModel")
|
|
420
|
-
? next.preferredModel || undefined
|
|
421
|
-
: current.preferredModel,
|
|
422
|
-
};
|
|
423
|
-
writeJsonFile(ROTATION_CONFIG_FILE, merged);
|
|
424
|
-
return merged;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export function clearPreferredModel(): RotationConfig {
|
|
428
|
-
return writeRotationConfig({ preferredModel: null });
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export function rememberPreferredModel(model: ModelRef): RotationConfig {
|
|
432
|
-
return writeRotationConfig({ preferredModel: model });
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// `readModelLadders()` always fills both providers from the builtin defaults, so this
|
|
436
|
-
// only returns undefined (never throws) in the defensive case where a rung is still
|
|
437
|
-
// genuinely missing; callers must treat that as a graceful "no fallback available" case
|
|
438
|
-
// rather than a config error to surface mid-turn.
|
|
439
|
-
export function resolveFallbackTarget(
|
|
440
|
-
currentProviderId: ManagedProviderId,
|
|
441
|
-
currentModelId?: string,
|
|
442
|
-
config = readRotationConfig(),
|
|
443
|
-
currentThinkingLevel?: string,
|
|
444
|
-
): FallbackTarget | undefined {
|
|
445
|
-
const ladders = readModelLadders();
|
|
446
|
-
const defaultStrength = config.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH;
|
|
447
|
-
const sourceStrength = findStrengthForModelId(currentProviderId, currentModelId, defaultStrength, ladders, currentThinkingLevel);
|
|
448
|
-
const strength = sourceStrength ?? defaultStrength;
|
|
449
|
-
const targetProvider = currentProviderId === ANTHROPIC_PROVIDER_ID ? OPENAI_CODEX_PROVIDER_ID : ANTHROPIC_PROVIDER_ID;
|
|
450
|
-
const targetLadder = ladders?.[PROVIDERS[targetProvider].ladderProviderId];
|
|
451
|
-
const candidates = strength === defaultStrength ? [strength] : [strength, defaultStrength];
|
|
452
|
-
const fallbackRef = candidates.map((candidate) => targetLadder?.[candidate]).find((ref): ref is string => typeof ref === "string" && ref.trim().length > 0);
|
|
453
|
-
if (!fallbackRef) {
|
|
454
|
-
return undefined;
|
|
455
|
-
}
|
|
456
|
-
const parsed = splitModelRef(fallbackRef);
|
|
457
|
-
const resolvedStrength = candidates.find((candidate) => targetLadder?.[candidate] === fallbackRef) ?? strength;
|
|
458
|
-
return { ...parsed, strength: resolvedStrength };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function shouldRestorePreferredModel(config = readRotationConfig(), now = Date.now()): boolean {
|
|
462
|
-
if (!config.revertWhenAvailable || !config.preferredModel) return false;
|
|
463
|
-
return Boolean(findAvailableSubscription(config.preferredModel.providerId, now));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
export async function restorePreferredModelIfPossible(
|
|
467
|
-
piApi: ExtensionAPI,
|
|
468
|
-
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
469
|
-
): Promise<boolean> {
|
|
470
|
-
const config = readRotationConfig();
|
|
471
|
-
if (!shouldRestorePreferredModel(config, Date.now())) return false;
|
|
472
|
-
const preferred = config.preferredModel;
|
|
473
|
-
if (!preferred) return false;
|
|
474
|
-
const current = getCurrentModelRef(ctx.model);
|
|
475
|
-
if (current?.providerId === preferred.providerId && current.modelId === preferred.modelId) {
|
|
476
|
-
clearPreferredModel();
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
const model = ctx.modelRegistry.find(preferred.providerId, preferred.modelId);
|
|
480
|
-
if (!model) return false;
|
|
481
|
-
const switched = await piApi.setModel(model);
|
|
482
|
-
if (switched) clearPreferredModel();
|
|
483
|
-
return switched;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
export async function switchToFallbackIfPossible(
|
|
487
|
-
piApi: ExtensionAPI,
|
|
488
|
-
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
489
|
-
preferredModel = getCurrentModelRef(ctx.model),
|
|
490
|
-
): Promise<FallbackTarget | undefined> {
|
|
491
|
-
if (!preferredModel) return undefined;
|
|
492
|
-
rememberPreferredModel(preferredModel);
|
|
493
|
-
const currentThinkingLevel = piApi.getThinkingLevel?.();
|
|
494
|
-
const target = resolveFallbackTarget(preferredModel.providerId, preferredModel.modelId, readRotationConfig(), currentThinkingLevel);
|
|
495
|
-
if (!target) return undefined;
|
|
496
|
-
const current = getCurrentModelRef(ctx.model);
|
|
497
|
-
const model = ctx.modelRegistry.find(target.providerId, target.modelId);
|
|
498
|
-
if (!model) return undefined;
|
|
499
|
-
if (current?.providerId !== target.providerId || current.modelId !== target.modelId) {
|
|
500
|
-
const switched = await piApi.setModel(model);
|
|
501
|
-
if (!switched) return undefined;
|
|
502
|
-
}
|
|
503
|
-
if (target.thinkingLevel) piApi.setThinkingLevel(target.thinkingLevel);
|
|
504
|
-
return target;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
export function formatStatusLine(providerId: ManagedProviderId, now = Date.now()): string {
|
|
508
|
-
const pool = readSubscriptionPool(providerId);
|
|
509
|
-
const ready = pool.filter((entry) => !entry.rateLimitedUntil || entry.rateLimitedUntil <= now);
|
|
510
|
-
const label = getProviderLabel(providerId);
|
|
511
|
-
if (ready.length === 0) {
|
|
512
|
-
const fallback = resolveFallbackTarget(providerId, undefined, readRotationConfig());
|
|
513
|
-
return fallback ? `${label} pool exhausted -> ${fallback.label}` : `${label} pool exhausted (no fallback configured)`;
|
|
514
|
-
}
|
|
515
|
-
return `${label} ${ready[0]?.label ?? providerId} (${ready.length}/${pool.length} ready)`;
|
|
516
|
-
}
|
|
1
|
+
// Provider pool state belongs to crouter core so every human surface and the rotation runtime share one invariant-preserving implementation.
|
|
2
|
+
export * from "../../../core/subscription-state.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "pi-
|
|
2
|
+
"name": "pi-crtr-extensions",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
|
-
"name": "pi-
|
|
8
|
+
"name": "pi-crtr-extensions",
|
|
9
9
|
"version": "0.1.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# pi-mode-switch
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
cycles plan / accept-edits. Whenever the mode changes, the new mode's operating
|
|
5
|
-
instructions are injected (invisibly) before your next message.
|
|
3
|
+
Cycle between operating modes with crouter's configurable **Cycle mode** binding (default **Alt+M**) — the way Claude's Shift+Tab cycles plan / accept-edits. Whenever the mode changes, the new mode's operating instructions are injected invisibly before your next message.
|
|
6
4
|
|
|
7
5
|
## Modes
|
|
8
6
|
|
|
@@ -14,51 +12,25 @@ instructions are injected (invisibly) before your next message.
|
|
|
14
12
|
|
|
15
13
|
## Usage
|
|
16
14
|
|
|
17
|
-
- `Alt+M` — cycle `normal → spec → plan → normal`
|
|
18
|
-
- `/mode` —
|
|
15
|
+
- the configured `crtr.mode.cycle` binding (default `Alt+M`) — cycle `normal → spec → plan → normal`
|
|
16
|
+
- `/mode` — cycle without using the shortcut
|
|
19
17
|
- `/mode <normal\|spec\|plan>` — jump to a specific mode
|
|
20
|
-
- the agent can switch its own mode by running the bundled `bin/mode` CLI through
|
|
21
|
-
its bash tool — e.g. `bash <plugin>/bin/mode normal` after a spec/plan is
|
|
22
|
-
approved. The command prints that mode's guidance, so the text only enters
|
|
23
|
-
context at the moment of the switch (no always-on tool)
|
|
18
|
+
- the agent can switch its own mode by running the bundled `bin/mode` CLI through its bash tool — e.g. `bash <plugin>/bin/mode normal` after a spec/plan is approved. The command prints that mode's guidance, so the text only enters context at the moment of the switch (no always-on tool)
|
|
24
19
|
|
|
25
|
-
The current mode renders as a badge on the **bottom-right of the input box**,
|
|
26
|
-
diagonally opposite the session-name badge (top-left). Mode persists across
|
|
27
|
-
`--resume`.
|
|
20
|
+
The current mode renders as a badge on the **bottom-right of the input box**, diagonally opposite the session-name badge (top-left). Mode persists across `--resume`.
|
|
28
21
|
|
|
29
22
|
## How it works
|
|
30
23
|
|
|
31
|
-
The extension tracks the active mode and the mode whose guidance was last
|
|
32
|
-
injected. On `before_agent_start` it compares the two: if they differ, it
|
|
33
|
-
prepends that mode's guidance to the turn (as a hidden `mode-switch-context`
|
|
34
|
-
message) and records the injection. Stay in the same mode and nothing extra is
|
|
35
|
-
added. Returning to `normal` also strips stale guidance from context.
|
|
24
|
+
The extension tracks the active mode and the mode whose guidance was last injected. On `before_agent_start` it compares the two: if they differ, it prepends that mode's guidance to the turn as a hidden `mode-switch-context` message and records the injection. Stay in the same mode and nothing extra is added. Returning to `normal` also strips stale guidance from context.
|
|
36
25
|
|
|
37
|
-
The agent can also switch its own mode without a permanent tool. The bundled
|
|
38
|
-
`bin/mode` CLI, run through the normal bash tool, writes the requested mode to
|
|
39
|
-
`<artifact-root>/mode-switch/request.json` and prints that mode's operating
|
|
40
|
-
instructions to stdout. The extension mirrors each mode's guidance to
|
|
41
|
-
`mode-switch/guidance/<mode>.txt` at session start (single source of truth stays
|
|
42
|
-
in the extension), watches the request file to keep the badge and session state in
|
|
43
|
-
sync, and consumes each request on read so a later Alt+M isn't clobbered. The
|
|
44
|
-
guidance reaches the agent through the command's own output, so it only enters
|
|
45
|
-
context at the moment of the switch — no tool sits in the context window, and
|
|
46
|
-
there's no coupling to any external runtime, just files.
|
|
26
|
+
The agent can also switch its own mode without a permanent tool. The bundled `bin/mode` CLI, run through the normal bash tool, writes the requested mode to `<artifact-root>/mode-switch/request.json` and prints that mode's operating instructions to stdout. The extension mirrors each mode's guidance to `mode-switch/guidance/<mode>.txt` at session start, watches the request file to keep the badge and session state in sync, and applies each tokenized request once. The guidance reaches the agent through the command's own output, so it only enters context at the moment of the switch — no tool sits in the context window, and there's no coupling to an external runtime beyond the shared files.
|
|
47
27
|
|
|
48
|
-
This is guidance-only — it does not hard-restrict tools. The instructions tell
|
|
49
|
-
the agent not to edit in spec/plan mode; combine with a tool gate if you want
|
|
50
|
-
enforcement.
|
|
28
|
+
This is guidance-only — it does not hard-restrict tools. The instructions tell the agent not to edit in spec/plan mode; combine with a tool gate if you want enforcement.
|
|
51
29
|
|
|
52
|
-
The input badge is drawn by wrapping whatever editor is already installed (
|
|
53
|
-
session-name badge extension) in a transparent Proxy that overrides only
|
|
54
|
-
`render()`. It composes instead of replacing, so other editor decorations keep
|
|
55
|
-
working.
|
|
30
|
+
The input badge is drawn by wrapping whatever editor is already installed (for example, a session-name badge extension) in a transparent Proxy that overrides only `render()`. It composes instead of replacing, so other editor decorations keep working.
|
|
56
31
|
|
|
57
32
|
## Install
|
|
58
33
|
|
|
59
|
-
|
|
60
|
-
pi install /Users/silasrhyneer/Code/cli/pi-mode-switch # user scope
|
|
61
|
-
pi install -l /Users/silasrhyneer/Code/cli/pi-mode-switch # project scope
|
|
62
|
-
```
|
|
34
|
+
This package ships inside crouter. `crtr sys setup` registers its built package path with pi; there is no separate repository or package to install.
|
|
63
35
|
|
|
64
|
-
Run `/reload` in pi after
|
|
36
|
+
Run `/reload` in pi after setup. The extension resolves `crtr.mode.cycle` from the sparse `keybindings` object in `~/.crouter/config.json`; missing overrides use Alt+M, multiple gestures all register, and an empty list disables the shortcut. `/mode` remains available when the shortcut is disabled. Pi's stock actions and `~/.pi/agent/keybindings.json` remain untouched.
|