hob-bot 1.0.3 → 1.1.0

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 (86) hide show
  1. package/README.md +75 -5
  2. package/README.zh-CN.md +75 -5
  3. package/dist/web/assets/DesktopView-rVxacveF.js +5 -0
  4. package/dist/web/assets/index-B-8ISOr1.js +267 -0
  5. package/dist/web/assets/index-Cu1Pf8yX.css +1 -0
  6. package/dist/web/assets/{math-zZ4JA8eM.js → math-weKGDUmL.js} +2 -2
  7. package/dist/web/index.html +2 -2
  8. package/dist/web/mock/desktop.jpg +0 -0
  9. package/dist/web/mock/photo.jpg +0 -0
  10. package/dist/web/mock/photo2.jpg +0 -0
  11. package/dist/web/mock/sunset.html +90 -0
  12. package/dist/web/mock//347/201/253/346/230/237/346/227/245/350/220/275/347/244/272/346/204/217/345/233/276.md +34 -0
  13. package/package.json +10 -2
  14. package/server/src/actions.ts +24 -57
  15. package/server/src/assistant-skills.ts +201 -0
  16. package/server/src/boot.ts +1 -0
  17. package/server/src/claude-status.ts +47 -0
  18. package/server/src/config.ts +3 -1
  19. package/server/src/delegations.ts +40 -12
  20. package/server/src/desktop-control.ts +152 -0
  21. package/server/src/desktop-events.ts +63 -0
  22. package/server/src/desktop-public.ts +43 -0
  23. package/server/src/desktop-tools.ts +161 -0
  24. package/server/src/desktop.ts +76 -0
  25. package/server/src/embedding.ts +52 -0
  26. package/server/src/files.ts +63 -43
  27. package/server/src/http.ts +334 -15
  28. package/server/src/jobs.ts +330 -59
  29. package/server/src/knowledge-index.ts +123 -0
  30. package/server/src/knowledge-tools.ts +94 -0
  31. package/server/src/knowledge.ts +451 -0
  32. package/server/src/lore-entry.ts +10 -0
  33. package/server/src/main.ts +6 -0
  34. package/server/src/mcp.ts +745 -0
  35. package/server/src/memory.ts +11 -4
  36. package/server/src/now.ts +33 -0
  37. package/server/src/overview.ts +1 -1
  38. package/server/src/pages.ts +1 -1
  39. package/server/src/pi.ts +92 -16
  40. package/server/src/prompt.ts +46 -154
  41. package/server/src/providers.ts +3 -2
  42. package/server/src/push.ts +42 -7
  43. package/server/src/router-activity.ts +4 -0
  44. package/server/src/router-tools.ts +132 -38
  45. package/server/src/router.ts +159 -19
  46. package/server/src/routines.ts +14 -4
  47. package/server/src/runtime.ts +15 -0
  48. package/server/src/settings.ts +43 -1
  49. package/server/src/store.ts +4 -3
  50. package/server/src/thumbs.ts +49 -0
  51. package/server/src/transcript.ts +57 -4
  52. package/server/src/worker-tools.ts +14 -12
  53. package/shared/knowledge.ts +92 -0
  54. package/shared/mcp.ts +91 -0
  55. package/shared/media.ts +6 -0
  56. package/shared/plain-text.ts +31 -0
  57. package/shared/skills.ts +18 -0
  58. package/shared/sprites.ts +198 -0
  59. package/shared/types.ts +105 -20
  60. package/vendor/claude-bridge/HOB-PATCH.md +10 -0
  61. package/vendor/claude-bridge/LICENSE +21 -0
  62. package/vendor/claude-bridge/package.json +62 -0
  63. package/vendor/claude-bridge/src/agents-md.ts +14 -0
  64. package/vendor/claude-bridge/src/askclaude-schema.ts +92 -0
  65. package/vendor/claude-bridge/src/askclaude-ui.ts +90 -0
  66. package/vendor/claude-bridge/src/attachments.ts +135 -0
  67. package/vendor/claude-bridge/src/config.ts +105 -0
  68. package/vendor/claude-bridge/src/convert.ts +222 -0
  69. package/vendor/claude-bridge/src/extract-tool-results.ts +47 -0
  70. package/vendor/claude-bridge/src/index.ts +2503 -0
  71. package/vendor/claude-bridge/src/log-paths.ts +19 -0
  72. package/vendor/claude-bridge/src/mcp-server.ts +169 -0
  73. package/vendor/claude-bridge/src/models.ts +164 -0
  74. package/vendor/claude-bridge/src/prompt-capture.ts +420 -0
  75. package/vendor/claude-bridge/src/prompt-stream.ts +102 -0
  76. package/vendor/claude-bridge/src/query-state.ts +118 -0
  77. package/vendor/claude-bridge/src/rate-limit.ts +48 -0
  78. package/vendor/claude-bridge/src/session-cwd.ts +38 -0
  79. package/vendor/claude-bridge/src/session-verify.ts +38 -0
  80. package/vendor/claude-bridge/src/skills.ts +20 -0
  81. package/vendor/claude-bridge/src/transcript.ts +86 -0
  82. package/vendor/claude-bridge/src/usage.ts +53 -0
  83. package/vendor/lore-pi-extension/package.json +1 -1
  84. package/vendor/lore-pi-extension/tools.ts +8 -5
  85. package/dist/web/assets/index-C72gAwgN.css +0 -1
  86. package/dist/web/assets/index-Dyw5G8Pg.js +0 -230
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The bridge's debug and diagnostics logs live in pi's agent dir, so a layout
3
+ * that sets PI_CODING_AGENT_DIR keeps them beside pi's other state instead of a
4
+ * fixed ~/.pi/agent. The debug log alone stays overridable, because the test
5
+ * harness and the probes redirect it to a throwaway path.
6
+ *
7
+ * Paths resolve per call rather than at import time, so tests can swap the
8
+ * environment mid-process and see the new paths immediately.
9
+ */
10
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
11
+ import { join } from "path";
12
+
13
+ export function debugLogPath(): string {
14
+ return process.env.CLAUDE_BRIDGE_DEBUG_PATH || join(getAgentDir(), "claude-bridge.log");
15
+ }
16
+
17
+ export function diagLogPath(): string {
18
+ return join(getAgentDir(), "claude-bridge-diag.log");
19
+ }
@@ -0,0 +1,169 @@
1
+ // In-process MCP server that exposes pi tools to Claude Code.
2
+ //
3
+ // Pi declares tool parameters as TypeBox objects, which are already JSON
4
+ // Schema at runtime — the same thing MCP puts on the wire. This serves them
5
+ // verbatim instead of going through the SDK's `createSdkMcpServer`, which only
6
+ // accepts Zod and therefore forces a JSON Schema → Zod → JSON Schema round
7
+ // trip. That round trip is lossy below the top level: nested objects collapse
8
+ // to open records and `anyOf`/`const` vanish, so Claude saw only the first
9
+ // level of any tool with a nested schema — including the builtin `edit`.
10
+ // The one exception is a top-level combinator, flattened below because Claude
11
+ // Code drops a tool whose schema has one.
12
+ //
13
+ // Handlers go on the underlying protocol server rather than through
14
+ // `McpServer.registerTool`, which is the Zod-only path. Skipping registerTool
15
+ // also skips its argument validation, which is what we want: pi validates and
16
+ // executes tools itself, and the arguments MCP sees are discarded. A rejection
17
+ // there would only prevent the handler from running, stranding the call.
18
+ //
19
+ // This rests on the Agent SDK treating what we hand it as an opaque JSON-RPC
20
+ // endpoint: `connectSdkMcpServer` in sdk.mjs calls `instance.connect(transport)`
21
+ // and nothing else, so none of McpServer's higher-level machinery is required.
22
+ // The `McpServer` wrapper is kept only because the SDK's `mcpServers` option is
23
+ // typed against that class. If this breaks after an SDK update, check whether
24
+ // the SDK began inspecting the instance — reading registered tools, or expecting
25
+ // tools/list_changed notifications we never send.
26
+
27
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
29
+ import type { McpResult } from "./extract-tool-results.js";
30
+
31
+ // Claude Code stamps every tools/call with the id of the tool_use block it came
32
+ // from. That is the only reliable way to pair a call with its result: call order
33
+ // is not guaranteed to match the order the tool_use blocks were emitted, so
34
+ // counting calls mispairs results as soon as the two diverge.
35
+ //
36
+ // This is a Claude Code extension, not part of the MCP spec — CC sets it in
37
+ // `src/services/mcp/client.ts` (see reference-code/claude-code-rip). If CC ever
38
+ // stops sending it, every tool call fails with the error below rather than
39
+ // silently pairing results to the wrong call, which is the intended tradeoff.
40
+ const TOOL_USE_ID_META = "claudecode/toolUseId";
41
+
42
+ export interface McpToolDef {
43
+ name: string;
44
+ description: string;
45
+ inputSchema: unknown;
46
+ handler: (toolCallId: string) => Promise<McpResult>;
47
+ }
48
+
49
+ // MCP requires an object schema. Pi types tool parameters as any TypeBox schema,
50
+ // so a scalar or array one typechecks but cannot go on the wire — that is a bug
51
+ // in the tool, and reporting it at startup names the culprit. Degrading it to
52
+ // "takes no arguments" instead would surface much later as Claude calling the
53
+ // tool with no arguments and pi's own validation rejecting them.
54
+ function assertObjectSchema(tool: McpToolDef): void {
55
+ const schema = tool.inputSchema as Record<string, unknown> | undefined;
56
+ if (!schema || schema.type !== "object") {
57
+ throw new Error(`${tool.name}: MCP tool parameters must be an object schema, got ${JSON.stringify(schema)}`);
58
+ }
59
+ }
60
+
61
+ // Claude Code drops an MCP tool outright when its input schema has a top-level
62
+ // anyOf/oneOf/allOf ("its input schema uses top-level anyOf, which the Anthropic
63
+ // API does not accept"; CC's own normalizer for this is gated off for our
64
+ // server). The tool is still in our served-name map, so a call Claude makes to it
65
+ // anyway is forwarded to pi and runs there while CC answers "No such tool
66
+ // available" and retries under a fresh id — the tool runs twice, or the retry's
67
+ // handler waits forever. Advertise a flat object instead, built the way CC's
68
+ // normalizer does: root properties win over branch ones, root `required` stays,
69
+ // and allOf requirements join it (anyOf/oneOf ones hold only for one branch).
70
+ // Like CC, branch `$ref`s into the root's `$defs`/`definitions` are followed,
71
+ // and the lost constraint is restated as a note for the tool description, so
72
+ // Claude's first call is likely to pass pi's validation against the full schema.
73
+ const ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"] as const;
74
+
75
+ function isRecord(value: unknown): value is Record<string, unknown> {
76
+ return typeof value === "object" && value !== null && !Array.isArray(value);
77
+ }
78
+
79
+ function resolveLocalRef(branch: unknown, root: Record<string, unknown>): unknown {
80
+ if (!isRecord(branch) || typeof branch.$ref !== "string") return branch;
81
+ const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(branch.$ref);
82
+ const defs = match && root[match[1]];
83
+ const target = match && isRecord(defs) ? defs[match[2]] : undefined;
84
+ return isRecord(target) ? target : branch;
85
+ }
86
+
87
+ // A branch is named by its required keys, else by all of its keys.
88
+ function branchGroup(branch: unknown): string | null {
89
+ if (!isRecord(branch)) return null;
90
+ const { required, properties } = branch;
91
+ if (Array.isArray(required) && required.length > 0 && required.every((key) => typeof key === "string")) return required.join(", ");
92
+ const keys = isRecord(properties) ? Object.keys(properties) : [];
93
+ return keys.length > 0 ? keys.join(", ") : null;
94
+ }
95
+
96
+ // CC's wording, so the tool reads the same as one CC normalized itself.
97
+ function combinatorNote(combinators: readonly string[], branchesOf: (key: string) => unknown[]): string {
98
+ if (!combinators.includes("anyOf") && !combinators.includes("oneOf")) {
99
+ return "Input constraint: all listed parameters apply together (flattened from a JSON Schema allOf).";
100
+ }
101
+ const key = combinators.includes("oneOf") ? "oneOf" : "anyOf";
102
+ const rule = key === "oneOf" ? "Provide parameters for exactly one of" : "Provide parameters for at least one of";
103
+ const groups = [...new Set(branchesOf(key).map(branchGroup).filter((group) => group !== null))];
104
+ if (groups.length === 0) return `Input constraint: ${rule} the documented parameter groups (flattened from a JSON Schema ${key}).`;
105
+ return `Input constraint: ${rule}: ${groups.map((group) => `(${group})`).join(" or ")}.`;
106
+ }
107
+
108
+ function flattenRootCombinators(schema: Record<string, unknown>): { schema: Record<string, unknown>; note?: string } {
109
+ const combinators = ROOT_COMBINATORS.filter((key) => key in schema);
110
+ if (combinators.length === 0) return { schema };
111
+
112
+ const { anyOf: _anyOf, oneOf: _oneOf, allOf: _allOf, ...rest } = schema;
113
+ const branchesOf = (key: string) => {
114
+ const branches = schema[key];
115
+ return Array.isArray(branches) ? branches.map((branch) => resolveLocalRef(branch, schema)) : [];
116
+ };
117
+ const properties: Record<string, unknown> = {};
118
+ const required: string[] = [];
119
+ const addProperties = (source: unknown) => {
120
+ if (!isRecord(source) || !isRecord(source.properties)) return;
121
+ for (const [key, value] of Object.entries(source.properties)) if (!(key in properties)) properties[key] = value;
122
+ };
123
+ const addRequired = (source: unknown) => {
124
+ if (!isRecord(source) || !Array.isArray(source.required)) return;
125
+ for (const key of source.required) if (typeof key === "string" && !required.includes(key)) required.push(key);
126
+ };
127
+
128
+ addProperties(schema);
129
+ for (const key of combinators) for (const branch of branchesOf(key)) addProperties(branch);
130
+ addRequired(schema);
131
+ for (const branch of branchesOf("allOf")) addRequired(branch);
132
+
133
+ const flat: Record<string, unknown> = { ...rest, type: "object", properties };
134
+ if (required.length > 0) flat.required = required;
135
+ else delete flat.required;
136
+ return { schema: flat, note: combinatorNote(combinators, branchesOf) };
137
+ }
138
+
139
+ export function createToolServer(name: string, tools: McpToolDef[]) {
140
+ const server = new McpServer({ name, version: "1.0.0" }, { capabilities: { tools: {} } });
141
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
142
+ for (const tool of tools) assertObjectSchema(tool);
143
+
144
+ server.server.setRequestHandler(ListToolsRequestSchema, () => ({
145
+ tools: tools.map((tool) => {
146
+ const { schema, note } = flattenRootCombinators(tool.inputSchema as Record<string, unknown>);
147
+ return {
148
+ name: tool.name,
149
+ description: [note, tool.description].filter(Boolean).join("\n\n"),
150
+ inputSchema: schema,
151
+ };
152
+ }),
153
+ }));
154
+
155
+ server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
+ const tool = byName.get(request.params.name);
157
+ if (!tool) throw new Error(`Unknown tool: ${request.params.name}`);
158
+ const toolCallId = request.params._meta?.[TOOL_USE_ID_META];
159
+ if (typeof toolCallId !== "string") {
160
+ throw new Error(`${tool.name}: tools/call is missing _meta["${TOOL_USE_ID_META}"] — cannot pair the result with its tool call`);
161
+ }
162
+ // Narrowed deliberately: McpResult also carries `toolCallId`, which is our
163
+ // own bookkeeping for pairing and not part of MCP's CallToolResult.
164
+ const { content, isError } = await tool.handler(toolCallId);
165
+ return { content, isError };
166
+ });
167
+
168
+ return { type: "sdk" as const, name, instance: server };
169
+ }
@@ -0,0 +1,164 @@
1
+ // Model selection + display-order policy for the model picker. The picker is
2
+ // driven by pi-ai's anthropic catalog: models appear (and disappear) with it,
3
+ // no per-model code here. Extracted from index.ts so tests can import without
4
+ // activating the extension.
5
+ // `resolveModel` resolves family shortcuts (opus/sonnet/fable) to the newest
6
+ // matching id regardless of sort order; sort order only drives picker display.
7
+
8
+ const TWO_HUNDRED_K_CONTEXT = 200_000;
9
+ const ONE_M_CONTEXT = 1_000_000;
10
+
11
+ // pi-ai ships dated snapshot ids (claude-opus-4-5-20251101, ...) alongside the
12
+ // bare ids. They are never exposed - and must not steal first-partial-match
13
+ // shortcuts like "opus-4-5" from the bare id.
14
+ function isDatedAlias(id: string): boolean {
15
+ return /-20\d{6}$/.test(id);
16
+ }
17
+
18
+ // Family tiers for display order: flagship families first; unknown families
19
+ // sink below all known ones.
20
+ const FAMILY_ORDER = ["fable", "opus", "sonnet", "haiku"];
21
+
22
+ // Project pi-ai's model entries down to the fields pi's registerProvider expects,
23
+ // newest generation first. Context-dependent display labels are applied after
24
+ // plan/long-context config is known.
25
+ // Version rank of a claude id, e.g. claude-opus-4-7 → ["opus", 4, 7]. Shared by
26
+ // the display sort and resolveModel's newest-first partial tiebreak.
27
+ function versionRank(id: string): { family: string; tuple: [number, number] } {
28
+ const [, family, major, minor] = id.split("-");
29
+ return { family, tuple: [Number(major) || 0, Number(minor) || 0] };
30
+ }
31
+
32
+ export function buildModels<T extends { id: string; [key: string]: any }>(piAiModels: T[]) {
33
+ return piAiModels
34
+ .filter((m) => typeof m.id === "string" && !isDatedAlias(m.id))
35
+ .sort((a, b) => {
36
+ const fa = FAMILY_ORDER.indexOf(versionRank(a.id).family);
37
+ const fb = FAMILY_ORDER.indexOf(versionRank(b.id).family);
38
+ const ta = fa === -1 ? FAMILY_ORDER.length : fa;
39
+ const tb = fb === -1 ? FAMILY_ORDER.length : fb;
40
+ if (ta !== tb) return ta - tb;
41
+ const ra = versionRank(a.id).tuple;
42
+ const rb = versionRank(b.id).tuple;
43
+ if (ra[0] !== rb[0]) return rb[0] - ra[0];
44
+ if (ra[1] !== rb[1]) return rb[1] - ra[1];
45
+ return a.id.localeCompare(b.id);
46
+ })
47
+ // Forward thinkingLevelMap so pi-ai's per-model overrides (e.g. opus-4-8
48
+ // mapping xhigh→xhigh and max→max) are visible to the effort lookup.
49
+ .map(({ id, name, reasoning, input, contextWindow, maxTokens, thinkingLevelMap }) => ({
50
+ id,
51
+ name,
52
+ reasoning, input, contextWindow, maxTokens,
53
+ thinkingLevelMap,
54
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
55
+ }));
56
+ }
57
+
58
+ export type LongContextSettings = {
59
+ plan: "pro" | "max";
60
+ longContextExtraUsage: boolean;
61
+ // Model ids whose declared 1M context Claude Code turned out not to serve;
62
+ // forces bare id at 200K without a code change.
63
+ forceTwoHundredK?: string[];
64
+ };
65
+
66
+ export type ClaudeCodeRuntimeModel = {
67
+ cliModelId: string;
68
+ contextWindow: number;
69
+ };
70
+
71
+ // Measured Claude Agent SDK behavior - see diag/CONTEXT-SIZE.md:
72
+ // - The `[1m]` suffix is the only reliable way to request 1M context through
73
+ // the SDK; bare ids serve 200K.
74
+ // - An unentitled `[1m]` id is rejected outright (400/429), failing every turn
75
+ // — worse than serving 200K, so the default is bare id at 200K and only
76
+ // measured-good ids get `[1m]`.
77
+ // - The registered contextWindow must match the window the bridge actually
78
+ // requests, or pi's status bar and compaction threshold misreport.
79
+ // [1m] ids verified to serve 1M on every plan (sonnet-5-5 measured on Pro with
80
+ // and without Extra Usage; opus-5-5 on Max per its run notes). A new model
81
+ // serves 200K until someone measures it (diag/context-size.mjs) and adds it
82
+ // here. Known exception unrelated to long context: fable-5 is not included
83
+ // with Pro account without Extra Usage.
84
+ const MEASURED_ONE_M = new Set([
85
+ "claude-fable-5",
86
+ "claude-fable-5-1",
87
+ "claude-opus-5-5",
88
+ "claude-opus-5",
89
+ "claude-opus-4-8",
90
+ "claude-opus-4-7",
91
+ "claude-sonnet-5",
92
+ "claude-sonnet-5-5",
93
+ "claude-haiku-5-5",
94
+ ]);
95
+
96
+ // Measured exceptions: pi-ai declares 1M and the [1m] id works, but only when
97
+ // the plan allows it.
98
+ const PLAN_GATED_ONE_M: Record<string, (settings: LongContextSettings) => boolean> = {
99
+ // [1m] measured 1M on Max plan / extra usage; 429 on Pro without it.
100
+ "claude-opus-4-6": (settings) => settings.plan === "max" || settings.longContextExtraUsage,
101
+ // [1m] measured 1M with extra usage only.
102
+ "claude-sonnet-4-6": (settings) => settings.longContextExtraUsage,
103
+ };
104
+
105
+ export function resolveClaudeCodeRuntimeModel(
106
+ model: { id: string },
107
+ settings: LongContextSettings,
108
+ ): ClaudeCodeRuntimeModel {
109
+ const modelId = model.id;
110
+ if (settings.forceTwoHundredK?.includes(modelId)) {
111
+ return { cliModelId: modelId, contextWindow: TWO_HUNDRED_K_CONTEXT };
112
+ }
113
+ if (MEASURED_ONE_M.has(modelId)) {
114
+ return { cliModelId: `${modelId}[1m]`, contextWindow: ONE_M_CONTEXT };
115
+ }
116
+ const planGate = PLAN_GATED_ONE_M[modelId];
117
+ if (planGate) {
118
+ const useOneM = planGate(settings);
119
+ return {
120
+ cliModelId: useOneM ? `${modelId}[1m]` : modelId,
121
+ contextWindow: useOneM ? ONE_M_CONTEXT : TWO_HUNDRED_K_CONTEXT,
122
+ };
123
+ }
124
+ // No measured row: bare id at 200K, the safe default (see diag/CONTEXT-SIZE.md).
125
+ return { cliModelId: modelId, contextWindow: TWO_HUNDRED_K_CONTEXT };
126
+ }
127
+
128
+ export function claudeCodeModelId(model: { id: string }, settings: LongContextSettings): string {
129
+ return resolveClaudeCodeRuntimeModel(model, settings).cliModelId;
130
+ }
131
+
132
+ export function resolveModel<T extends { id: string }>(models: T[], input: string): T | undefined {
133
+ const lower = input.toLowerCase();
134
+ // Exact first, then partial (mirrors pi's tryMatchModel ordering), so a
135
+ // longer newer id containing the input (claude-fable-5-1 vs "claude-fable-5")
136
+ // cannot shadow the exact match.
137
+ return models.find((m) => m.id === lower)
138
+ ?? newestPartialMatch(models.filter((m) => m.id.includes(lower)));
139
+ }
140
+
141
+ // Newest match by version rank — independent of registration order.
142
+ function newestPartialMatch<T extends { id: string }>(candidates: T[]): T | undefined {
143
+ if (candidates.length === 0) return undefined;
144
+ return candidates.reduce((best, m) => {
145
+ const [vb, vbest] = [versionRank(m.id).tuple, versionRank(best.id).tuple];
146
+ const newer = vb[0] !== vbest[0] ? vb[0] > vbest[0] : vb[1] > vbest[1];
147
+ return newer ? m : best;
148
+ });
149
+ }
150
+
151
+ // Produce the model metadata registered with pi. The registered contextWindow must
152
+ // match the window the bridge actually requests from Claude Code, or pi's status
153
+ // bar and auto-compaction threshold will misreport. The runtime policy is based
154
+ // on measured SDK behavior - see diag/CONTEXT-SIZE.md
155
+ export function applyLongContext<T extends { id: string; name: string; contextWindow?: number | null }>(
156
+ models: T[],
157
+ settings: LongContextSettings,
158
+ ): T[] {
159
+ return models.map((m) => {
160
+ const { contextWindow } = resolveClaudeCodeRuntimeModel(m, settings);
161
+ const name = contextWindow > TWO_HUNDRED_K_CONTEXT && !/\b1M\b/i.test(m.name) ? `${m.name} 1M` : m.name;
162
+ return contextWindow === m.contextWindow && name === m.name ? m : { ...m, contextWindow, name };
163
+ });
164
+ }