@huseynsnmz/pi-suite 0.2.2 → 0.2.3

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +8 -1
  3. package/package.json +6 -1
  4. package/packages/board/README.md +1 -1
  5. package/packages/commands/README.md +1 -1
  6. package/packages/commands/src/index.ts +22 -0
  7. package/packages/frame/README.md +8 -2
  8. package/packages/frame/docs/reference.md +1 -1
  9. package/packages/frame/src/index.ts +8 -3
  10. package/packages/frame/src/kit/settings/files.ts +8 -8
  11. package/packages/frame/src/kit/settings/store.ts +3 -1
  12. package/packages/frame/src/ui/editor.ts +64 -7
  13. package/packages/frame/src/ui/history-search.ts +21 -0
  14. package/packages/frame/src/ui/pi-entries.ts +2 -0
  15. package/packages/frame/src/ui/thinking.ts +34 -0
  16. package/packages/memory/README.md +1 -1
  17. package/packages/modes/README.md +2 -2
  18. package/packages/modes/src/index.ts +3 -6
  19. package/packages/router/README.md +29 -9
  20. package/packages/router/src/config.ts +28 -27
  21. package/packages/router/src/index.ts +61 -13
  22. package/packages/router/src/route.ts +16 -10
  23. package/packages/subscription/LICENSE +21 -0
  24. package/packages/subscription/README.md +60 -0
  25. package/packages/subscription/package.json +23 -0
  26. package/packages/subscription/src/anchors.ts +75 -0
  27. package/packages/subscription/src/billing.ts +39 -0
  28. package/packages/subscription/src/catalog.ts +310 -0
  29. package/packages/subscription/src/cc-sessions.ts +122 -0
  30. package/packages/subscription/src/config.ts +34 -0
  31. package/packages/subscription/src/convert.ts +161 -0
  32. package/packages/subscription/src/index.ts +69 -0
  33. package/packages/subscription/src/mcp.ts +78 -0
  34. package/packages/subscription/src/models.ts +65 -0
  35. package/packages/subscription/src/prompt.ts +57 -0
  36. package/packages/subscription/src/provider.ts +617 -0
  37. package/packages/subscription/src/session.ts +294 -0
  38. package/packages/subscription/src/util.ts +73 -0
  39. package/packages/web/README.md +5 -5
  40. package/packages/web/src/config.ts +13 -23
  41. package/packages/web/src/index.ts +11 -7
  42. package/packages/web/src/reader.ts +15 -4
  43. package/packages/web/src/validate.ts +2 -2
  44. package/src/index.ts +92 -10
  45. package/src/settings-ui.ts +63 -0
  46. package/src/settings.ts +116 -15
@@ -0,0 +1,78 @@
1
+ // The little of MCP that Claude Code needs to see pi's tools: the Agent SDK hands this object a
2
+ // transport and passes JSON-RPC through it. The MCP SDK would bring a web server and more to do this.
3
+ import type { Tool } from "@earendil-works/pi-ai";
4
+
5
+ export interface ToolResult {
6
+ content: unknown[];
7
+ isError?: boolean;
8
+ }
9
+
10
+ interface Transport {
11
+ onmessage?: (message: JsonRpc) => void;
12
+ start(): Promise<void>;
13
+ send(message: unknown): Promise<void>;
14
+ close(): Promise<void>;
15
+ }
16
+
17
+ interface JsonRpc {
18
+ id?: string | number;
19
+ method?: string;
20
+ params?: { protocolVersion?: string; _meta?: Record<string, unknown> };
21
+ }
22
+
23
+ const METHOD_NOT_FOUND = -32601;
24
+ const INTERNAL_ERROR = -32603;
25
+
26
+ export class ToolServer {
27
+ readonly #tools: unknown[];
28
+ readonly #call: (toolUseId: string | undefined) => Promise<ToolResult>;
29
+ #transport: Transport | undefined;
30
+
31
+ constructor(name: string, tools: readonly Tool[], call: (toolUseId: string | undefined) => Promise<ToolResult>) {
32
+ this.name = name;
33
+ this.#call = call;
34
+ this.#tools = tools.map((tool) => ({
35
+ name: tool.name,
36
+ description: tool.description,
37
+ inputSchema: { type: "object", ...(JSON.parse(JSON.stringify(tool.parameters ?? {})) as object) },
38
+ }));
39
+ }
40
+
41
+ readonly name: string;
42
+
43
+ async connect(transport: Transport): Promise<void> {
44
+ this.#transport = transport;
45
+ transport.onmessage = (message) => void this.#handle(message);
46
+ await transport.start();
47
+ }
48
+
49
+ async close(): Promise<void> {
50
+ await this.#transport?.close();
51
+ }
52
+
53
+ async #handle(message: JsonRpc): Promise<void> {
54
+ // A notification has no id and wants no answer.
55
+ if (message.id === undefined) return;
56
+ const answer = await this.#answer(message);
57
+ await this.#transport?.send({ jsonrpc: "2.0", id: message.id, ...answer }).catch(() => undefined);
58
+ }
59
+
60
+ async #answer(message: JsonRpc): Promise<{ result: unknown } | { error: { code: number; message: string } }> {
61
+ switch (message.method) {
62
+ case "initialize":
63
+ return { result: { protocolVersion: message.params?.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: this.name, version: "1.0.0" } } };
64
+ case "ping":
65
+ return { result: {} };
66
+ case "tools/list":
67
+ return { result: { tools: this.#tools } };
68
+ case "tools/call":
69
+ try {
70
+ return { result: await this.#call(message.params?._meta?.["claudecode/toolUseId"] as string | undefined) };
71
+ } catch (error) {
72
+ return { error: { code: INTERNAL_ERROR, message: (error as Error).message } };
73
+ }
74
+ default:
75
+ return { error: { code: METHOD_NOT_FOUND, message: `Unknown method ${message.method}` } };
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,65 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
3
+ import { CATALOG } from "./catalog.ts";
4
+
5
+ type ProviderChatModelConfig = Extract<ProviderModelConfig, { reasoning: boolean }>;
6
+ type CatalogModel = Pick<Model<Api>, "id" | "name" | "reasoning" | "input" | "cost" | "contextWindow" | "maxTokens" | "thinkingLevelMap">;
7
+
8
+ /**
9
+ * Chat models offered through Claude Code. Limits, prices and thinking levels come from pi's
10
+ * Anthropic catalog: a snapshot at load (so `--model` works at startup), refreshed from pi's live
11
+ * registry once a session starts (see index.ts). Dated aliases are skipped; Claude Code resolves
12
+ * the undated ids itself.
13
+ */
14
+ export function claudeModels(source: readonly CatalogModel[] = CATALOG as unknown as CatalogModel[]): ProviderChatModelConfig[] {
15
+ return source
16
+ .filter((m) => !/-\d{8}$/.test(m.id))
17
+ .map((m) => ({
18
+ id: m.id,
19
+ name: `${m.name} (Claude subscription)`,
20
+ reasoning: m.reasoning,
21
+ input: m.input,
22
+ cost: m.cost,
23
+ contextWindow: m.contextWindow,
24
+ maxTokens: m.maxTokens,
25
+ ...(m.thinkingLevelMap ? { thinkingLevelMap: m.thinkingLevelMap } : {}),
26
+ // No promptCache: Claude Code already writes one-hour cache entries, and pi's cache
27
+ // warmer (a one-token replay of an earlier request) cannot be expressed through it.
28
+ }));
29
+ }
30
+
31
+ /** pi's Anthropic models that can back this provider. */
32
+ export function anthropicChatModels(models: readonly (CatalogModel & { provider: string; type?: string })[]): CatalogModel[] {
33
+ return models.filter((m) => m.provider === "anthropic" && (m.type === undefined || m.type === "chat"));
34
+ }
35
+
36
+ export type ThinkingOption = { type: "adaptive"; display: "summarized" } | { type: "disabled" } | { type: "enabled"; budgetTokens: number; display: "summarized" };
37
+
38
+ const EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
39
+
40
+ /**
41
+ * Map a pi thinking level to Agent SDK thinking options. pi resolves the level through the
42
+ * model's thinkingLevelMap before it reaches the provider, so the value is already the
43
+ * provider-native effort when the model declares one.
44
+ */
45
+ export function thinkingOptions(
46
+ level: string | undefined,
47
+ hasLevelMap: boolean,
48
+ ): { thinking: ThinkingOption; effort?: "low" | "medium" | "high" | "xhigh" | "max" } {
49
+ if (!level || level === "off") return { thinking: { type: "disabled" } };
50
+ if (hasLevelMap) {
51
+ const effort = EFFORTS.has(level) ? (level as any) : level === "minimal" ? "low" : "medium";
52
+ return { thinking: { type: "adaptive", display: "summarized" }, effort };
53
+ }
54
+ // Budget-based models (e.g. Haiku 4.5).
55
+ const budgets: Record<string, number> = { minimal: 1024, low: 2048, medium: 8192, high: 16384, xhigh: 32000, max: 48000 };
56
+ return { thinking: { type: "enabled", budgetTokens: budgets[level] ?? 8192, display: "summarized" } };
57
+ }
58
+
59
+ /** A budget counts toward the output cap, so the cap grows to fit it, as in pi's Anthropic provider, up to what the model allows. */
60
+ export function fitThinking(thinking: ThinkingOption, maxTokens: number, modelMax: number): { thinking: ThinkingOption; maxOutputTokens: number } {
61
+ if (thinking.type !== "enabled") return { thinking, maxOutputTokens: maxTokens };
62
+ const maxOutputTokens = Math.min(maxTokens + thinking.budgetTokens, modelMax);
63
+ const budgetTokens = maxOutputTokens > thinking.budgetTokens ? thinking.budgetTokens : Math.max(1024, maxOutputTokens - 1024);
64
+ return { thinking: { ...thinking, budgetTokens }, maxOutputTokens };
65
+ }
@@ -0,0 +1,57 @@
1
+ import type { BeforeAgentStartEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ export type PromptMode = "claude" | "pi";
4
+ type PromptOptions = BeforeAgentStartEvent["systemPromptOptions"];
5
+ export interface DocPaths {
6
+ readme: string;
7
+ docs: string;
8
+ examples: string;
9
+ }
10
+
11
+ /**
12
+ * pi's prompt prefix rewritten for Claude. pi drops its own tools, rules and docs sections when a
13
+ * custom prefix is set, so this renders them itself: every selected tool and every guideline pi or
14
+ * another extension contributed. Context files, skills and the working directory stay pi's.
15
+ */
16
+ export function claudePreamble(o: PromptOptions, docs: DocPaths): string {
17
+ const tools = o.selectedTools.filter((name) => o.toolSnippets[name]).map((name) => `- ${name}: ${o.toolSnippets[name]}`);
18
+ const guidelines = new Set<string>();
19
+ const searchTools = ["grep", "find", "ls"].some((t) => o.selectedTools.includes(t));
20
+ if (o.selectedTools.includes("bash") && !searchTools) guidelines.add("Use bash for file discovery and search (ls, rg, find).");
21
+ for (const name of o.selectedTools) for (const g of o.toolGuidelines[name] ?? []) guidelines.add(g.trim());
22
+ for (const g of o.promptGuidelines) guidelines.add(g.trim());
23
+ guidelines.delete("");
24
+
25
+ return `You are Claude, working as a coding agent in pi, a terminal coding harness. You help the user by reading files, running commands, and editing code in their project.
26
+
27
+ Tools:
28
+ ${tools.length > 0 ? tools.join("\n") : "(none)"}
29
+ Other tools may be available; their definitions describe them.
30
+
31
+ How to work:
32
+ - Understand before you change: read the relevant code and search the project before editing. Don't guess file contents or APIs.
33
+ - Make independent tool calls in parallel, such as reading several files at once. Sequence only calls that depend on each other.
34
+ - Change existing files with targeted edits rather than rewrites. Create files only when the task needs them.
35
+ - Stay within the request: no unrequested features, refactors, or comments.
36
+ - Verify: run the project's tests, type checker, or the program itself when they exist, and fix what fails. If you can't verify, say so.
37
+ - Report faithfully: never claim that something passed, ran, or exists unless you saw it.
38
+ - If the request is ambiguous in a way that changes the result, ask. Otherwise state your assumption and proceed.
39
+ - Be concise. Lead with the outcome and show file paths clearly.${guidelines.size > 0 ? `\n\nGuidelines:\n${[...guidelines].map((g) => `- ${g}`).join("\n")}` : ""}
40
+
41
+ Questions about pi itself (extensions, skills, settings, its SDK): read ${docs.readme}, the docs in ${docs.docs} and the examples in ${docs.examples} first.`;
42
+ }
43
+
44
+ /**
45
+ * before_agent_start: give Claude models the Claude prefix. A user's own SYSTEM.md (customPrompt)
46
+ * always wins, and other providers keep pi's default prompt.
47
+ */
48
+ export function applyClaudePrompt(
49
+ event: { systemPromptOptions: PromptOptions },
50
+ model: { provider: string } | undefined,
51
+ mode: PromptMode,
52
+ docs: DocPaths = { readme: "README.md", docs: "docs", examples: "examples" },
53
+ ): void {
54
+ if (mode !== "claude" || model?.provider !== "claude-sdk") return;
55
+ if (event.systemPromptOptions.customPrompt) return;
56
+ event.systemPromptOptions.customPrompt = claudePreamble(event.systemPromptOptions, docs);
57
+ }