one-code-extension 0.1.4 → 0.3.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.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model each role runs on, and why (pure).
|
|
3
|
+
*
|
|
4
|
+
* The doctor does not re-decide anything: it calls the SAME resolvers the
|
|
5
|
+
* session uses — `resolveSubagentModel` for delegated work, `classifierCandidates`
|
|
6
|
+
* for auto mode's screener, `pickEconomicalContainedModel` for the web-fetch /
|
|
7
|
+
* recap reader, `resolveModelTier` for the prompt register — and reports their
|
|
8
|
+
* answers next to their sources. A user who asks "why did my subagent run on
|
|
9
|
+
* Haiku?" gets the resolver's own notice, not a paraphrase.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
13
|
+
import { loadAutoModeConfig } from "../auto-mode/config.ts";
|
|
14
|
+
import { classifierCandidates, describeCandidate, type ClassifierNotice } from "../auto-mode/model-select.ts";
|
|
15
|
+
import { ATTRIBUTION, capabilityFloor, configuredCapabilityKey, type FloorRole, type FloorVerdict, KEY_ADVICE, snapshotAgeMs } from "../lib/capability-index.ts";
|
|
16
|
+
import { modelSpec, pricedInput } from "../lib/model-policy.ts";
|
|
17
|
+
import {
|
|
18
|
+
currentCapabilitySnapshot,
|
|
19
|
+
intrinsicTier,
|
|
20
|
+
pickEconomicalContainedModel,
|
|
21
|
+
type PromptTier,
|
|
22
|
+
resolveModelTier,
|
|
23
|
+
tierOverride,
|
|
24
|
+
} from "../lib/model-tier.ts";
|
|
25
|
+
import { applicableSubagentDefault, loadSubagentDefault, type SubagentDefault } from "../subagents/default-model.ts";
|
|
26
|
+
import { resolveSubagentModel, type SubagentModelResolution } from "../subagents/model-select.ts";
|
|
27
|
+
import { contextLabel, type Finding, priceLabel, type ReportLine, type ReportSection, type SessionView } from "./report.ts";
|
|
28
|
+
|
|
29
|
+
export interface ModelFacts {
|
|
30
|
+
session?: Model<Api>;
|
|
31
|
+
sessionTier?: PromptTier;
|
|
32
|
+
/** The register the system prompt is built in — the tier, or a CC_PROMPT_TIER override. */
|
|
33
|
+
promptTier: PromptTier;
|
|
34
|
+
promptTierForced: boolean;
|
|
35
|
+
subagent: SubagentModelResolution;
|
|
36
|
+
subagentConfigured?: SubagentDefault;
|
|
37
|
+
/** The configured default exists but does not apply to this session (Claude Code's env var on a non-Claude model). */
|
|
38
|
+
subagentConfiguredInapplicable: boolean;
|
|
39
|
+
classifier: { model?: Model<Api>; description?: string; notices: ClassifierNotice[]; configured?: string };
|
|
40
|
+
reader?: { model: Model<Api>; via: "tier" | "session" };
|
|
41
|
+
/**
|
|
42
|
+
* The optional Artificial Analysis snapshot behind the measured capability
|
|
43
|
+
* floor (lib/capability-index.ts): whether a key is configured, the snapshot's
|
|
44
|
+
* age, and the verdict each automatic pick was judged on.
|
|
45
|
+
*/
|
|
46
|
+
capability: {
|
|
47
|
+
keyConfigured: boolean;
|
|
48
|
+
snapshot?: { fetchedAt: string; rows: number };
|
|
49
|
+
subagent?: FloorVerdict;
|
|
50
|
+
classifier?: FloorVerdict;
|
|
51
|
+
reader?: FloorVerdict;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const TIER_LABEL: Record<PromptTier, string> = {
|
|
56
|
+
frontier: "frontier tier — terse Claude Code prompt",
|
|
57
|
+
workhorse: "workhorse tier — full Claude Code prompt",
|
|
58
|
+
cheap: "cheap tier — full prompt, Haiku register",
|
|
59
|
+
tiny: "tiny tier — full prompt plus weak-model scaffolding and grep/find/ls",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function collectModelFacts(available: Model<Api>[], session: SessionView, home: string, env: NodeJS.ProcessEnv): ModelFacts {
|
|
63
|
+
const sessionModel = session.model;
|
|
64
|
+
const configuredAll = loadSubagentDefault(home, env);
|
|
65
|
+
const configured = applicableSubagentDefault(configuredAll, sessionModel);
|
|
66
|
+
const subagent = resolveSubagentModel({ configuredDefault: configured, sessionModel, available });
|
|
67
|
+
const autoConfig = loadAutoModeConfig(home);
|
|
68
|
+
const chain = classifierCandidates({
|
|
69
|
+
available,
|
|
70
|
+
sessionModel,
|
|
71
|
+
configured: autoConfig.classifierModel,
|
|
72
|
+
configuredSetForContainment: autoConfig.classifierModelSetFor,
|
|
73
|
+
});
|
|
74
|
+
const first = chain.candidates[0];
|
|
75
|
+
const reader = pickEconomicalContainedModel(available, sessionModel);
|
|
76
|
+
const snapshot = currentCapabilitySnapshot();
|
|
77
|
+
const verdict = (pick: Model<Api> | undefined, role: FloorRole): FloorVerdict | undefined =>
|
|
78
|
+
snapshot && sessionModel && pick && modelSpec(pick) !== modelSpec(sessionModel) ? capabilityFloor(snapshot, pick, sessionModel, role) : undefined;
|
|
79
|
+
const capability: ModelFacts["capability"] = {
|
|
80
|
+
keyConfigured: configuredCapabilityKey(home, env) !== undefined,
|
|
81
|
+
snapshot: snapshot ? { fetchedAt: snapshot.fetchedAt, rows: snapshot.rows.length } : undefined,
|
|
82
|
+
subagent: verdict(subagent.source === "automatic" ? subagent.model : undefined, "subagent"),
|
|
83
|
+
classifier: verdict(first?.source === "economical" ? first.model : undefined, "classifier"),
|
|
84
|
+
reader: verdict(reader?.via === "tier" ? reader.model : undefined, "reader"),
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
capability,
|
|
88
|
+
session: sessionModel,
|
|
89
|
+
sessionTier: sessionModel ? intrinsicTier(sessionModel) : undefined,
|
|
90
|
+
promptTier: resolveModelTier(sessionModel, env),
|
|
91
|
+
promptTierForced: tierOverride(env) !== undefined,
|
|
92
|
+
subagent,
|
|
93
|
+
subagentConfigured: configuredAll,
|
|
94
|
+
subagentConfiguredInapplicable: configuredAll !== undefined && configured === undefined,
|
|
95
|
+
classifier: {
|
|
96
|
+
model: first?.model,
|
|
97
|
+
description: first ? describeCandidate(first) : undefined,
|
|
98
|
+
notices: chain.notices,
|
|
99
|
+
configured: autoConfig.classifierModel,
|
|
100
|
+
},
|
|
101
|
+
reader,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SUBAGENT_SOURCE: Record<SubagentModelResolution["source"], string> = {
|
|
106
|
+
call: "named per call",
|
|
107
|
+
agent: "from the agent file",
|
|
108
|
+
default: "the configured default",
|
|
109
|
+
automatic: "automatic: cheapest capable model on this provider, cheaper than the main model",
|
|
110
|
+
session: "the main model — nothing cheaper and capable on this provider",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export function modelsSection(facts: ModelFacts, session: SessionView, findings: Finding[]): ReportSection {
|
|
114
|
+
const lines: ReportLine[] = [];
|
|
115
|
+
const { session: main } = facts;
|
|
116
|
+
|
|
117
|
+
if (!main) {
|
|
118
|
+
lines.push({
|
|
119
|
+
text: "Main: none — no model is available, so a session cannot start work yet",
|
|
120
|
+
level: "error",
|
|
121
|
+
});
|
|
122
|
+
return { title: "Models", lines };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sourceNote =
|
|
126
|
+
session.modelSource === "session"
|
|
127
|
+
? "this session"
|
|
128
|
+
: session.modelSource === "default-setting"
|
|
129
|
+
? "saved default; the next start uses it"
|
|
130
|
+
: session.modelSource === "first-available"
|
|
131
|
+
? "no saved default — pi picks the first available model at startup"
|
|
132
|
+
: "";
|
|
133
|
+
const details = [priceLabel(main), contextLabel(main.contextWindow), sourceNote].filter(Boolean).join(" · ");
|
|
134
|
+
lines.push({ text: `Main: ${modelSpec(main)} — ${details}`, level: "ok" });
|
|
135
|
+
lines.push({
|
|
136
|
+
text: `Prompt register: ${TIER_LABEL[facts.promptTier]}${facts.promptTierForced ? " (forced by CC_PROMPT_TIER)" : ""}`,
|
|
137
|
+
indent: 1,
|
|
138
|
+
level: "dim",
|
|
139
|
+
});
|
|
140
|
+
if (session.thinkingLevel) lines.push({ text: `Effort: ${session.thinkingLevel} (/effort or shift+tab to change)`, indent: 1, level: "dim" });
|
|
141
|
+
|
|
142
|
+
const sub = facts.subagent;
|
|
143
|
+
if (sub.model) {
|
|
144
|
+
lines.push({ text: `Subagents and workflow agents: ${modelSpec(sub.model)} — ${SUBAGENT_SOURCE[sub.source]}`, level: "ok" });
|
|
145
|
+
if (facts.subagentConfigured) {
|
|
146
|
+
const knob = facts.subagentConfigured.source === "subagentModel setting" ? "subagentModel in ~/.onecode/settings.json" : "CLAUDE_CODE_SUBAGENT_MODEL";
|
|
147
|
+
lines.push({
|
|
148
|
+
text: `Setting: "${facts.subagentConfigured.spec}" via ${knob}${facts.subagentConfiguredInapplicable ? " — not applied: Claude Code's knob applies only when the main model is a Claude model" : ""}`,
|
|
149
|
+
indent: 1,
|
|
150
|
+
level: facts.subagentConfiguredInapplicable ? "warn" : "dim",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
for (const notice of sub.notices) {
|
|
154
|
+
lines.push({ text: notice, indent: 1, level: "warn" });
|
|
155
|
+
findings.push({ level: "warn", text: `Subagent model: ${notice}`, fix: "Re-set the default with /subagent on this session, or /subagent clear." });
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
lines.push({ text: "Subagents: no model resolves", level: "error" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const classifier = facts.classifier;
|
|
162
|
+
if (classifier.model) {
|
|
163
|
+
const live = session.permission?.classifier;
|
|
164
|
+
const pinned = session.permission?.pinned && live ? ` — screening this session on ${live}` : "";
|
|
165
|
+
lines.push({ text: `Auto-mode classifier: ${classifier.description ?? modelSpec(classifier.model)}${pinned}`, level: "ok" });
|
|
166
|
+
} else {
|
|
167
|
+
lines.push({ text: "Auto-mode classifier: none — auto mode stays out of the mode cycle until a model is available", level: "warn" });
|
|
168
|
+
}
|
|
169
|
+
for (const notice of classifier.notices) {
|
|
170
|
+
lines.push({ text: notice.text, indent: 1, level: notice.level === "warning" ? "warn" : "dim" });
|
|
171
|
+
if (notice.level === "warning") {
|
|
172
|
+
findings.push({ level: "warn", text: `Classifier: ${notice.text}`, fix: "Pick a model with /auto-mode model, or /auto-mode model clear for the automatic choice." });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (facts.reader) {
|
|
177
|
+
lines.push({
|
|
178
|
+
text: `Web-fetch and recap reader: ${modelSpec(facts.reader.model)} — ${facts.reader.via === "tier" ? "cheapest capable model on this provider" : "the main model"}`,
|
|
179
|
+
level: "dim",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The measured capability floor: what each automatic pick was judged on, or
|
|
184
|
+
// how to switch it on. Scores shown carry the attribution the free API requires.
|
|
185
|
+
const cap = facts.capability;
|
|
186
|
+
const verdictLine = (label: string, v: FloorVerdict | undefined): ReportLine | undefined => {
|
|
187
|
+
if (!v) return undefined;
|
|
188
|
+
if (v.verdict === "unscored") return { text: `${label}: not measured — ${v.reason ?? "no confirmed score"}; the name-class rule decided`, indent: 1, level: "dim" };
|
|
189
|
+
const basis = v.candidate?.variant === "non-reasoning" ? "thinking-off" : "default-effort";
|
|
190
|
+
return {
|
|
191
|
+
text: `${label}: coding index ${v.candidate?.coding} vs floor ${v.floor?.toFixed(1)} (${basis}; session ${v.session?.coding}, Sonnet 5 ${v.reference?.coding}) — ${v.verdict === "pass" ? "measured capable" : "below the floor"}`,
|
|
192
|
+
indent: 1,
|
|
193
|
+
level: "dim",
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
if (cap.snapshot) {
|
|
197
|
+
const ageH = Math.round(snapshotAgeMs({ fetchedAt: cap.snapshot.fetchedAt, source: "", rows: [] }) / 3_600_000);
|
|
198
|
+
lines.push({ text: `Capability scores: Artificial Analysis snapshot, ${cap.snapshot.rows} models, ${ageH} h old — measured picks for subagents and the classifier`, level: "ok" });
|
|
199
|
+
for (const line of [verdictLine("Subagent pick", cap.subagent), verdictLine("Classifier pick", cap.classifier), verdictLine("Reader pick", cap.reader)]) {
|
|
200
|
+
if (line) lines.push(line);
|
|
201
|
+
}
|
|
202
|
+
lines.push({ text: ATTRIBUTION, indent: 1, level: "dim" });
|
|
203
|
+
} else if (cap.keyConfigured) {
|
|
204
|
+
lines.push({ text: "Capability scores: key configured, snapshot not fetched yet — it downloads in the background on the next interactive start", level: "dim" });
|
|
205
|
+
} else {
|
|
206
|
+
lines.push({ text: "Capability scores: none — automatic picks use model names and generations only", level: "warn" });
|
|
207
|
+
findings.push({ level: "warn", text: "No Artificial Analysis key: subagent and classifier picks cannot be judged by measured coding ability.", fix: KEY_ADVICE });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (session.permission) {
|
|
211
|
+
const mode = session.permission.mode;
|
|
212
|
+
const from = session.permission.source ? ` (${session.permission.source})` : "";
|
|
213
|
+
lines.push({ text: `Permission mode: ${mode}${from} — ctrl+q cycles, /permissions lists the rules`, level: "dim" });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (facts.sessionTier === "tiny") {
|
|
217
|
+
findings.push({
|
|
218
|
+
level: "warn",
|
|
219
|
+
text: `${modelSpec(main)} is classed as a tiny-tier model: One Code adds weak-model scaffolding and search tools, and never auto-selects a tiny model for subagents or the classifier.`,
|
|
220
|
+
fix: "For coding work pick a cheap- or workhorse-tier model with /model; see /doctor presets.",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (pricedInput(main) === undefined) {
|
|
224
|
+
lines.push({
|
|
225
|
+
text: "This model carries no price in the catalog, so automatic cost-based choices (cheaper subagents, a cheaper classifier) fall back to the main model.",
|
|
226
|
+
indent: 1,
|
|
227
|
+
level: "dim",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return { title: "Models", lines };
|
|
231
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model presets (pure): three named configurations — economical, balanced,
|
|
3
|
+
* maximum quality — computed against the models the user can actually run,
|
|
4
|
+
* shown with the concrete model each role would get, and applied with
|
|
5
|
+
* `/doctor preset <name>`.
|
|
6
|
+
*
|
|
7
|
+
* Presets never leave the current provider family: every automatic model choice
|
|
8
|
+
* in One Code is contained to the session's provider (a subagent inherits the
|
|
9
|
+
* transcript, the classifier reads the prompts — docs/decisions/model-policy.md),
|
|
10
|
+
* and a preset is a bundle of those same choices. A user who wants another
|
|
11
|
+
* provider switches with /model first and reruns /doctor presets there.
|
|
12
|
+
*
|
|
13
|
+
* Each preset pins ONE thing — the main model — and sets the two secondary
|
|
14
|
+
* knobs to the shape that matches its intent:
|
|
15
|
+
* economical main = cheapest capable (cheap tier, never tiny); subagents inherit it;
|
|
16
|
+
* classifier automatic (lands on the same model)
|
|
17
|
+
* balanced main = cheapest workhorse-tier model; subagents automatic (the
|
|
18
|
+
* cheapest model at the main's floor — often the main itself,
|
|
19
|
+
* since subagents share the classifier's workhorse floor);
|
|
20
|
+
* classifier automatic (workhorse floor)
|
|
21
|
+
* maximum quality main = the strongest (frontier, else priciest workhorse);
|
|
22
|
+
* subagents inherit it; classifier automatic
|
|
23
|
+
* so applying a preset is three settings writes the user can each undo by hand
|
|
24
|
+
* (/model, /subagent, /auto-mode model). The rows display what the resolvers
|
|
25
|
+
* would pick for that main model, computed with the resolvers themselves.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
29
|
+
import { classifierCandidates } from "../auto-mode/model-select.ts";
|
|
30
|
+
import { isPriorGeneration, lacksToolCalls } from "../lib/model-facts.ts";
|
|
31
|
+
import { isDatedDuplicate, modelsContainedToSession, modelSpec, pricedInput } from "../lib/model-policy.ts";
|
|
32
|
+
import { intrinsicTier, type PromptTier } from "../lib/model-tier.ts";
|
|
33
|
+
import { resolveSubagentModel } from "../subagents/model-select.ts";
|
|
34
|
+
import type { ReportLine, ReportSection } from "./report.ts";
|
|
35
|
+
|
|
36
|
+
export type PresetName = "economical" | "balanced" | "quality";
|
|
37
|
+
|
|
38
|
+
export const PRESET_NAMES: readonly PresetName[] = ["economical", "balanced", "quality"];
|
|
39
|
+
|
|
40
|
+
export const PRESET_LABEL: Record<PresetName, string> = {
|
|
41
|
+
economical: "economical",
|
|
42
|
+
balanced: "balanced",
|
|
43
|
+
quality: "maximum quality",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const PRESET_INTENT: Record<PresetName, string> = {
|
|
47
|
+
economical: "lowest cost — one cheap model for everything",
|
|
48
|
+
balanced: "a capable main model, delegated work on the cheapest model at its floor",
|
|
49
|
+
quality: "the strongest model for the main session and its subagents",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface PresetPlan {
|
|
53
|
+
name: PresetName;
|
|
54
|
+
label: string;
|
|
55
|
+
main: Model<Api>;
|
|
56
|
+
/** What subagents resolve to under this preset, and the setting that produces it. */
|
|
57
|
+
subagents: { model: Model<Api>; setting: "inherit" | "auto" };
|
|
58
|
+
classifier?: Model<Api>;
|
|
59
|
+
/** True when the main session already runs this preset's main model. */
|
|
60
|
+
current: boolean;
|
|
61
|
+
/** Why the pick is weaker than the preset promises (one provider, one model…). */
|
|
62
|
+
note?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Only well-known reasons a preset list is empty; the section renders them. */
|
|
66
|
+
export type PresetsUnavailable = "no-model" | "no-priced-models";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The candidate pool for one provider family: contained, priced, dated duplicates
|
|
70
|
+
* collapsed when the undated alias is present (`claude-haiku-4-5-20251001` next
|
|
71
|
+
* to `claude-haiku-4-5`). Tiny-tier rows stay in the pool only as a last resort.
|
|
72
|
+
*/
|
|
73
|
+
export function presetPool(available: Model<Api>[], sessionModel: Model<Api>): Model<Api>[] {
|
|
74
|
+
const contained = modelsContainedToSession(available, sessionModel).filter(
|
|
75
|
+
// A preset recommends a MAIN model: never one a generation behind its family
|
|
76
|
+
// or one models.dev says cannot call tools (model-facts.ts), whatever its tier.
|
|
77
|
+
(m) => pricedInput(m) !== undefined && !isPriorGeneration(m) && !lacksToolCalls(m),
|
|
78
|
+
);
|
|
79
|
+
return contained.filter((m) => !isDatedDuplicate(m, contained));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const price = (m: Model<Api>): number => pricedInput(m) ?? 0;
|
|
83
|
+
const byTier = (pool: Model<Api>[], tier: PromptTier): Model<Api>[] => pool.filter((m) => intrinsicTier(m) === tier);
|
|
84
|
+
const cheapest = (models: Model<Api>[]): Model<Api> | undefined => [...models].sort((a, b) => price(a) - price(b))[0];
|
|
85
|
+
const priciest = (models: Model<Api>[]): Model<Api> | undefined => [...models].sort((a, b) => price(b) - price(a))[0];
|
|
86
|
+
|
|
87
|
+
function pickMain(name: PresetName, pool: Model<Api>[]): { model: Model<Api>; note?: string } | undefined {
|
|
88
|
+
const cheap = byTier(pool, "cheap");
|
|
89
|
+
const workhorse = byTier(pool, "workhorse");
|
|
90
|
+
const frontier = byTier(pool, "frontier");
|
|
91
|
+
const capable = [...cheap, ...workhorse, ...frontier];
|
|
92
|
+
switch (name) {
|
|
93
|
+
case "economical": {
|
|
94
|
+
const pick = cheapest(cheap) ?? cheapest(workhorse) ?? cheapest(frontier);
|
|
95
|
+
if (pick) return { model: pick, note: cheap.length === 0 ? "no cheap-tier model on this provider; the cheapest capable one serves" : undefined };
|
|
96
|
+
const tiny = cheapest(pool);
|
|
97
|
+
return tiny ? { model: tiny, note: "only tiny-tier models on this provider — expect weaker results" } : undefined;
|
|
98
|
+
}
|
|
99
|
+
case "balanced": {
|
|
100
|
+
const pick = cheapest(workhorse) ?? cheapest(frontier) ?? priciest(cheap);
|
|
101
|
+
if (pick) return { model: pick, note: workhorse.length === 0 ? "no workhorse-tier model on this provider" : undefined };
|
|
102
|
+
const any = priciest(pool);
|
|
103
|
+
return any ? { model: any, note: "only tiny-tier models on this provider — expect weaker results" } : undefined;
|
|
104
|
+
}
|
|
105
|
+
case "quality": {
|
|
106
|
+
const pick = priciest(frontier) ?? priciest(workhorse) ?? priciest(capable) ?? priciest(pool);
|
|
107
|
+
if (!pick) return undefined;
|
|
108
|
+
const note =
|
|
109
|
+
frontier.length === 0 && workhorse.length === 0
|
|
110
|
+
? "no frontier- or workhorse-tier model on this provider"
|
|
111
|
+
: frontier.length === 0
|
|
112
|
+
? "no frontier-tier model on this provider; the strongest workhorse serves"
|
|
113
|
+
: undefined;
|
|
114
|
+
return { model: pick, note };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The three presets for the session's provider family. Empty with a reason when
|
|
121
|
+
* there is no session model (nothing to contain to) or no priced model (tiers
|
|
122
|
+
* cannot be told apart, so a preset would be a guess).
|
|
123
|
+
*/
|
|
124
|
+
export function computePresets(
|
|
125
|
+
available: Model<Api>[],
|
|
126
|
+
sessionModel: Model<Api> | undefined,
|
|
127
|
+
): { presets: PresetPlan[]; unavailable?: PresetsUnavailable } {
|
|
128
|
+
if (!sessionModel) return { presets: [], unavailable: "no-model" };
|
|
129
|
+
const pool = presetPool(available, sessionModel);
|
|
130
|
+
if (pool.length === 0) return { presets: [], unavailable: "no-priced-models" };
|
|
131
|
+
|
|
132
|
+
const presets: PresetPlan[] = [];
|
|
133
|
+
for (const name of PRESET_NAMES) {
|
|
134
|
+
const main = pickMain(name, pool);
|
|
135
|
+
if (!main) continue;
|
|
136
|
+
const inherit = name !== "balanced";
|
|
137
|
+
// The resolvers judge the preset's main model as if it were the session's,
|
|
138
|
+
// over the same catalog the live session uses, so the preview names exactly
|
|
139
|
+
// what applying the preset produces.
|
|
140
|
+
const subagentModel = inherit
|
|
141
|
+
? main.model
|
|
142
|
+
: (resolveSubagentModel({ sessionModel: main.model, available }).model ?? main.model);
|
|
143
|
+
const classifier = classifierCandidates({ available, sessionModel: main.model }).candidates[0]?.model;
|
|
144
|
+
presets.push({
|
|
145
|
+
name,
|
|
146
|
+
label: PRESET_LABEL[name],
|
|
147
|
+
main: main.model,
|
|
148
|
+
subagents: { model: subagentModel, setting: inherit ? "inherit" : "auto" },
|
|
149
|
+
classifier,
|
|
150
|
+
current: modelSpec(main.model) === modelSpec(sessionModel),
|
|
151
|
+
note: main.note,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return { presets };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function findPreset(presets: PresetPlan[], raw: string): PresetPlan | undefined {
|
|
158
|
+
const wanted = raw.trim().toLowerCase().replace(/\s+/g, "-");
|
|
159
|
+
const aliases: Record<string, PresetName> = { economical: "economical", economy: "economical", cheap: "economical", balanced: "balanced", balance: "balanced", quality: "quality", "maximum-quality": "quality", max: "quality", maximum: "quality", best: "quality" };
|
|
160
|
+
const name = aliases[wanted];
|
|
161
|
+
return name ? presets.find((p) => p.name === name) : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const shortId = (m: Model<Api>): string => m.id;
|
|
165
|
+
|
|
166
|
+
export function presetsSection(result: ReturnType<typeof computePresets>, sessionModel: Model<Api> | undefined): ReportSection {
|
|
167
|
+
const lines: ReportLine[] = [];
|
|
168
|
+
if (result.unavailable === "no-model") {
|
|
169
|
+
lines.push({ text: "Connect a provider first (/login); presets are computed for the provider your main model runs on.", level: "dim" });
|
|
170
|
+
return { title: "Presets", lines };
|
|
171
|
+
}
|
|
172
|
+
if (result.unavailable === "no-priced-models" || result.presets.length === 0) {
|
|
173
|
+
lines.push({ text: "No priced models on this provider, so tiers cannot be told apart and no preset is offered.", level: "dim" });
|
|
174
|
+
return { title: "Presets", lines };
|
|
175
|
+
}
|
|
176
|
+
const family = sessionModel ? sessionModel.provider : "";
|
|
177
|
+
for (const preset of result.presets) {
|
|
178
|
+
const sub = preset.subagents.setting === "inherit" ? "same" : shortId(preset.subagents.model);
|
|
179
|
+
const cls = preset.classifier ? shortId(preset.classifier) : "none";
|
|
180
|
+
const cost = pricedInput(preset.main);
|
|
181
|
+
const tail = [cost !== undefined ? `$${cost}/M in` : "", preset.current ? "← current main model" : ""].filter(Boolean).join(" · ");
|
|
182
|
+
lines.push({
|
|
183
|
+
text: `${preset.label}: main ${shortId(preset.main)} · subagents ${sub} · classifier ${cls}${tail ? ` — ${tail}` : ""}`,
|
|
184
|
+
level: preset.current ? "ok" : "info",
|
|
185
|
+
});
|
|
186
|
+
lines.push({ text: PRESET_INTENT[preset.name] + (preset.note ? ` (${preset.note})` : ""), indent: 1, level: "dim" });
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
title: "Presets",
|
|
190
|
+
subtitle: `within ${family} · apply with /doctor preset <economical|balanced|quality>`,
|
|
191
|
+
lines,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The three settings writes a preset amounts to, one line each with its undo —
|
|
197
|
+
* the text `/doctor preset` reports after applying. `mainSwitched` false means
|
|
198
|
+
* the session already ran the preset's main model, so that line says so.
|
|
199
|
+
*/
|
|
200
|
+
export function describePresetChanges(preset: PresetPlan, mainSwitched = true): string[] {
|
|
201
|
+
return [
|
|
202
|
+
mainSwitched ? `main model → ${modelSpec(preset.main)} (undo: /model)` : `main model: already ${modelSpec(preset.main)}`,
|
|
203
|
+
preset.subagents.setting === "inherit"
|
|
204
|
+
? "subagent default → inherit the main model (undo: /subagent clear)"
|
|
205
|
+
: `subagent default → automatic (picks ${modelSpec(preset.subagents.model)}; undo: /subagent)`,
|
|
206
|
+
`auto-mode classifier → automatic${preset.classifier ? ` (picks ${modelSpec(preset.classifier)})` : ""} (undo: /auto-mode model)`,
|
|
207
|
+
];
|
|
208
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor report (pure): the data shape every check writes into, and the
|
|
3
|
+
* plain-text renderer both the `/doctor` panel and the `onecode doctor` CLI
|
|
4
|
+
* print from.
|
|
5
|
+
*
|
|
6
|
+
* Shape follows Claude Code's `claude doctor` terminal diagnostics — titled
|
|
7
|
+
* sections of `└ label: value` lines, a warnings block, one closing verdict —
|
|
8
|
+
* extended with what a One Code user actually needs on a first session and
|
|
9
|
+
* Claude Code never has to explain: which provider is ready, which model each
|
|
10
|
+
* role (main, subagents, classifier, reader) resolves to and why, what of the
|
|
11
|
+
* imported Claude Code configuration is honoured, and which external programs
|
|
12
|
+
* are missing. Rationale: docs/decisions/doctor.md.
|
|
13
|
+
*
|
|
14
|
+
* No pi imports. The wiring (index.ts, cli.ts) gathers the live inputs and hands
|
|
15
|
+
* them to `buildDoctorReport`; everything below is deterministic given them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
19
|
+
import type { McpStatusEvent } from "../lib/mcp-status.ts";
|
|
20
|
+
import { wrapProse } from "../subagents/panel-render.ts";
|
|
21
|
+
import { countNoun } from "../lib/tui-render.ts";
|
|
22
|
+
export { countNoun };
|
|
23
|
+
|
|
24
|
+
export type LineLevel = "ok" | "info" | "warn" | "error" | "dim";
|
|
25
|
+
|
|
26
|
+
export interface ReportLine {
|
|
27
|
+
text: string;
|
|
28
|
+
level?: LineLevel;
|
|
29
|
+
/** Nesting depth: 0 = `└ ` item, 1 = ` └ ` sub-item, 2 = deeper. */
|
|
30
|
+
indent?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ReportSection {
|
|
34
|
+
title: string;
|
|
35
|
+
lines: ReportLine[];
|
|
36
|
+
/** A dim trailer under the title (scope, hint). */
|
|
37
|
+
subtitle?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Finding {
|
|
41
|
+
level: "warn" | "error";
|
|
42
|
+
text: string;
|
|
43
|
+
/** What to do about it, when known. Rendered as a `Fix:` line. */
|
|
44
|
+
fix?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DoctorReport {
|
|
48
|
+
title: string;
|
|
49
|
+
/** Two or three plain sentences a first-time user can act on. */
|
|
50
|
+
summary: string;
|
|
51
|
+
sections: ReportSection[];
|
|
52
|
+
findings: Finding[];
|
|
53
|
+
/** False when an error-level finding means a session cannot do useful work yet. */
|
|
54
|
+
ready: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Everything the environment tells the doctor without touching pi's registry. */
|
|
58
|
+
export interface DoctorEnvironment {
|
|
59
|
+
cwd: string;
|
|
60
|
+
home: string;
|
|
61
|
+
/** pi's live agent dir (honours PI_CODING_AGENT_DIR — the app isolates to ~/.onecode/agent). */
|
|
62
|
+
agentDir: string;
|
|
63
|
+
/** One Code's own state dir (~/.onecode or ONECODE_STATE_DIR). */
|
|
64
|
+
stateDir: string;
|
|
65
|
+
env: NodeJS.ProcessEnv;
|
|
66
|
+
platform: string;
|
|
67
|
+
arch: string;
|
|
68
|
+
nodeVersion: string;
|
|
69
|
+
oneCodeVersion: string;
|
|
70
|
+
/** How One Code was launched: the bundled `onecode` app or the extension on the user's own pi. */
|
|
71
|
+
install: "app" | "pi-package";
|
|
72
|
+
piVersion?: string;
|
|
73
|
+
/** Result of the registry lookup for the newest release, when one was attempted. */
|
|
74
|
+
latest?: { status: "current" | "behind" | "unknown" | "skipped"; version?: string; reason?: string };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The slice of pi's ModelRegistry the checks read — structural so tests and the CLI can supply it. */
|
|
78
|
+
export interface RegistryView {
|
|
79
|
+
/** Every catalog row, authenticated or not (`getAll()`). */
|
|
80
|
+
all: Model<Api>[];
|
|
81
|
+
/** Rows the user has working credentials for (`getAvailable()`). */
|
|
82
|
+
available: Model<Api>[];
|
|
83
|
+
authStatus(provider: string): { configured: boolean; source?: string; label?: string };
|
|
84
|
+
displayName?(provider: string): string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Live session facts; the CLI fills what it can from settings and leaves the rest undefined. */
|
|
88
|
+
export interface SessionView {
|
|
89
|
+
model?: Model<Api>;
|
|
90
|
+
/** Where `model` came from: the running session, pi's saved default, or nowhere. */
|
|
91
|
+
modelSource: "session" | "default-setting" | "first-available" | "none";
|
|
92
|
+
thinkingLevel?: string;
|
|
93
|
+
permission?: { mode: string; classifier?: string; pinned?: boolean; source?: string };
|
|
94
|
+
mcp?: McpStatusEvent;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const REPORT_TITLE = "One Code doctor";
|
|
98
|
+
|
|
99
|
+
const GLYPH: Record<LineLevel, string> = { ok: "✔", info: "", warn: "⚠", error: "✘", dim: "" };
|
|
100
|
+
|
|
101
|
+
export interface RenderOptions {
|
|
102
|
+
width?: number;
|
|
103
|
+
/** Theme painter; identity when absent (the CLI prints plain text). */
|
|
104
|
+
paint?: (color: string, text: string) => string;
|
|
105
|
+
bold?: (text: string) => string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const COLOR: Record<LineLevel, string> = { ok: "success", info: "text", warn: "warning", error: "error", dim: "dim" };
|
|
109
|
+
|
|
110
|
+
/** Resolve the optional painters once; identity when absent (the CLI prints plain text). */
|
|
111
|
+
function painters(options: RenderOptions) {
|
|
112
|
+
return {
|
|
113
|
+
width: Math.max(20, options.width ?? 100),
|
|
114
|
+
paint: options.paint ?? ((_color: string, text: string) => text),
|
|
115
|
+
bold: options.bold ?? ((text: string) => text),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type Paint = (color: string, text: string) => string;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One `└ `-style entry: the glyph for its level, word-wrapped to `width`, with
|
|
123
|
+
* continuation lines aligned under the text (past the bullet and the glyph) so a
|
|
124
|
+
* 300-char path never produces an overwide line (pi-tui crashes on one).
|
|
125
|
+
*/
|
|
126
|
+
function renderEntry(prefix: string, text: string, level: LineLevel | undefined, width: number, paint: Paint): string[] {
|
|
127
|
+
const glyph = level ? GLYPH[level] : "";
|
|
128
|
+
const lead = glyph ? `${glyph} ` : "";
|
|
129
|
+
return wrapProse(text, Math.max(10, width - prefix.length - lead.length)).map((segment, i) => {
|
|
130
|
+
const head = i === 0 ? `${prefix}${lead}` : " ".repeat(prefix.length + lead.length);
|
|
131
|
+
return head + (level ? paint(COLOR[level], segment) : segment);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One section — title, optional dim subtitle, its `└ ` lines — rendered standalone (the `/doctor presets` listing). */
|
|
136
|
+
export function renderSection(section: ReportSection, options: RenderOptions = {}): string[] {
|
|
137
|
+
const { width, paint, bold } = painters(options);
|
|
138
|
+
const out: string[] = [bold(section.title) + (section.subtitle ? paint("dim", ` — ${section.subtitle}`) : "")];
|
|
139
|
+
if (section.lines.length === 0) out.push(paint("dim", "└ (nothing)"));
|
|
140
|
+
for (const line of section.lines) out.push(...renderEntry(`${" ".repeat(line.indent ?? 0)}└ `, line.text, line.level, width, paint));
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Render the whole report to lines no wider than `width`, Claude Code's `└ ` style. */
|
|
145
|
+
export function renderDoctorReport(report: DoctorReport, options: RenderOptions = {}): string[] {
|
|
146
|
+
const { width, paint, bold } = painters(options);
|
|
147
|
+
const out: string[] = [bold(report.title), ...wrapProse(report.summary, width)];
|
|
148
|
+
for (const section of report.sections) out.push("", ...renderSection(section, options));
|
|
149
|
+
out.push("");
|
|
150
|
+
if (report.findings.length === 0) {
|
|
151
|
+
out.push(paint("success", "No setup issues found."));
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
const errors = report.findings.filter((f) => f.level === "error").length;
|
|
155
|
+
const warns = report.findings.length - errors;
|
|
156
|
+
out.push(bold(`Issues (${[errors ? countNoun(errors, "problem") : "", warns ? countNoun(warns, "warning") : ""].filter(Boolean).join(", ")})`));
|
|
157
|
+
for (const finding of report.findings) {
|
|
158
|
+
out.push(...renderEntry("└ ", finding.text, finding.level, width, paint));
|
|
159
|
+
if (finding.fix) out.push(...renderEntry(" ", `Fix: ${finding.fix}`, "dim", width, paint));
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Plain-text form for `ctx.ui.notify`, `-p` output and the CLI. */
|
|
165
|
+
export function renderDoctorText(report: DoctorReport, width = 100): string {
|
|
166
|
+
return renderDoctorReport(report, { width }).join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** `$3/M in · $15/M out`, or `unpriced` when the catalog carries no usable price. */
|
|
170
|
+
export function priceLabel(model: Model<Api>): string {
|
|
171
|
+
const input = model.cost?.input;
|
|
172
|
+
const output = model.cost?.output;
|
|
173
|
+
if (typeof input !== "number" || input <= 0) return "unpriced";
|
|
174
|
+
return typeof output === "number" && output > 0 ? `$${input}/M in · $${output}/M out` : `$${input}/M in`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `1M`, `200k`, `32k` — a context window as users read it. */
|
|
178
|
+
export function contextLabel(tokens: number | undefined): string {
|
|
179
|
+
if (!tokens || tokens <= 0) return "";
|
|
180
|
+
if (tokens >= 1_000_000) return `${Number.isInteger(tokens / 1_000_000) ? tokens / 1_000_000 : (tokens / 1_000_000).toFixed(1)}M context`;
|
|
181
|
+
return `${Math.round(tokens / 1000)}k context`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** `~/…` for anything under home, so paths stay readable at any width. */
|
|
185
|
+
export function shortenHome(path: string, home: string): string {
|
|
186
|
+
if (path === home) return "~";
|
|
187
|
+
const root = home.endsWith("/") ? home : `${home}/`;
|
|
188
|
+
return path.startsWith(root) ? `~/${path.slice(root.length)}` : path;
|
|
189
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newest-release lookup for the doctor's Installation section (pure given a
|
|
3
|
+
* fetch). Mirrors the app's daily update check (app/update-check.mjs): the same
|
|
4
|
+
* registry endpoints, a short timeout, silent on failure, and skipped under
|
|
5
|
+
* pi's offline switch or ONECODE_NO_UPDATE_CHECK — a diagnostic must never hang
|
|
6
|
+
* on the network or restore egress the user turned off.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { compareVersions } from "../lib/pi-version.ts";
|
|
10
|
+
import type { DoctorEnvironment } from "./report.ts";
|
|
11
|
+
|
|
12
|
+
export const APP_REGISTRY_URL = "https://registry.npmjs.org/@one-ai%2Fone-code/latest";
|
|
13
|
+
export const PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/one-code-extension/latest";
|
|
14
|
+
export const LOOKUP_TIMEOUT_MS = 3000;
|
|
15
|
+
|
|
16
|
+
export type LatestLookup = NonNullable<DoctorEnvironment["latest"]>;
|
|
17
|
+
|
|
18
|
+
/** pi's truthy-flag reading for PI_OFFLINE (1/true/yes). */
|
|
19
|
+
export function isOffline(env: NodeJS.ProcessEnv): boolean {
|
|
20
|
+
return /^(1|true|yes)$/i.test(String(env.PI_OFFLINE ?? "").trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function lookupLatestVersion(input: {
|
|
24
|
+
install: "app" | "pi-package";
|
|
25
|
+
current: string;
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}): Promise<LatestLookup> {
|
|
30
|
+
if (isOffline(input.env)) return { status: "skipped", reason: "offline" };
|
|
31
|
+
if (input.env.ONECODE_NO_UPDATE_CHECK === "1") return { status: "skipped", reason: "ONECODE_NO_UPDATE_CHECK=1" };
|
|
32
|
+
const url = input.install === "app" ? APP_REGISTRY_URL : PACKAGE_REGISTRY_URL;
|
|
33
|
+
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetchImpl(url, { signal: AbortSignal.timeout(input.timeoutMs ?? LOOKUP_TIMEOUT_MS) });
|
|
36
|
+
if (!response.ok) return { status: "unknown", reason: `registry answered ${response.status}` };
|
|
37
|
+
const body = (await response.json()) as { version?: unknown };
|
|
38
|
+
if (typeof body.version !== "string") return { status: "unknown", reason: "unexpected registry payload" };
|
|
39
|
+
const cmp = compareVersions(body.version, input.current);
|
|
40
|
+
if (cmp === undefined) return { status: "unknown", version: body.version, reason: "unparseable version" };
|
|
41
|
+
return { status: cmp > 0 ? "behind" : "current", version: body.version };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? (error.name === "TimeoutError" ? "timed out" : error.message) : String(error);
|
|
44
|
+
return { status: "unknown", reason: message };
|
|
45
|
+
}
|
|
46
|
+
}
|