@klarkxy/dsh-model-picker 0.1.0 → 0.1.1

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.
@@ -1070,6 +1070,281 @@ var EffortPreference = class {
1070
1070
  }
1071
1071
  };
1072
1072
  //#endregion
1073
+ //#region ../dsh-plugin-kit/src/log-shared.ts
1074
+ const PLUGIN_LOG_LEVELS = [
1075
+ "debug",
1076
+ "info",
1077
+ "warn",
1078
+ "error",
1079
+ "fault"
1080
+ ];
1081
+ const DEFAULT_PLUGIN_LOG_LEVEL = "info";
1082
+ const PLUGIN_LOG_MAX_MESSAGE_CHARS = 16e3;
1083
+ const SOURCE_RE = /^[a-z][a-z0-9-]{0,63}$/;
1084
+ const EVENT_RE = /^[a-z][a-z0-9._-]{0,127}$/;
1085
+ const CONTEXT_KEY_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/;
1086
+ const SECRET_KEY_RE = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|bearer|authorization|cookie|set[_-]?cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|z_c0|d_c0)$/i;
1087
+ const CHAT_PAYLOAD_RE = /"role"\s*:\s*"(?:system|user|assistant|tool)"|"(?:messages|content|prompt|input_text|input_image|image_url)"\s*:\s*[\[{"']/i;
1088
+ const CREDENTIAL_BODY_RE = /-----BEGIN (?:RSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----|"type"\s*:\s*"service_account"|credentials\.json|\.dsh[/\\](?:credentials|auth)/i;
1089
+ const AUTH_HEADER_RE = /\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/gi;
1090
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+=/]{8,}/g;
1091
+ const SECRET_FIELD_RE = /(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|bearer|authorization|cookie|set-cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|password|z_c0|d_c0)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,&;}]+)/gi;
1092
+ const SECRET_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|password|bootstrap|code|sig|signature|credential|password)=)[^\s&#"']+/gi;
1093
+ const URL_CREDENTIAL_RE = /\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
1094
+ const BOOTSTRAP_RE = /\/bootstrap\/[^\s"'?&#]+/gi;
1095
+ const KNOWN_TOKEN_RE = /\b(?:sk-[A-Za-z0-9_-]{10,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{20,}|tvly-[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16})\b/g;
1096
+ const PATH_RE = /(?:[A-Za-z]:\\|\\\\)[^\s"'`<>|]+|(?:\/(?:home|Users|tmp|var|root)\/|~\/)[^\s"'`<>|]+/g;
1097
+ function isPluginLogLevel(value) {
1098
+ return typeof value === "string" && PLUGIN_LOG_LEVELS.includes(value);
1099
+ }
1100
+ function pluginLogLevelEnabled(level, minimum) {
1101
+ return PLUGIN_LOG_LEVELS.indexOf(level) >= PLUGIN_LOG_LEVELS.indexOf(minimum);
1102
+ }
1103
+ function normalizePluginLogSource(raw) {
1104
+ const stripped = String(raw || "").trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1105
+ const bounded = (/^[a-z]/.test(stripped) ? stripped : `p-${stripped}`).slice(0, 64).replace(/-+$/g, "");
1106
+ return SOURCE_RE.test(bounded) ? bounded : "dsh-plugin";
1107
+ }
1108
+ function normalizePluginLogEvent(raw) {
1109
+ const cleaned = String(raw || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 128);
1110
+ return EVENT_RE.test(cleaned) ? cleaned : "plugin.event";
1111
+ }
1112
+ /**
1113
+ * Parse a level specification like `debug` or `dsh-zhihu=debug,dsh-dot:warn,info`.
1114
+ * Source-specific rules win over a bare fallback level.
1115
+ */
1116
+ function parsePluginLogLevel(spec, source, fallback = DEFAULT_PLUGIN_LOG_LEVEL) {
1117
+ if (!spec || typeof spec !== "string") return fallback;
1118
+ const normalizedSource = source ? normalizePluginLogSource(source) : void 0;
1119
+ let wildcard;
1120
+ for (const token of spec.split(",").map((part) => part.trim()).filter(Boolean)) {
1121
+ const lowered = token.toLowerCase();
1122
+ if (isPluginLogLevel(lowered)) {
1123
+ wildcard = lowered;
1124
+ continue;
1125
+ }
1126
+ const match = /^([^=:]+)[=:]([a-z]+)$/i.exec(token);
1127
+ if (!match) continue;
1128
+ const target = match[1].trim();
1129
+ const candidate = match[2].trim().toLowerCase();
1130
+ if (!isPluginLogLevel(candidate)) continue;
1131
+ if (target === "*") {
1132
+ wildcard = candidate;
1133
+ continue;
1134
+ }
1135
+ if (normalizedSource && normalizePluginLogSource(target) === normalizedSource) return candidate;
1136
+ }
1137
+ return wildcard ?? fallback;
1138
+ }
1139
+ function sanitizePluginLogText(text, maxChars = PLUGIN_LOG_MAX_MESSAGE_CHARS) {
1140
+ if (!text) return "";
1141
+ if (CREDENTIAL_BODY_RE.test(text)) return "[omitted credential material]";
1142
+ if (CHAT_PAYLOAD_RE.test(text)) return "[omitted chat payload]";
1143
+ return truncateLogText(text.replace(AUTH_HEADER_RE, "$1: [redacted]").replace(BEARER_RE, "Bearer [redacted]").replace(SECRET_FIELD_RE, (full, prefix) => {
1144
+ const value = full.slice(prefix.length);
1145
+ if (value.startsWith("\"")) return `${prefix}"[redacted]"`;
1146
+ if (value.startsWith("'")) return `${prefix}'[redacted]'`;
1147
+ return `${prefix}[redacted]`;
1148
+ }).replace(SECRET_QUERY_RE, "$1[redacted]").replace(URL_CREDENTIAL_RE, "$1[redacted]@").replace(BOOTSTRAP_RE, "/bootstrap/[redacted]").replace(KNOWN_TOKEN_RE, "[redacted]").replace(PATH_RE, "[path]"), maxChars);
1149
+ }
1150
+ function sanitizePluginLogContext(input) {
1151
+ if (!input) return void 0;
1152
+ const output = {};
1153
+ for (const [key, value] of Object.entries(input).slice(0, 32)) {
1154
+ if (value === void 0 || !CONTEXT_KEY_RE.test(key)) continue;
1155
+ if (typeof value === "number" && !Number.isFinite(value)) continue;
1156
+ if (SECRET_KEY_RE.test(key)) {
1157
+ output[key] = "[redacted]";
1158
+ continue;
1159
+ }
1160
+ output[key] = typeof value === "string" ? sanitizePluginLogText(value, 1e3) : value;
1161
+ }
1162
+ if (!Object.keys(output).length) return void 0;
1163
+ if (JSON.stringify(output).length <= 8e3) return output;
1164
+ return {
1165
+ omitted: true,
1166
+ reason: "Diagnostic context exceeded its size limit."
1167
+ };
1168
+ }
1169
+ function sanitizePluginLogError(error) {
1170
+ if (error === void 0) return void 0;
1171
+ const row = error && typeof error === "object" ? error : {};
1172
+ const rawMessage = typeof row.message === "string" ? row.message : String(error);
1173
+ const result = {
1174
+ name: typeof row.name === "string" && row.name.trim() ? sanitizePluginLogText(row.name.trim(), 100) : "Error",
1175
+ message: sanitizePluginLogText(rawMessage) || "Unknown error"
1176
+ };
1177
+ if (typeof row.stack === "string" && row.stack.trim()) result.stack = sanitizePluginLogText(row.stack, 24e3);
1178
+ if (typeof row.code === "string" && /^[A-Za-z0-9_./:-]{1,100}$/.test(row.code)) result.code = sanitizePluginLogText(row.code, 100);
1179
+ else if (typeof row.code === "number" && Number.isFinite(row.code)) result.code = String(row.code);
1180
+ return result;
1181
+ }
1182
+ function mergePluginLogContext(base, extra) {
1183
+ if (!base) return extra;
1184
+ if (!extra) return base;
1185
+ return {
1186
+ ...base,
1187
+ ...extra
1188
+ };
1189
+ }
1190
+ function buildPluginLogEntry(input, meta) {
1191
+ const entry = {
1192
+ version: 1,
1193
+ at: meta.at.toISOString(),
1194
+ level: isPluginLogLevel(input.level) ? input.level : "info",
1195
+ source: normalizePluginLogSource(meta.source),
1196
+ surface: meta.surface,
1197
+ ...meta.profile ? { profile: meta.profile } : {},
1198
+ event: normalizePluginLogEvent(input.event),
1199
+ message: sanitizePluginLogText(input.message || "No diagnostic message was provided.", PLUGIN_LOG_MAX_MESSAGE_CHARS),
1200
+ ...meta.pid !== void 0 ? { pid: meta.pid } : {}
1201
+ };
1202
+ const context = sanitizePluginLogContext(mergePluginLogContext(meta.correlation, input.context));
1203
+ if (context) entry.context = context;
1204
+ const error = sanitizePluginLogError(input.error);
1205
+ if (error) entry.error = error;
1206
+ return entry;
1207
+ }
1208
+ function createChildPluginLogger(parent, bound) {
1209
+ return {
1210
+ get source() {
1211
+ return parent.source;
1212
+ },
1213
+ get file() {
1214
+ return parent.file;
1215
+ },
1216
+ get lastWriteError() {
1217
+ return parent.lastWriteError;
1218
+ },
1219
+ debug: (event, message, context) => parent.debug(event, message, mergePluginLogContext(bound, context)),
1220
+ info: (event, message, context) => parent.info(event, message, mergePluginLogContext(bound, context)),
1221
+ warn: (event, message, error, context) => parent.warn(event, message, error, mergePluginLogContext(bound, context)),
1222
+ error: (event, message, error, context) => parent.error(event, message, error, mergePluginLogContext(bound, context)),
1223
+ fault: (event, message, error, context) => parent.fault(event, message, error, mergePluginLogContext(bound, context)),
1224
+ write: (input) => parent.write({
1225
+ ...input,
1226
+ context: mergePluginLogContext(bound, input.context)
1227
+ }),
1228
+ child: (next) => createChildPluginLogger(parent, mergePluginLogContext(bound, next) ?? {})
1229
+ };
1230
+ }
1231
+ function truncateLogText(text, maxChars) {
1232
+ if (text.length <= maxChars) return text;
1233
+ const marker = "[truncated]\n";
1234
+ if (maxChars <= 12) return text.slice(0, Math.max(0, maxChars));
1235
+ return `${marker}${text.slice(-(maxChars - 12))}`;
1236
+ }
1237
+ //#endregion
1238
+ //#region ../dsh-plugin-kit/src/client-logger.ts
1239
+ const CLIENT_PLUGIN_LOG_GLOBAL = "__KLARKXY_PLUGIN_LOGS__";
1240
+ function readStorageFlag(key) {
1241
+ try {
1242
+ const value = (globalThis.window?.localStorage)?.getItem?.(key);
1243
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1244
+ } catch {
1245
+ return;
1246
+ }
1247
+ }
1248
+ function sharedClientBuffer(limit) {
1249
+ const carrier = globalThis;
1250
+ if (!Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"])) try {
1251
+ carrier[CLIENT_PLUGIN_LOG_GLOBAL] = [];
1252
+ } catch {
1253
+ return [];
1254
+ }
1255
+ const list = carrier[CLIENT_PLUGIN_LOG_GLOBAL];
1256
+ const overflow = list.length - limit;
1257
+ if (overflow > 0) list.splice(0, overflow);
1258
+ return list;
1259
+ }
1260
+ function createClientPluginLogger(source, ctx, options = {}) {
1261
+ const normalized = normalizePluginLogSource(source);
1262
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1263
+ const limit = Math.max(10, options.bufferLimit ?? 500);
1264
+ const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
1265
+ const resolveMinimum = () => {
1266
+ if (options.minimumLevel) return options.minimumLevel;
1267
+ return parsePluginLogLevel(readStorageFlag("dsh-plugin-log-level"), normalized, DEFAULT_PLUGIN_LOG_LEVEL);
1268
+ };
1269
+ const logger = {
1270
+ source: normalized,
1271
+ file: null,
1272
+ lastWriteError: null,
1273
+ debug: (event, message, context) => logger.write({
1274
+ level: "debug",
1275
+ event,
1276
+ message,
1277
+ ...context ? { context } : {}
1278
+ }),
1279
+ info: (event, message, context) => logger.write({
1280
+ level: "info",
1281
+ event,
1282
+ message,
1283
+ ...context ? { context } : {}
1284
+ }),
1285
+ warn: (event, message, error, context) => logger.write({
1286
+ level: "warn",
1287
+ event,
1288
+ message,
1289
+ ...error !== void 0 ? { error } : {},
1290
+ ...context ? { context } : {}
1291
+ }),
1292
+ error: (event, message, error, context) => logger.write({
1293
+ level: "error",
1294
+ event,
1295
+ message,
1296
+ ...error !== void 0 ? { error } : {},
1297
+ ...context ? { context } : {}
1298
+ }),
1299
+ fault: (event, message, error, context) => logger.write({
1300
+ level: "fault",
1301
+ event,
1302
+ message,
1303
+ ...error !== void 0 ? { error } : {},
1304
+ ...context ? { context } : {}
1305
+ }),
1306
+ child: (context) => createChildPluginLogger(logger, context),
1307
+ write(input) {
1308
+ try {
1309
+ if (!pluginLogLevelEnabled(input.level, resolveMinimum())) return false;
1310
+ const entry = buildPluginLogEntry(input, {
1311
+ at: now(),
1312
+ source: normalized,
1313
+ surface: "client"
1314
+ });
1315
+ const buffer = sharedClientBuffer(limit);
1316
+ buffer.push(entry);
1317
+ const overflow = buffer.length - limit;
1318
+ if (overflow > 0) buffer.splice(0, overflow);
1319
+ if (options.forwardToHostLogger !== false && browserCtx?.logger) {
1320
+ const summary = sanitizePluginLogText(`${normalized} [${entry.event}]: ${entry.message}`, 500);
1321
+ if (entry.level === "warn" && typeof browserCtx.logger.warn === "function") browserCtx.logger.warn(summary);
1322
+ else if ((entry.level === "error" || entry.level === "fault") && typeof browserCtx.logger.error === "function") browserCtx.logger.error(summary);
1323
+ }
1324
+ if (readStorageFlag("dsh-plugin-log-console") === "1") {
1325
+ const con = globalThis.console;
1326
+ (entry.level === "debug" ? con?.debug : entry.level === "info" ? con?.info : entry.level === "warn" ? con?.warn : con?.error)?.call(con, `[${normalized}] ${entry.event}: ${entry.message}`, entry);
1327
+ }
1328
+ return true;
1329
+ } catch {
1330
+ return false;
1331
+ }
1332
+ }
1333
+ };
1334
+ return logger;
1335
+ }
1336
+ function installClientPluginLogger(ctx, source, options = {}) {
1337
+ const logger = createClientPluginLogger(source, ctx, options);
1338
+ logger.info("client.loaded", "Client plugin initialized.", options.loadedContext);
1339
+ const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
1340
+ if (typeof browserCtx?.effect === "function") try {
1341
+ browserCtx.effect(() => () => {
1342
+ logger.debug("client.disposed", "Client plugin disposed.");
1343
+ }, `${logger.source}.client-logger`);
1344
+ } catch {}
1345
+ return logger;
1346
+ }
1347
+ //#endregion
1073
1348
  //#region src/client.tsx
1074
1349
  /**
1075
1350
  * Model picker, browser half.
@@ -1621,7 +1896,7 @@ function ModelMenuFallback(props) {
1621
1896
  * follow the default. Memory updates strictly on `result.ok === true`; a
1622
1897
  * failure leaves the remembered route untouched. Exported for the wiring spec.
1623
1898
  */
1624
- function createPreferredSelect(directory, preference) {
1899
+ function createPreferredSelect(directory, preference, logger) {
1625
1900
  return async (selection) => {
1626
1901
  const state = directory.store.getSnapshot();
1627
1902
  const current = state.current;
@@ -1636,11 +1911,22 @@ function createPreferredSelect(directory, preference) {
1636
1911
  };
1637
1912
  }
1638
1913
  const result = await directory.select(final);
1639
- if (result?.ok === true) preference.remember(final, final.reasoningEffort);
1914
+ if (result?.ok === true) {
1915
+ preference.remember(final, final.reasoningEffort);
1916
+ logger?.debug("model.selected", "Applied model selection.", {
1917
+ provider: final.provider,
1918
+ model: final.model,
1919
+ effort: final.reasoningEffort ?? null
1920
+ });
1921
+ } else if (result && result.ok === false) logger?.warn("model.select-rejected", "Model selection failed.", void 0, {
1922
+ provider: final.provider,
1923
+ model: final.model
1924
+ });
1640
1925
  return result;
1641
1926
  };
1642
1927
  }
1643
1928
  function apply(ctx) {
1929
+ const logger = installClientPluginLogger(ctx, "@klarkxy/dsh-model-picker");
1644
1930
  const preference = new EffortPreference();
1645
1931
  ctx.effect(() => {
1646
1932
  const zh = ctx.locale?.register(NS, "zh", dictZh);
@@ -1683,12 +1969,15 @@ function apply(ctx) {
1683
1969
  available: true,
1684
1970
  directory: directory.store,
1685
1971
  load: () => {
1686
- directory.load().catch(() => {});
1972
+ directory.load().catch((error) => {
1973
+ logger.warn("directory.load-failed", "Model directory load failed.", error, { sessionId });
1974
+ });
1687
1975
  },
1688
1976
  resolveText: (text) => ctx.locale?.resolveText(text) ?? String(text),
1689
- select: createPreferredSelect(directory, preference)
1977
+ select: createPreferredSelect(directory, preference, logger)
1690
1978
  };
1691
- } catch {
1979
+ } catch (error) {
1980
+ logger.warn("directory.unavailable", "Could not resolve model directory for session.", error, { sessionId });
1692
1981
  return unavailable;
1693
1982
  }
1694
1983
  }