@sayknow-cli/coding-agent 0.3.5 → 0.3.7
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 +76 -0
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/telegram-cli.d.ts +13 -0
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/commands/telegram.d.ts +22 -0
- package/dist/types/config/keybindings.d.ts +12 -6
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +284 -4
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +14 -0
- package/dist/types/config/telegram-env-bridge.d.ts +18 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/defaults/skc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +5 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +15 -3
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +6 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +4 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/skc-runtime/state-writer.d.ts +1 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +0 -5
- package/src/capability/mcp.ts +2 -0
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/telegram-cli.ts +156 -0
- package/src/cli/update-cli.ts +179 -13
- package/src/cli/web-search-cli.ts +5 -7
- package/src/cli.ts +20 -1
- package/src/commands/launch.ts +47 -2
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/commands/telegram.ts +47 -0
- package/src/config/keybindings.ts +15 -6
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +256 -4
- package/src/config/settings.ts +11 -0
- package/src/config/telegram-autostart.ts +107 -0
- package/src/config/telegram-env-bridge.ts +92 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
- package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/skc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +30 -2
- package/src/hooks/skill-state.ts +33 -73
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.settings.ts +28 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.settings.ts +28 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.settings.ts +28 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -7
- package/src/main.ts +5 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +3 -35
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +246 -11
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +551 -11
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +281 -117
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +165 -30
- package/src/modes/controllers/command-controller.ts +26 -2
- package/src/modes/controllers/event-controller.ts +102 -4
- package/src/modes/controllers/extension-ui-controller.ts +21 -2
- package/src/modes/controllers/input-controller.ts +214 -47
- package/src/modes/controllers/selector-controller.ts +35 -33
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +48 -8
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/theme/theme.ts +5 -1
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +32 -2
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +88 -46
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +20 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +10 -11
- package/src/session/agent-session.ts +103 -13
- package/src/session/session-manager.ts +58 -18
- package/src/skc-runtime/launch-tmux.ts +34 -8
- package/src/skc-runtime/launch-worktree.ts +16 -2
- package/src/skc-runtime/ralplan-runtime.ts +22 -6
- package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/skc-runtime/session-state-sidecar.ts +167 -24
- package/src/skc-runtime/state-runtime.ts +8 -5
- package/src/skc-runtime/state-writer.ts +5 -5
- package/src/skc-runtime/team-runtime.ts +21 -6
- package/src/skc-runtime/tmux-common.ts +16 -0
- package/src/skc-runtime/tmux-sessions.ts +13 -9
- package/src/skc-runtime/ultragoal-guard.ts +150 -24
- package/src/skc-runtime/ultragoal-runtime.ts +16 -124
- package/src/skc-runtime/workflow-manifest.generated.json +0 -8
- package/src/skc-runtime/workflow-manifest.ts +0 -1
- package/src/slash-commands/builtin-registry.ts +136 -23
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/web/search/render.ts +3 -0
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/vendor/insane-search/MANIFEST.json +1 -3
- package/vendor/insane-search/engine/tests/test_u1.py +0 -28
- package/vendor/insane-search/engine/transport.py +2 -3
- package/vendor/insane-search/engine/validators.py +0 -14
- package/src/defaults/skc/rules/ponytail.md +0 -68
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ModelsConfigFile } from "../config/model-registry";
|
|
3
|
+
import type { ModelsConfig } from "../config/models-config-schema";
|
|
4
|
+
|
|
5
|
+
export interface LocalProviderSmokeCommandArgs {
|
|
6
|
+
model?: string;
|
|
7
|
+
modelsPath?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
smoke?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LocalOpenAICompatConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type LocalProviderDiagnosticCheckName = "config" | "models" | "chat_stream";
|
|
19
|
+
export type LocalProviderDiagnosticStatus = "ok" | "skipped" | "error";
|
|
20
|
+
export type LocalProviderDiagnosticCategory =
|
|
21
|
+
| "auth"
|
|
22
|
+
| "timeout"
|
|
23
|
+
| "unreachable"
|
|
24
|
+
| "not_ready"
|
|
25
|
+
| "oom"
|
|
26
|
+
| "malformed_response"
|
|
27
|
+
| "http_error"
|
|
28
|
+
| "configuration"
|
|
29
|
+
| "empty_response";
|
|
30
|
+
|
|
31
|
+
export interface LocalProviderDiagnosticCheck {
|
|
32
|
+
name: LocalProviderDiagnosticCheckName;
|
|
33
|
+
status: LocalProviderDiagnosticStatus;
|
|
34
|
+
message: string;
|
|
35
|
+
action?: string;
|
|
36
|
+
category?: LocalProviderDiagnosticCategory;
|
|
37
|
+
error?: string;
|
|
38
|
+
httpStatus?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface LocalProviderSmokeResult {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
baseUrl?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
message: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
category?: LocalProviderDiagnosticCategory;
|
|
48
|
+
action?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LocalProviderDiscoveryResult {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
provider: string;
|
|
54
|
+
baseUrl?: string;
|
|
55
|
+
models: string[];
|
|
56
|
+
message: string;
|
|
57
|
+
error?: string;
|
|
58
|
+
category?: LocalProviderDiagnosticCategory;
|
|
59
|
+
action?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LocalProviderStatusResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
provider: "local";
|
|
65
|
+
baseUrl?: string;
|
|
66
|
+
model?: string;
|
|
67
|
+
models: string[];
|
|
68
|
+
checks: LocalProviderDiagnosticCheck[];
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ClassifiedFailure {
|
|
73
|
+
category: LocalProviderDiagnosticCategory;
|
|
74
|
+
message: string;
|
|
75
|
+
action: string;
|
|
76
|
+
error?: string;
|
|
77
|
+
httpStatus?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
81
|
+
const DEFAULT_SMOKE_PROMPT = "Reply with ok.";
|
|
82
|
+
|
|
83
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
+
return typeof value === "object" && value !== null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveApiKey(apiKey: string | undefined, apiKeyEnv: string | undefined): string | undefined {
|
|
88
|
+
if (apiKeyEnv) return Bun.env[apiKeyEnv];
|
|
89
|
+
if (!apiKey) return undefined;
|
|
90
|
+
return Bun.env[apiKey] ?? apiKey;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeOpenAICompatBaseUrl(baseUrl: string): string {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(baseUrl);
|
|
96
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
97
|
+
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
98
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
99
|
+
} catch {
|
|
100
|
+
const trimmed = baseUrl.replace(/\/+$/g, "");
|
|
101
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getLocalOpenAICompatConfig(config: ModelsConfig | undefined): LocalOpenAICompatConfig | undefined {
|
|
106
|
+
const openaiCompat = config?.providers?.local?.openaiCompat;
|
|
107
|
+
if (!openaiCompat?.baseUrl) return undefined;
|
|
108
|
+
return {
|
|
109
|
+
baseUrl: normalizeOpenAICompatBaseUrl(openaiCompat.baseUrl),
|
|
110
|
+
apiKey: resolveApiKey(openaiCompat.apiKey, openaiCompat.apiKeyEnv),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractModelIds(payload: unknown): string[] {
|
|
115
|
+
if (!isRecord(payload)) {
|
|
116
|
+
throw new Error("/models response was not a JSON object");
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(payload.data)) {
|
|
119
|
+
throw new Error("/models response did not include a data array");
|
|
120
|
+
}
|
|
121
|
+
const models = payload.data.flatMap(item => {
|
|
122
|
+
if (!isRecord(item) || typeof item.id !== "string") return [];
|
|
123
|
+
const id = item.id.trim();
|
|
124
|
+
return id ? [id] : [];
|
|
125
|
+
});
|
|
126
|
+
if (models.length === 0) {
|
|
127
|
+
throw new Error("/models returned no model ids");
|
|
128
|
+
}
|
|
129
|
+
return [...new Set(models)].sort((left, right) => left.localeCompare(right));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readLocalConfig(
|
|
133
|
+
modelsPath: string | undefined,
|
|
134
|
+
): Promise<LocalProviderSmokeResult | LocalOpenAICompatConfig> {
|
|
135
|
+
const configFile = modelsPath ? ModelsConfigFile.relocate(modelsPath) : ModelsConfigFile;
|
|
136
|
+
configFile.invalidate?.();
|
|
137
|
+
const loaded = configFile.tryLoad();
|
|
138
|
+
if (loaded.status === "error") {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
message: "Failed to load models config.",
|
|
142
|
+
error: loaded.error.message,
|
|
143
|
+
category: "configuration",
|
|
144
|
+
action: "Fix the models config file syntax, then retry the local-provider diagnostic.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const localConfig = getLocalOpenAICompatConfig(loaded.value ?? undefined);
|
|
148
|
+
if (!localConfig) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
message: `No local OpenAI-compatible endpoint configured. Add providers.local.openaiCompat.baseUrl to ${configFile.path()}.`,
|
|
152
|
+
category: "configuration",
|
|
153
|
+
action: "Configure providers.local.openaiCompat.baseUrl for the local server you already run.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return localConfig;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildHeaders(apiKey: string | undefined): Record<string, string> {
|
|
160
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
161
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
162
|
+
return headers;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toErrorMessage(error: unknown): string {
|
|
166
|
+
return error instanceof Error ? error.message : String(error);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function responsePreview(response: Response): Promise<string> {
|
|
170
|
+
const text = await response.text().catch(() => "");
|
|
171
|
+
return text.slice(0, 500);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function bodyLooksLike(text: string | undefined, needles: readonly string[]): boolean {
|
|
175
|
+
const lower = (text ?? "").toLowerCase();
|
|
176
|
+
return needles.some(needle => lower.includes(needle));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function classifyHttpFailure(context: "models" | "chat_stream", status: number, body: string): ClassifiedFailure {
|
|
180
|
+
if (status === 401 || status === 403) {
|
|
181
|
+
return {
|
|
182
|
+
category: "auth",
|
|
183
|
+
httpStatus: status,
|
|
184
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} authentication failed.`,
|
|
185
|
+
action:
|
|
186
|
+
"Check providers.local.openaiCompat.apiKey/apiKeyEnv, or remove auth if the local server does not require it.",
|
|
187
|
+
error: `HTTP ${status}${body ? `: ${body}` : ""}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (bodyLooksLike(body, ["out of memory", "oom", "cuda out", "insufficient memory"])) {
|
|
191
|
+
return {
|
|
192
|
+
category: "oom",
|
|
193
|
+
httpStatus: status,
|
|
194
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} reported an out-of-memory condition.`,
|
|
195
|
+
action: "Free GPU/CPU memory, lower the model/context size, or unload another model before retrying.",
|
|
196
|
+
error: `HTTP ${status}${body ? `: ${body}` : ""}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (
|
|
200
|
+
status === 408 ||
|
|
201
|
+
status === 409 ||
|
|
202
|
+
status === 425 ||
|
|
203
|
+
status === 429 ||
|
|
204
|
+
status === 503 ||
|
|
205
|
+
status === 504 ||
|
|
206
|
+
bodyLooksLike(body, ["loading", "warming", "not ready", "initializing", "starting", "model is loading"])
|
|
207
|
+
) {
|
|
208
|
+
return {
|
|
209
|
+
category: "not_ready",
|
|
210
|
+
httpStatus: status,
|
|
211
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} reached the server, but it is not ready.`,
|
|
212
|
+
action: "Wait for the local server/model load to finish, verify the selected model is loaded, then retry.",
|
|
213
|
+
error: `HTTP ${status}${body ? `: ${body}` : ""}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
category: "http_error",
|
|
218
|
+
httpStatus: status,
|
|
219
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} returned HTTP ${status}.`,
|
|
220
|
+
action:
|
|
221
|
+
"Check the local server logs and confirm the configured base URL points at an OpenAI-compatible /v1 endpoint.",
|
|
222
|
+
error: `HTTP ${status}${body ? `: ${body}` : ""}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function classifyThrownFailure(
|
|
227
|
+
context: "models" | "chat_stream",
|
|
228
|
+
error: unknown,
|
|
229
|
+
timeoutMs: number,
|
|
230
|
+
): ClassifiedFailure {
|
|
231
|
+
const message = toErrorMessage(error);
|
|
232
|
+
const name = error instanceof Error ? error.name : "";
|
|
233
|
+
if (name === "AbortError" || name === "TimeoutError" || message.toLowerCase().includes("abort")) {
|
|
234
|
+
return {
|
|
235
|
+
category: "timeout",
|
|
236
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} timed out after ${timeoutMs}ms.`,
|
|
237
|
+
action:
|
|
238
|
+
"Confirm the local server is running and responsive; if it is loading a model, retry after it is ready or pass a slightly larger --timeout-ms.",
|
|
239
|
+
error: message,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
bodyLooksLike(message, [
|
|
244
|
+
"connection refused",
|
|
245
|
+
"econnrefused",
|
|
246
|
+
"couldn't connect",
|
|
247
|
+
"failed to connect",
|
|
248
|
+
"connection reset",
|
|
249
|
+
"enotfound",
|
|
250
|
+
"fetch failed",
|
|
251
|
+
])
|
|
252
|
+
) {
|
|
253
|
+
return {
|
|
254
|
+
category: "unreachable",
|
|
255
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} could not reach the configured endpoint.`,
|
|
256
|
+
action:
|
|
257
|
+
"Start the local OpenAI-compatible server or update providers.local.openaiCompat.baseUrl to the listening host/port.",
|
|
258
|
+
error: message,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (bodyLooksLike(message, ["out of memory", "oom", "cuda out", "insufficient memory"])) {
|
|
262
|
+
return {
|
|
263
|
+
category: "oom",
|
|
264
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} reported an out-of-memory condition.`,
|
|
265
|
+
action: "Free GPU/CPU memory, lower the model/context size, or unload another model before retrying.",
|
|
266
|
+
error: message,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
category: "http_error",
|
|
271
|
+
message: `${context === "models" ? "GET /v1/models" : "Streaming chat smoke"} failed.`,
|
|
272
|
+
action: "Check the local server logs and retry the diagnostic after the endpoint is healthy.",
|
|
273
|
+
error: message,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function malformedFailure(error: unknown): ClassifiedFailure {
|
|
278
|
+
return {
|
|
279
|
+
category: "malformed_response",
|
|
280
|
+
message: "GET /v1/models returned a malformed OpenAI-compatible response.",
|
|
281
|
+
action:
|
|
282
|
+
"Confirm the endpoint serves OpenAI-compatible JSON shaped like { data: [{ id: string }] } at /v1/models.",
|
|
283
|
+
error: toErrorMessage(error),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function fetchLocalModelIds(config: LocalOpenAICompatConfig, timeoutMs: number): Promise<string[]> {
|
|
288
|
+
const response = await fetch(`${config.baseUrl}/models`, {
|
|
289
|
+
headers: buildHeaders(config.apiKey),
|
|
290
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
291
|
+
});
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const body = await responsePreview(response);
|
|
294
|
+
throw new Error(classifyHttpFailure("models", response.status, body).error);
|
|
295
|
+
}
|
|
296
|
+
let payload: unknown;
|
|
297
|
+
try {
|
|
298
|
+
payload = await response.json();
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new Error(`Failed to parse /models JSON: ${toErrorMessage(error)}`);
|
|
301
|
+
}
|
|
302
|
+
return extractModelIds(payload);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function diagnoseLocalModels(
|
|
306
|
+
config: LocalOpenAICompatConfig,
|
|
307
|
+
timeoutMs: number,
|
|
308
|
+
): Promise<{ models: string[]; check: LocalProviderDiagnosticCheck }> {
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetch(`${config.baseUrl}/models`, {
|
|
311
|
+
headers: buildHeaders(config.apiKey),
|
|
312
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
313
|
+
});
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
const body = await responsePreview(response);
|
|
316
|
+
const failure = classifyHttpFailure("models", response.status, body);
|
|
317
|
+
return { models: [], check: { name: "models", status: "error", ...failure } };
|
|
318
|
+
}
|
|
319
|
+
let payload: unknown;
|
|
320
|
+
try {
|
|
321
|
+
payload = await response.json();
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const failure = malformedFailure(new Error(`Failed to parse /models JSON: ${toErrorMessage(error)}`));
|
|
324
|
+
return { models: [], check: { name: "models", status: "error", ...failure } };
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const models = extractModelIds(payload);
|
|
328
|
+
return {
|
|
329
|
+
models,
|
|
330
|
+
check: {
|
|
331
|
+
name: "models",
|
|
332
|
+
status: "ok",
|
|
333
|
+
message: `GET /v1/models succeeded and returned ${models.length} model${models.length === 1 ? "" : "s"}.`,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const failure = malformedFailure(error);
|
|
338
|
+
return { models: [], check: { name: "models", status: "error", ...failure } };
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const failure = classifyThrownFailure("models", error, timeoutMs);
|
|
342
|
+
return { models: [], check: { name: "models", status: "error", ...failure } };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function discoverFirstModel(config: LocalOpenAICompatConfig, timeoutMs: number): Promise<string> {
|
|
347
|
+
try {
|
|
348
|
+
return (await fetchLocalModelIds(config, timeoutMs))[0]!;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const message = toErrorMessage(error);
|
|
351
|
+
if (message === "/models returned no model ids") {
|
|
352
|
+
throw new Error("/models returned no model ids; pass --model explicitly");
|
|
353
|
+
}
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function readStreamingBody(response: Response): Promise<number> {
|
|
359
|
+
if (!response.body) return 0;
|
|
360
|
+
const reader = response.body.getReader();
|
|
361
|
+
let chunks = 0;
|
|
362
|
+
try {
|
|
363
|
+
while (true) {
|
|
364
|
+
const { done, value } = await reader.read();
|
|
365
|
+
if (done) break;
|
|
366
|
+
if (value.byteLength > 0) chunks += 1;
|
|
367
|
+
}
|
|
368
|
+
} finally {
|
|
369
|
+
reader.releaseLock();
|
|
370
|
+
}
|
|
371
|
+
return chunks;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function diagnoseChatStream(
|
|
375
|
+
config: LocalOpenAICompatConfig,
|
|
376
|
+
model: string,
|
|
377
|
+
timeoutMs: number,
|
|
378
|
+
): Promise<LocalProviderDiagnosticCheck> {
|
|
379
|
+
try {
|
|
380
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: buildHeaders(config.apiKey),
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
model,
|
|
385
|
+
messages: [{ role: "user", content: DEFAULT_SMOKE_PROMPT }],
|
|
386
|
+
stream: true,
|
|
387
|
+
max_tokens: 16,
|
|
388
|
+
}),
|
|
389
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
390
|
+
});
|
|
391
|
+
if (!response.ok) {
|
|
392
|
+
const body = await responsePreview(response);
|
|
393
|
+
const failure = classifyHttpFailure("chat_stream", response.status, body);
|
|
394
|
+
return { name: "chat_stream", status: "error", ...failure };
|
|
395
|
+
}
|
|
396
|
+
const chunks = await readStreamingBody(response);
|
|
397
|
+
if (chunks === 0) {
|
|
398
|
+
return {
|
|
399
|
+
name: "chat_stream",
|
|
400
|
+
status: "error",
|
|
401
|
+
category: "empty_response",
|
|
402
|
+
message: "Streaming chat smoke reached the server but returned no body chunks.",
|
|
403
|
+
action:
|
|
404
|
+
"Check whether the selected model supports streaming chat completions and inspect the local server logs.",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
name: "chat_stream",
|
|
409
|
+
status: "ok",
|
|
410
|
+
message: `Streaming chat smoke succeeded for ${model} (${chunks} chunk${chunks === 1 ? "" : "s"}).`,
|
|
411
|
+
};
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const failure = classifyThrownFailure("chat_stream", error, timeoutMs);
|
|
414
|
+
return { name: "chat_stream", status: "error", ...failure };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function runLocalProviderDiscover(
|
|
419
|
+
cmd: Omit<LocalProviderSmokeCommandArgs, "model" | "smoke">,
|
|
420
|
+
): Promise<LocalProviderDiscoveryResult> {
|
|
421
|
+
const timeoutMs = cmd.timeoutMs && cmd.timeoutMs > 0 ? cmd.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
422
|
+
const configResult = await readLocalConfig(cmd.modelsPath);
|
|
423
|
+
if ("ok" in configResult) {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
provider: "local",
|
|
427
|
+
models: [],
|
|
428
|
+
message: configResult.message,
|
|
429
|
+
error: configResult.error,
|
|
430
|
+
category: configResult.category,
|
|
431
|
+
action: configResult.action,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const diagnostics = await diagnoseLocalModels(configResult, timeoutMs);
|
|
436
|
+
if (diagnostics.check.status === "ok") {
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
provider: "local",
|
|
440
|
+
baseUrl: configResult.baseUrl,
|
|
441
|
+
models: diagnostics.models,
|
|
442
|
+
message: `Discovered ${diagnostics.models.length} model${diagnostics.models.length === 1 ? "" : "s"}.`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
provider: "local",
|
|
448
|
+
baseUrl: configResult.baseUrl,
|
|
449
|
+
models: [],
|
|
450
|
+
message: "Local provider model discovery failed.",
|
|
451
|
+
error: diagnostics.check.error,
|
|
452
|
+
category: diagnostics.check.category,
|
|
453
|
+
action: diagnostics.check.action,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function runLocalProviderStatus(cmd: LocalProviderSmokeCommandArgs): Promise<LocalProviderStatusResult> {
|
|
458
|
+
const timeoutMs = cmd.timeoutMs && cmd.timeoutMs > 0 ? cmd.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
459
|
+
const checks: LocalProviderDiagnosticCheck[] = [];
|
|
460
|
+
const configResult = await readLocalConfig(cmd.modelsPath);
|
|
461
|
+
if ("ok" in configResult) {
|
|
462
|
+
checks.push({
|
|
463
|
+
name: "config",
|
|
464
|
+
status: "error",
|
|
465
|
+
message: configResult.message,
|
|
466
|
+
error: configResult.error,
|
|
467
|
+
category: configResult.category,
|
|
468
|
+
action: configResult.action,
|
|
469
|
+
});
|
|
470
|
+
return { ok: false, provider: "local", models: [], checks, message: "Local provider diagnostics failed." };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
checks.push({
|
|
474
|
+
name: "config",
|
|
475
|
+
status: "ok",
|
|
476
|
+
message: "Found providers.local.openaiCompat without mutating config.",
|
|
477
|
+
});
|
|
478
|
+
const modelDiagnostics = await diagnoseLocalModels(configResult, timeoutMs);
|
|
479
|
+
checks.push(modelDiagnostics.check);
|
|
480
|
+
let model = cmd.model?.trim();
|
|
481
|
+
const shouldSmoke = Boolean(cmd.smoke || model);
|
|
482
|
+
if (!shouldSmoke) {
|
|
483
|
+
checks.push({
|
|
484
|
+
name: "chat_stream",
|
|
485
|
+
status: "skipped",
|
|
486
|
+
message: "Streaming chat smoke skipped; pass --smoke or --model to run it.",
|
|
487
|
+
});
|
|
488
|
+
} else if (modelDiagnostics.check.status !== "ok") {
|
|
489
|
+
checks.push({
|
|
490
|
+
name: "chat_stream",
|
|
491
|
+
status: "skipped",
|
|
492
|
+
message: "Streaming chat smoke skipped because GET /v1/models did not pass.",
|
|
493
|
+
action: "Fix the /v1/models diagnostic first, then retry with --smoke or --model.",
|
|
494
|
+
});
|
|
495
|
+
} else {
|
|
496
|
+
model = model || modelDiagnostics.models[0];
|
|
497
|
+
if (!model) {
|
|
498
|
+
checks.push({
|
|
499
|
+
name: "chat_stream",
|
|
500
|
+
status: "skipped",
|
|
501
|
+
message: "Streaming chat smoke skipped because no model id was available.",
|
|
502
|
+
action: "Pass --model with a loaded local model id.",
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
checks.push(await diagnoseChatStream(configResult, model, timeoutMs));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const ok = checks.every(check => check.status !== "error");
|
|
509
|
+
return {
|
|
510
|
+
ok,
|
|
511
|
+
provider: "local",
|
|
512
|
+
baseUrl: configResult.baseUrl,
|
|
513
|
+
model,
|
|
514
|
+
models: modelDiagnostics.models,
|
|
515
|
+
checks,
|
|
516
|
+
message: ok ? "Local provider diagnostics passed." : "Local provider diagnostics failed.",
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function runLocalProviderSmoke(cmd: LocalProviderSmokeCommandArgs): Promise<LocalProviderSmokeResult> {
|
|
521
|
+
const timeoutMs = cmd.timeoutMs && cmd.timeoutMs > 0 ? cmd.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
522
|
+
const configResult = await readLocalConfig(cmd.modelsPath);
|
|
523
|
+
if ("ok" in configResult) return configResult;
|
|
524
|
+
|
|
525
|
+
let model = cmd.model;
|
|
526
|
+
try {
|
|
527
|
+
model = model?.trim() || (await discoverFirstModel(configResult, timeoutMs));
|
|
528
|
+
const check = await diagnoseChatStream(configResult, model, timeoutMs);
|
|
529
|
+
if (check.status === "ok") {
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
baseUrl: configResult.baseUrl,
|
|
533
|
+
model,
|
|
534
|
+
message: check.message,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
baseUrl: configResult.baseUrl,
|
|
540
|
+
model,
|
|
541
|
+
message: check.message,
|
|
542
|
+
error: check.error,
|
|
543
|
+
category: check.category,
|
|
544
|
+
action: check.action,
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const failure = classifyThrownFailure("chat_stream", error, timeoutMs);
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
baseUrl: configResult.baseUrl,
|
|
551
|
+
model,
|
|
552
|
+
message: failure.message,
|
|
553
|
+
error: failure.error,
|
|
554
|
+
category: failure.category,
|
|
555
|
+
action: failure.action,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function runLocalProviderDiscoverCommand(
|
|
561
|
+
cmd: Omit<LocalProviderSmokeCommandArgs, "model" | "smoke">,
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
const result = await runLocalProviderDiscover(cmd);
|
|
564
|
+
if (cmd.json) {
|
|
565
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
566
|
+
} else if (result.ok) {
|
|
567
|
+
process.stdout.write(`${chalk.green("ok")} ${result.message}\n`);
|
|
568
|
+
process.stdout.write(`provider=${result.provider} baseUrl=${result.baseUrl}\n`);
|
|
569
|
+
for (const model of result.models) {
|
|
570
|
+
process.stdout.write(`${model}\n`);
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
process.stderr.write(`${chalk.red("error")} ${result.message}\n`);
|
|
574
|
+
process.stderr.write(`${chalk.dim(`provider=${result.provider} baseUrl=${result.baseUrl ?? "<unknown>"}`)}\n`);
|
|
575
|
+
if (result.error) process.stderr.write(`${chalk.dim(result.error)}\n`);
|
|
576
|
+
if (result.action) process.stderr.write(`${chalk.dim(`action: ${result.action}`)}\n`);
|
|
577
|
+
}
|
|
578
|
+
if (!result.ok) process.exitCode = 1;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function renderStatusCheck(check: LocalProviderDiagnosticCheck): string {
|
|
582
|
+
const label =
|
|
583
|
+
check.status === "ok"
|
|
584
|
+
? chalk.green("ok")
|
|
585
|
+
: check.status === "skipped"
|
|
586
|
+
? chalk.yellow("skip")
|
|
587
|
+
: chalk.red("error");
|
|
588
|
+
const suffix = check.category ? chalk.dim(` [${check.category}]`) : "";
|
|
589
|
+
return `${label} ${check.name}: ${check.message}${suffix}\n`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export async function runLocalProviderStatusCommand(cmd: LocalProviderSmokeCommandArgs): Promise<void> {
|
|
593
|
+
const result = await runLocalProviderStatus(cmd);
|
|
594
|
+
if (cmd.json) {
|
|
595
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
596
|
+
} else {
|
|
597
|
+
const stream = result.ok ? process.stdout : process.stderr;
|
|
598
|
+
stream.write(`${result.ok ? chalk.green("ok") : chalk.red("error")} ${result.message}\n`);
|
|
599
|
+
stream.write(
|
|
600
|
+
`${chalk.dim(`provider=${result.provider} endpoint=${result.baseUrl ?? "<unknown>"}${result.model ? ` model=${result.model}` : ""}`)}\n`,
|
|
601
|
+
);
|
|
602
|
+
for (const check of result.checks) {
|
|
603
|
+
stream.write(renderStatusCheck(check));
|
|
604
|
+
if (check.error) stream.write(`${chalk.dim(` ${check.error}`)}\n`);
|
|
605
|
+
if (check.action) stream.write(`${chalk.dim(` action: ${check.action}`)}\n`);
|
|
606
|
+
}
|
|
607
|
+
if (result.models.length > 0) {
|
|
608
|
+
stream.write(`${chalk.dim(`models: ${result.models.join(", ")}`)}\n`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (!result.ok) process.exitCode = 1;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export async function runLocalProviderSmokeCommand(cmd: LocalProviderSmokeCommandArgs): Promise<void> {
|
|
615
|
+
const result = await runLocalProviderSmoke(cmd);
|
|
616
|
+
if (cmd.json) {
|
|
617
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
618
|
+
} else if (result.ok) {
|
|
619
|
+
process.stdout.write(`${chalk.green("ok")} ${result.message}\n`);
|
|
620
|
+
process.stdout.write(`${chalk.dim(`endpoint=${result.baseUrl} model=${result.model}`)}\n`);
|
|
621
|
+
} else {
|
|
622
|
+
process.stderr.write(`${chalk.red("error")} ${result.message}\n`);
|
|
623
|
+
if (result.baseUrl || result.model) {
|
|
624
|
+
process.stderr.write(
|
|
625
|
+
`${chalk.dim(`endpoint=${result.baseUrl ?? "<unknown>"} model=${result.model ?? "<unset>"}`)}\n`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (result.error) process.stderr.write(`${chalk.dim(result.error)}\n`);
|
|
629
|
+
if (result.action) process.stderr.write(`${chalk.dim(`action: ${result.action}`)}\n`);
|
|
630
|
+
}
|
|
631
|
+
if (!result.ok) process.exitCode = 1;
|
|
632
|
+
}
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { APP_NAME } from "@sayknow-cli/utils";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import { Settings } from "../config/settings";
|
|
9
|
+
import type { Settings } from "../config/settings";
|
|
10
10
|
import { getNotificationConfig, maskToken } from "../notifications/config";
|
|
11
11
|
|
|
12
12
|
export type NotifyAction = "setup" | "status" | "daemon-internal";
|
|
@@ -122,7 +122,9 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
async function getSettings(deps: NotifyCommandDeps): Promise<Settings> {
|
|
125
|
-
|
|
125
|
+
if (deps.settings) return deps.settings;
|
|
126
|
+
const { Settings } = await import("../config/settings");
|
|
127
|
+
return await Settings.init();
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|