@klarkxy/dsh-plugin-kit 0.7.0 → 0.8.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [简体中文](docs/README.zh-CN.md)
6
6
 
7
- Shared runtime and UI support library for the DeepSeek Harness plugins in this repository — covering the default auxiliary model, shared session settings, storage and host-client helpers, official UI styling, the optional shared model picker hookup, and subagent session detection. Plugins that depend on it install it automatically; plugin developers should refer to the source code and type declarations.
7
+ Shared runtime and UI support library for the DeepSeek Harness plugins in this repository — covering the default auxiliary model, shared session settings, diagnostic logging, storage and host-client helpers, official UI styling, the optional shared model picker hookup, and subagent session detection. Plugins that depend on it install it automatically; plugin developers should refer to the source code and type declarations.
8
8
 
9
9
  ## License
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [English](../README.md)
6
6
 
7
- 本仓库 DeepSeek Harness 插件共用的运行时与界面支撑库,提供默认辅助模型、共享会话设置页、存储与前后端通信辅助、官方 UI 样式、可选共享模型选择器接入及子代理会话识别等共享能力。依赖它的插件会自动安装;插件开发者请直接参考源码和类型声明。
7
+ 本仓库 DeepSeek Harness 插件共用的运行时与界面支撑库,提供默认辅助模型、共享会话设置页、诊断日志、存储与前后端通信辅助、官方 UI 样式、可选共享模型选择器接入及子代理会话识别等共享能力。依赖它的插件会自动安装;插件开发者请直接参考源码和类型声明。
8
8
 
9
9
  ## 许可证
10
10
 
@@ -1,4 +1,5 @@
1
1
  import { AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_NS, AUXILIARY_MODEL_SERVICE, getAuxiliaryModel } from "./contracts.js";
2
+ import { createPluginLogger } from "./logger.js";
2
3
  import { registerHostRpc } from "./host-rpc.js";
3
4
  import { notifyModelSettingsChanged, registerModelSettings } from "./model-settings.js";
4
5
  import { createModelSettingsChanges } from "./model-settings-changes.js";
@@ -84,7 +85,13 @@ function setSharedModel(state, ctx, input, expectedRevision) {
84
85
  if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
85
86
  const commit = state.tail.then(async () => {
86
87
  if (state.closed || !state.domain) throw new Error("Auxiliary model settings are unloading; retry in a moment");
87
- if (expectedRevision !== state.data.revision) throw conflictError("Auxiliary model settings changed; reload and retry");
88
+ if (expectedRevision !== state.data.revision) {
89
+ createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).warn("auxiliary-model.conflict", "Auxiliary model update rejected due to revision conflict.", void 0, {
90
+ expectedRevision: Number(expectedRevision),
91
+ currentRevision: state.data.revision
92
+ });
93
+ throw conflictError("Auxiliary model settings changed; reload and retry");
94
+ }
88
95
  const next = {
89
96
  revision: state.data.revision + 1,
90
97
  model: modelRouteOverride(normalized) ?? null
@@ -94,6 +101,11 @@ function setSharedModel(state, ctx, input, expectedRevision) {
94
101
  model: next.model
95
102
  });
96
103
  state.data = next;
104
+ createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).info("auxiliary-model.updated", "Updated global auxiliary model default.", {
105
+ revision: next.revision,
106
+ provider: next.model?.provider ?? null,
107
+ model: next.model?.model ?? null
108
+ });
97
109
  notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision);
98
110
  return next;
99
111
  });
@@ -101,6 +113,7 @@ function setSharedModel(state, ctx, input, expectedRevision) {
101
113
  return commit;
102
114
  }
103
115
  function initScope(state, ctx, scope) {
116
+ const log = createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false });
104
117
  const generation = ++state.generation;
105
118
  let disposed = false;
106
119
  scope.effect(() => () => {
@@ -132,6 +145,11 @@ function initScope(state, ctx, scope) {
132
145
  };
133
146
  state.error = void 0;
134
147
  state.available = true;
148
+ log.info("auxiliary-model.opened", "Shared auxiliary model store opened.", {
149
+ generation,
150
+ revision: state.data.revision,
151
+ hasModel: Boolean(state.data.model)
152
+ });
135
153
  scope.effect(() => registerModelSettings(scope, {
136
154
  ns: AUXILIARY_MODEL_NS,
137
155
  title: {
@@ -215,7 +233,10 @@ function initScope(state, ctx, scope) {
215
233
  message: `Unknown endpoint: ${endpoint}`
216
234
  }
217
235
  };
218
- }, { isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint) }));
236
+ }, {
237
+ isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint),
238
+ logger: log
239
+ }));
219
240
  createModelSettingsChanges(scope, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS);
220
241
  scope.effect(() => () => {
221
242
  if (state.generation === generation) {
@@ -224,6 +245,8 @@ function initScope(state, ctx, scope) {
224
245
  }
225
246
  });
226
247
  })().catch((error) => {
248
+ if (error && typeof error === "object" && error[RECOVERABLE]) log.debug("auxiliary-model.superseded", "Shared auxiliary model init superseded during open.", { generation });
249
+ else log.error("auxiliary-model.init-failed", "Shared auxiliary model store failed to open.", error, { generation });
227
250
  if (error && typeof error === "object") error.auxGeneration = generation;
228
251
  if (state.generation === generation) {
229
252
  state.available = false;
@@ -247,6 +270,7 @@ function teardown(state, ctx) {
247
270
  try {
248
271
  await state.fiber?.dispose();
249
272
  } catch (error) {
273
+ createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).fault("auxiliary-model.dispose-failed", "Shared auxiliary model owner fiber failed to dispose.", error);
250
274
  try {
251
275
  ctx.logger?.warn("auxiliary-model: fiber dispose failed", error);
252
276
  } catch {}
@@ -1 +1 @@
1
- {"version":3,"file":"auxiliary-model.js","names":[],"sources":["../src/auxiliary-model.ts"],"sourcesContent":["/**\n * Shared auxiliary-model default: host-side lifecycle.\n *\n * The default model is repository-shared infrastructure, not one plugin's\n * business state. Its owner is a dedicated Cordis fiber started on the root\n * context by the first participant and disposed by the last release, so the\n * storage domain, the Typert change stream, the RPC channel and the\n * model-settings provider all live and die with the shared owner — never\n * with a business plugin's fiber. Every bundler's own copy of this module\n * rendezvous through `ctx.root`, so no participant depends on another being\n * enabled.\n *\n * Concurrency:\n * - Writes serialize on `state.tail`; the revision check and the bump both\n * happen inside the queued commit, so two callers carrying the same\n * expectedRevision cannot both succeed.\n * - `state.closing` is the single teardown promise: it settles init, drains\n * queued writes, disposes the owner fiber (route, stream, provider,\n * domain) and only then unprovides the state, so a re-entrant retain can\n * always await it and reinstall cleanly.\n * - Initialization and stored-row failures are sticky (`state.error`) and\n * rethrow from `snapshot()`: a storage error must never read as \"no\n * default\" and silently fall back to another model.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { modelRouteOverride, normalizeModelRoute, type ModelRoute } from '@klarkxy/dsh-model-route'\nimport {\n AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS,\n AUXILIARY_MODEL_SERVICE, type AuxiliaryModelDefaults, type AuxiliaryModelShared, type RpcResult,\n} from './contracts.ts'\nimport { registerHostRpc, type HostRpcContext } from './host-rpc.ts'\nimport { createModelSettingsChanges } from './model-settings-changes.ts'\nimport { registerModelSettings, notifyModelSettingsChanged, type ModelSettingsEvents } from './model-settings.ts'\n\nconst DOMAIN_NAME = 'dsh_auxiliary'\nconst DOMAIN_VERSION = 1\nconst TABLE = 'settings'\nconst KEY = 'defaults'\nconst INJECT = ['storageDomain', 'webServer', 'connection'] as const\n\ninterface DomainHandle {\n table(name: string): {\n get(key: string): { revision: number; model: ModelRoute | null } | undefined\n put(key: string, value: unknown): Promise<void>\n }\n close(): Promise<void>\n}\n\ninterface Scope extends HostRpcContext, ModelSettingsEvents {\n storageDomain: { open(spec: unknown): Promise<DomainHandle> }\n effect(callback: () => (() => void) | Promise<(() => void) | void>): unknown\n}\n\ninterface RootLike {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n inject(deps: readonly string[], callback: (scope: Scope) => void): { dispose(): Promise<void> } & PromiseLike<unknown>\n}\n\ninterface SharedState extends AuxiliaryModelShared {\n ready: Promise<void>\n users: number\n closed: boolean\n /** True only while the owner fiber's domain is open; snapshot() gates on it. */\n available: boolean\n /** Generation of the latest scope init; a stale run must not win. */\n generation: number\n /** Sticky init/storage failure; snapshot() rethrows it. */\n error?: unknown\n closing?: Promise<void>\n domain?: DomainHandle\n data: AuxiliaryModelDefaults\n tail: Promise<void>\n fiber?: { dispose(): Promise<void> }\n scopeInit?: Promise<void>\n stopUnprovide?: () => void\n teardown(): Promise<void>\n}\n\n/** Marker on the benign \"released while the domain was still opening\" error. */\nconst RECOVERABLE = Symbol.for('klarkxy.dsh.auxiliary-model.recoverable')\nfunction recoverable<T extends object>(error: T): T & { [RECOVERABLE]?: boolean } {\n (error as Record<symbol, unknown>)[RECOVERABLE] = true\n return error as T & { [RECOVERABLE]?: boolean }\n}\n\nasync function openSharedDomain(scope: Scope): Promise<DomainHandle> {\n const [{ defineDomain, domainTable }, { z }] = await Promise.all([\n import('@deepseek-ai/dsh-storage-domain'), import('zod'),\n ])\n // Strict, write-side semantics at the durable boundary: identifiers must be\n // non-blank, already trimmed and control-free, or the row is rejected —\n // a malformed stored route must never normalize into \"no default\".\n const identifier = z.string().min(1).max(250).refine(\n value => value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value),\n 'model route identifiers must be non-blank, trimmed and control-free',\n )\n const model = z.union([\n z.object({\n provider: identifier,\n model: identifier,\n reasoningEffort: identifier.max(80).optional(),\n }).strict(),\n z.null(),\n ])\n return scope.storageDomain.open(defineDomain({\n name: DOMAIN_NAME, version: DOMAIN_VERSION,\n tables: {\n [TABLE]: domainTable(z.object({\n revision: z.number().int().nonnegative(),\n model,\n }).strict()),\n },\n }))\n}\n\n/** Marks an optimistic-lock conflict so the RPC envelope can carry 'conflict'. */\nfunction conflictError(message: string): Error & { conflict: true } {\n return Object.assign(new Error(message), { conflict: true as const })\n}\n\n/** RPC-visible error classification; storage failures stay sanitized. */\nfunction rpcFailure(error: unknown): RpcResult {\n if (error && typeof error === 'object' && 'conflict' in error) {\n return { ok: false, error: { code: 'auxiliary-model/conflict', message: error instanceof Error ? error.message : 'Revision conflict' } }\n }\n if (error instanceof Error && /Invalid model route|valid expected revision|Unknown auxiliary model setting/.test(error.message)) {\n return { ok: false, error: { code: 'auxiliary-model/rejected', message: error.message } }\n }\n return { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } }\n}\n\n/**\n * Serialized optimistic-lock write; resolves after the durable put commits.\n * Revision verification happens inside the queue: two writers carrying the\n * same expectedRevision race for one slot and only the first commits.\n * Notification fires only after commit, so a failed observer can never turn\n * an already committed write into an apparent save failure.\n */\nfunction setSharedModel(state: SharedState, ctx: Context, input: unknown, expectedRevision: unknown): Promise<AuxiliaryModelDefaults> {\n if (!Number.isSafeInteger(expectedRevision)) return Promise.reject(conflictError('A valid expected revision is required; reload and retry'))\n const row = input && typeof input === 'object' && !Array.isArray(input) ? input as Record<string, unknown> : undefined\n const validField = (value: unknown): value is string =>\n typeof value === 'string' && value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value)\n if (!row || ['provider', 'model', 'reasoningEffort'].some(key => row[key] !== undefined && !validField(row[key]))) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const normalized = normalizeModelRoute(row)\n if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const commit = state.tail.then(async (): Promise<AuxiliaryModelDefaults> => {\n if (state.closed || !state.domain) throw new Error('Auxiliary model settings are unloading; retry in a moment')\n if (expectedRevision !== state.data.revision) throw conflictError('Auxiliary model settings changed; reload and retry')\n const next: AuxiliaryModelDefaults = { revision: state.data.revision + 1, model: modelRouteOverride(normalized) ?? null }\n await state.domain.table(TABLE).put(KEY, { revision: next.revision, model: next.model })\n state.data = next\n notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision)\n return next\n })\n // The tail itself never rejects: a failed commit must not poison later writes.\n state.tail = commit.then(() => {}, () => {})\n return commit\n}\n\nfunction initScope(state: SharedState, ctx: Context, scope: Scope): Promise<void> {\n // A dependency loss unloads this fiber (running its disposers) and a\n // remount re-runs the callback (bumping state.generation), so an init that\n // was still opening always detects its own supersession through these two\n // public signals. Service identity is intentionally NOT compared anywhere:\n // `ctx.<service>` reads hand out a fresh traceable proxy per access for\n // every tracker'd impl (all Service subclasses — storageDomain, webServer,\n // connection), so identity comparisons reported a fake dep swap after each\n // await and aborted every open with 'released during open'.\n const generation = ++state.generation\n let disposed = false\n // Registered synchronously while the callback runs; a throw means the\n // fiber is already inactive and this init must not open anything.\n scope.effect(() => () => { disposed = true })\n const stale = () => state.closed || disposed || state.generation !== generation\n const run = (async () => {\n // Reads must stay unavailable until this init completes.\n state.available = false\n const domain = await openSharedDomain(scope)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store released during open')) }\n state.domain = domain\n // scope.effect throws on an inactive fiber (disposed mid-open): close the\n // domain directly so it is never leaked, then rethrow — a store that\n // cannot register its own cleanup must not publish data.\n try { scope.effect(() => () => domain.close()) } catch (error) { await domain.close(); throw error }\n // Strict schema: a malformed stored row fails the open path instead of\n // silently collapsing to \"no default\".\n const stored = domain.table(TABLE).get(KEY)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store superseded during open')) }\n state.data = { revision: stored?.revision ?? 0, model: stored?.model ?? null }\n state.error = undefined\n state.available = true\n scope.effect(() => registerModelSettings(scope, {\n ns: AUXILIARY_MODEL_NS,\n title: { zh: '辅助模型', en: 'Auxiliary model' },\n read(signal) {\n signal.throwIfAborted()\n const { revision, model } = state.data\n return {\n revision,\n fields: [{\n path: ['model'],\n label: { zh: '全局辅助模型', en: 'Global auxiliary model' },\n current: model ?? undefined,\n allowFollow: true,\n followLabel: { zh: '当前会话模型', en: 'Current conversation model' },\n }],\n }\n },\n async write(path, route, expectedRevision, signal) {\n signal.throwIfAborted()\n if (path.length !== 1 || path[0] !== 'model') throw new Error('Unknown auxiliary model setting')\n return setSharedModel(state, ctx, route, expectedRevision)\n },\n }))\n scope.effect(() => registerHostRpc(scope, AUXILIARY_MODEL_CHANNEL, async (endpoint, payload, signal) => {\n signal.throwIfAborted()\n if (state.closed) return { ok: false, error: { code: 'auxiliary-model/unloading', message: 'Auxiliary model settings are unloading; retry in a moment' } } satisfies RpcResult\n if (!state.available) {\n return state.error\n ? { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } } satisfies RpcResult\n : { ok: false, error: { code: 'auxiliary-model/unavailable', message: 'Auxiliary model settings are unavailable; retry in a moment' } } satisfies RpcResult\n }\n if (endpoint === 'defaults.get') {\n const { revision, model } = state.data\n return { ok: true, value: { revision, model } } satisfies RpcResult\n }\n if (endpoint === 'defaults.set') {\n try {\n const body = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload as Record<string, unknown> : {}\n const value = await setSharedModel(state, ctx, body.model, body.expectedRevision)\n signal.throwIfAborted()\n return { ok: true, value } satisfies RpcResult\n } catch (error) {\n if (signal.aborted) throw error\n return rpcFailure(error)\n }\n }\n return { ok: false, error: { code: 'auxiliary-model/unknown-endpoint', message: `Unknown endpoint: ${endpoint}` } } satisfies RpcResult\n }, { isEndpoint: endpoint => /^[a-z.]+$/.test(endpoint) }))\n // The change stream lives and dies with this shared fiber.\n createModelSettingsChanges(scope as unknown as Context, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS)\n // Disposers run LIFO on unload: registered last, this runs FIRST when a\n // dependency remount tears the scope down — reads go unavailable before\n // the domain closes, so no stale default ever leaks. Gated on generation\n // alone: the same generation's unload must clear state even when its dep\n // impl just changed; only a superseded scope's teardown stays out.\n scope.effect(() => () => { if (state.generation === generation) { state.available = false; state.domain = undefined } })\n })().catch(error => {\n // Tag the failure with its generation so ready.catch can distinguish a\n // superseded init's rejection from the live scope's.\n if (error && typeof error === 'object') (error as Record<string, unknown>).auxGeneration = generation\n // Only THIS generation's failure poisons the store: a superseded init's\n // rejection must not clear a newer scope's available state.\n if (state.generation === generation) {\n state.available = false\n state.error = error\n }\n throw error\n })\n // Retainers always await the CURRENT init: re-arm state.ready so nobody is\n // left waiting on a superseded generation's settled promise. Both promises\n // get a handled branch so a rejected init never surfaces as an unhandled\n // rejection while awaiters still see it.\n run.catch(() => {})\n const armed = run.then(() => {})\n armed.catch(() => {})\n state.ready = armed\n return run\n}\n\nfunction teardown(state: SharedState, ctx: Context): Promise<void> {\n if (!state.closing) {\n state.closed = true\n const root = ctx.root as unknown as RootLike\n state.closing = (async () => {\n // A still-opening init settles here so its domain/fiber are disposed.\n await state.scopeInit?.catch(() => {})\n await state.tail.catch(() => {})\n try { await state.fiber?.dispose() } catch (error) {\n // A failed dispose may leave the domain open; log loudly so the next\n // open's 'already open' error is attributable instead of mysterious.\n try { (ctx as unknown as { logger?: { warn(...args: unknown[]): void } }).logger?.warn('auxiliary-model: fiber dispose failed', error) }\n catch { /* no logger */ }\n }\n try {\n if (root.get(AUXILIARY_MODEL_SERVICE, false) === state) state.stopUnprovide?.()\n } catch { /* root already gone */ }\n state.stopUnprovide = undefined\n })()\n }\n return state.closing\n}\n\nfunction createSharedState(ctx: Context): SharedState {\n const root = ctx.root as unknown as RootLike\n const state: SharedState = {\n users: 0, closed: false, available: false, generation: 0, tail: Promise.resolve(),\n data: { revision: 0, model: null },\n snapshot() {\n // A persisted-open failure propagates: falling back to another model\n // after a storage error would silently switch what the user pays for.\n if (this.error) throw this.error\n // While the owner fiber re-initializes after a dependency remount, the\n // stored default is unreadable: reads fail instead of serving stale data.\n if (!this.available) throw new Error('Auxiliary model store is unavailable; retry in a moment')\n return this.data\n },\n ready: Promise.resolve(),\n teardown() { return teardown(this, ctx) },\n }\n // Strict requirement: every service must already be ACTIVE. Callers are\n // plugins declaring these in inject — Cordis only runs their apply once the\n // deps are active, so a missing or still-pending service is a hard error,\n // not something to wait on. Retry is the caller's Cordis lifecycle, not ours.\n const inactive = INJECT.filter(service => {\n try { return root.get(service, true) === undefined } catch { return true }\n })\n if (inactive.length) {\n throw new Error(`retainAuxiliaryModel requires active ${inactive.join(', ')}; declare them in inject`)\n }\n state.fiber = root.inject(INJECT, scope => { state.scopeInit = initScope(state, ctx, scope) })\n state.ready = Promise.resolve(state.fiber).then(() => {\n // The owner fiber ran to settle: its scope must have started init, and\n // that init must complete before the store reads as ready. A dep dropping\n // between the strict check and inject leaves the fiber PENDING; Cordis\n // re-runs it when the dep returns — never a silent half-initialized read.\n if (!state.scopeInit) throw recoverable(new Error('Auxiliary model owner fiber settled without initializing its scope'))\n return state.scopeInit\n })\n // A failed open must not leave a live-looking state behind: mark it\n // closed and tear it down so the next retain reinstalls. A rejection\n // tagged with a superseded generation describes a scope this state has\n // already moved past — it must not tear down a newer, live scope.\n state.ready.catch(error => {\n const tag = (error as { auxGeneration?: number }).auxGeneration\n if (tag !== undefined && tag !== state.generation) return\n state.error = state.error ?? error\n state.closed = true\n void state.teardown()\n })\n // Publish only after construction completes: another retainer can now\n // either reuse this state or watch its closed flag.\n state.stopUnprovide = root.provide(AUXILIARY_MODEL_SERVICE, state)\n return state\n}\n\nasync function obtainShared(ctx: Context): Promise<SharedState> {\n const root = ctx.root as unknown as RootLike\n for (;;) {\n const existing = root.get(AUXILIARY_MODEL_SERVICE, false) as SharedState | undefined\n if (existing) {\n if (!existing.closed) return existing\n // closing always settles; wait for it, then re-lookup.\n await existing.closing\n continue\n }\n return createSharedState(ctx)\n }\n}\n\n/**\n * Retain the shared auxiliary-model store for the calling plugin.\n *\n * Resolves once storage is open and the read/write surfaces are live, with a\n * release function; register it as `ctx.effect(() => release)` so the store\n * unloads with the plugin. The first participant opens the shared owner\n * fiber; `await release()` drains pending writes and fully tears the owner\n * down when the last participant leaves. Requires `storageDomain`,\n * `webServer` and `connection` to exist on the host.\n *\n * Concurrent retainers share one owner; a retain arriving during teardown\n * waits for the closing owner and then reinstalls — it never inherits a\n * half-torn-down state.\n */\nexport async function retainAuxiliaryModel(ctx: Context): Promise<() => Promise<void>> {\n for (;;) {\n const state = await obtainShared(ctx)\n // One await, one answer: a failed or superseded init rejects the retainer\n // once. The caller's plugin lifecycle (inject refresh) drives any retry —\n // no same-state spin here.\n await state.ready\n if (state.closed) continue\n state.users += 1\n let released = false\n return async () => {\n if (released) return\n released = true\n state.users -= 1\n if (state.users <= 0) await state.teardown()\n else await state.tail.catch(() => {})\n }\n }\n}\n\n/**\n * Re-exported here so consumers reading the shared default use the same\n * subpath as the store lifecycle: '@klarkxy/dsh-plugin-kit/auxiliary-model'.\n */\nexport { getAuxiliaryModel } from './contracts.ts'\n"],"mappings":"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"}
1
+ {"version":3,"file":"auxiliary-model.js","names":[],"sources":["../src/auxiliary-model.ts"],"sourcesContent":["/**\n * Shared auxiliary-model default: host-side lifecycle.\n *\n * The default model is repository-shared infrastructure, not one plugin's\n * business state. Its owner is a dedicated Cordis fiber started on the root\n * context by the first participant and disposed by the last release, so the\n * storage domain, the Typert change stream, the RPC channel and the\n * model-settings provider all live and die with the shared owner — never\n * with a business plugin's fiber. Every bundler's own copy of this module\n * rendezvous through `ctx.root`, so no participant depends on another being\n * enabled.\n *\n * Concurrency:\n * - Writes serialize on `state.tail`; the revision check and the bump both\n * happen inside the queued commit, so two callers carrying the same\n * expectedRevision cannot both succeed.\n * - `state.closing` is the single teardown promise: it settles init, drains\n * queued writes, disposes the owner fiber (route, stream, provider,\n * domain) and only then unprovides the state, so a re-entrant retain can\n * always await it and reinstall cleanly.\n * - Initialization and stored-row failures are sticky (`state.error`) and\n * rethrow from `snapshot()`: a storage error must never read as \"no\n * default\" and silently fall back to another model.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { modelRouteOverride, normalizeModelRoute, type ModelRoute } from '@klarkxy/dsh-model-route'\nimport {\n AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS,\n AUXILIARY_MODEL_SERVICE, type AuxiliaryModelDefaults, type AuxiliaryModelShared, type RpcResult,\n} from './contracts.ts'\nimport { registerHostRpc, type HostRpcContext } from './host-rpc.ts'\nimport { createPluginLogger } from './logger.ts'\nimport { createModelSettingsChanges } from './model-settings-changes.ts'\nimport { registerModelSettings, notifyModelSettingsChanged, type ModelSettingsEvents } from './model-settings.ts'\n\nconst DOMAIN_NAME = 'dsh_auxiliary'\nconst DOMAIN_VERSION = 1\nconst TABLE = 'settings'\nconst KEY = 'defaults'\nconst INJECT = ['storageDomain', 'webServer', 'connection'] as const\n\ninterface DomainHandle {\n table(name: string): {\n get(key: string): { revision: number; model: ModelRoute | null } | undefined\n put(key: string, value: unknown): Promise<void>\n }\n close(): Promise<void>\n}\n\ninterface Scope extends HostRpcContext, ModelSettingsEvents {\n storageDomain: { open(spec: unknown): Promise<DomainHandle> }\n effect(callback: () => (() => void) | Promise<(() => void) | void>): unknown\n}\n\ninterface RootLike {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n inject(deps: readonly string[], callback: (scope: Scope) => void): { dispose(): Promise<void> } & PromiseLike<unknown>\n}\n\ninterface SharedState extends AuxiliaryModelShared {\n ready: Promise<void>\n users: number\n closed: boolean\n /** True only while the owner fiber's domain is open; snapshot() gates on it. */\n available: boolean\n /** Generation of the latest scope init; a stale run must not win. */\n generation: number\n /** Sticky init/storage failure; snapshot() rethrows it. */\n error?: unknown\n closing?: Promise<void>\n domain?: DomainHandle\n data: AuxiliaryModelDefaults\n tail: Promise<void>\n fiber?: { dispose(): Promise<void> }\n scopeInit?: Promise<void>\n stopUnprovide?: () => void\n teardown(): Promise<void>\n}\n\n/** Marker on the benign \"released while the domain was still opening\" error. */\nconst RECOVERABLE = Symbol.for('klarkxy.dsh.auxiliary-model.recoverable')\nfunction recoverable<T extends object>(error: T): T & { [RECOVERABLE]?: boolean } {\n (error as Record<symbol, unknown>)[RECOVERABLE] = true\n return error as T & { [RECOVERABLE]?: boolean }\n}\n\nasync function openSharedDomain(scope: Scope): Promise<DomainHandle> {\n const [{ defineDomain, domainTable }, { z }] = await Promise.all([\n import('@deepseek-ai/dsh-storage-domain'), import('zod'),\n ])\n // Strict, write-side semantics at the durable boundary: identifiers must be\n // non-blank, already trimmed and control-free, or the row is rejected —\n // a malformed stored route must never normalize into \"no default\".\n const identifier = z.string().min(1).max(250).refine(\n value => value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value),\n 'model route identifiers must be non-blank, trimmed and control-free',\n )\n const model = z.union([\n z.object({\n provider: identifier,\n model: identifier,\n reasoningEffort: identifier.max(80).optional(),\n }).strict(),\n z.null(),\n ])\n return scope.storageDomain.open(defineDomain({\n name: DOMAIN_NAME, version: DOMAIN_VERSION,\n tables: {\n [TABLE]: domainTable(z.object({\n revision: z.number().int().nonnegative(),\n model,\n }).strict()),\n },\n }))\n}\n\n/** Marks an optimistic-lock conflict so the RPC envelope can carry 'conflict'. */\nfunction conflictError(message: string): Error & { conflict: true } {\n return Object.assign(new Error(message), { conflict: true as const })\n}\n\n/** RPC-visible error classification; storage failures stay sanitized. */\nfunction rpcFailure(error: unknown): RpcResult {\n if (error && typeof error === 'object' && 'conflict' in error) {\n return { ok: false, error: { code: 'auxiliary-model/conflict', message: error instanceof Error ? error.message : 'Revision conflict' } }\n }\n if (error instanceof Error && /Invalid model route|valid expected revision|Unknown auxiliary model setting/.test(error.message)) {\n return { ok: false, error: { code: 'auxiliary-model/rejected', message: error.message } }\n }\n return { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } }\n}\n\n/**\n * Serialized optimistic-lock write; resolves after the durable put commits.\n * Revision verification happens inside the queue: two writers carrying the\n * same expectedRevision race for one slot and only the first commits.\n * Notification fires only after commit, so a failed observer can never turn\n * an already committed write into an apparent save failure.\n */\nfunction setSharedModel(state: SharedState, ctx: Context, input: unknown, expectedRevision: unknown): Promise<AuxiliaryModelDefaults> {\n if (!Number.isSafeInteger(expectedRevision)) return Promise.reject(conflictError('A valid expected revision is required; reload and retry'))\n const row = input && typeof input === 'object' && !Array.isArray(input) ? input as Record<string, unknown> : undefined\n const validField = (value: unknown): value is string =>\n typeof value === 'string' && value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value)\n if (!row || ['provider', 'model', 'reasoningEffort'].some(key => row[key] !== undefined && !validField(row[key]))) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const normalized = normalizeModelRoute(row)\n if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const commit = state.tail.then(async (): Promise<AuxiliaryModelDefaults> => {\n if (state.closed || !state.domain) throw new Error('Auxiliary model settings are unloading; retry in a moment')\n if (expectedRevision !== state.data.revision) {\n createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false }).warn(\n 'auxiliary-model.conflict',\n 'Auxiliary model update rejected due to revision conflict.',\n undefined,\n { expectedRevision: Number(expectedRevision), currentRevision: state.data.revision },\n )\n throw conflictError('Auxiliary model settings changed; reload and retry')\n }\n const next: AuxiliaryModelDefaults = { revision: state.data.revision + 1, model: modelRouteOverride(normalized) ?? null }\n await state.domain.table(TABLE).put(KEY, { revision: next.revision, model: next.model })\n state.data = next\n createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false }).info(\n 'auxiliary-model.updated',\n 'Updated global auxiliary model default.',\n { revision: next.revision, provider: next.model?.provider ?? null, model: next.model?.model ?? null },\n )\n notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision)\n return next\n })\n // The tail itself never rejects: a failed commit must not poison later writes.\n state.tail = commit.then(() => {}, () => {})\n return commit\n}\n\nfunction initScope(state: SharedState, ctx: Context, scope: Scope): Promise<void> {\n const log = createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false })\n // A dependency loss unloads this fiber (running its disposers) and a\n // remount re-runs the callback (bumping state.generation), so an init that\n // was still opening always detects its own supersession through these two\n // public signals. Service identity is intentionally NOT compared anywhere:\n // `ctx.<service>` reads hand out a fresh traceable proxy per access for\n // every tracker'd impl (all Service subclasses — storageDomain, webServer,\n // connection), so identity comparisons reported a fake dep swap after each\n // await and aborted every open with 'released during open'.\n const generation = ++state.generation\n let disposed = false\n // Registered synchronously while the callback runs; a throw means the\n // fiber is already inactive and this init must not open anything.\n scope.effect(() => () => { disposed = true })\n const stale = () => state.closed || disposed || state.generation !== generation\n const run = (async () => {\n // Reads must stay unavailable until this init completes.\n state.available = false\n const domain = await openSharedDomain(scope)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store released during open')) }\n state.domain = domain\n // scope.effect throws on an inactive fiber (disposed mid-open): close the\n // domain directly so it is never leaked, then rethrow — a store that\n // cannot register its own cleanup must not publish data.\n try { scope.effect(() => () => domain.close()) } catch (error) { await domain.close(); throw error }\n // Strict schema: a malformed stored row fails the open path instead of\n // silently collapsing to \"no default\".\n const stored = domain.table(TABLE).get(KEY)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store superseded during open')) }\n state.data = { revision: stored?.revision ?? 0, model: stored?.model ?? null }\n state.error = undefined\n state.available = true\n log.info('auxiliary-model.opened', 'Shared auxiliary model store opened.', {\n generation, revision: state.data.revision, hasModel: Boolean(state.data.model),\n })\n scope.effect(() => registerModelSettings(scope, {\n ns: AUXILIARY_MODEL_NS,\n title: { zh: '辅助模型', en: 'Auxiliary model' },\n read(signal) {\n signal.throwIfAborted()\n const { revision, model } = state.data\n return {\n revision,\n fields: [{\n path: ['model'],\n label: { zh: '全局辅助模型', en: 'Global auxiliary model' },\n current: model ?? undefined,\n allowFollow: true,\n followLabel: { zh: '当前会话模型', en: 'Current conversation model' },\n }],\n }\n },\n async write(path, route, expectedRevision, signal) {\n signal.throwIfAborted()\n if (path.length !== 1 || path[0] !== 'model') throw new Error('Unknown auxiliary model setting')\n return setSharedModel(state, ctx, route, expectedRevision)\n },\n }))\n scope.effect(() => registerHostRpc(scope, AUXILIARY_MODEL_CHANNEL, async (endpoint, payload, signal) => {\n signal.throwIfAborted()\n if (state.closed) return { ok: false, error: { code: 'auxiliary-model/unloading', message: 'Auxiliary model settings are unloading; retry in a moment' } } satisfies RpcResult\n if (!state.available) {\n return state.error\n ? { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } } satisfies RpcResult\n : { ok: false, error: { code: 'auxiliary-model/unavailable', message: 'Auxiliary model settings are unavailable; retry in a moment' } } satisfies RpcResult\n }\n if (endpoint === 'defaults.get') {\n const { revision, model } = state.data\n return { ok: true, value: { revision, model } } satisfies RpcResult\n }\n if (endpoint === 'defaults.set') {\n try {\n const body = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload as Record<string, unknown> : {}\n const value = await setSharedModel(state, ctx, body.model, body.expectedRevision)\n signal.throwIfAborted()\n return { ok: true, value } satisfies RpcResult\n } catch (error) {\n if (signal.aborted) throw error\n return rpcFailure(error)\n }\n }\n return { ok: false, error: { code: 'auxiliary-model/unknown-endpoint', message: `Unknown endpoint: ${endpoint}` } } satisfies RpcResult\n }, { isEndpoint: endpoint => /^[a-z.]+$/.test(endpoint), logger: log }))\n // The change stream lives and dies with this shared fiber.\n createModelSettingsChanges(scope as unknown as Context, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS)\n // Disposers run LIFO on unload: registered last, this runs FIRST when a\n // dependency remount tears the scope down — reads go unavailable before\n // the domain closes, so no stale default ever leaks. Gated on generation\n // alone: the same generation's unload must clear state even when its dep\n // impl just changed; only a superseded scope's teardown stays out.\n scope.effect(() => () => { if (state.generation === generation) { state.available = false; state.domain = undefined } })\n })().catch(error => {\n if (error && typeof error === 'object' && (error as Record<symbol, unknown>)[RECOVERABLE]) {\n log.debug('auxiliary-model.superseded', 'Shared auxiliary model init superseded during open.', { generation })\n } else {\n log.error('auxiliary-model.init-failed', 'Shared auxiliary model store failed to open.', error, { generation })\n }\n // Tag the failure with its generation so ready.catch can distinguish a\n // superseded init's rejection from the live scope's.\n if (error && typeof error === 'object') (error as Record<string, unknown>).auxGeneration = generation\n // Only THIS generation's failure poisons the store: a superseded init's\n // rejection must not clear a newer scope's available state.\n if (state.generation === generation) {\n state.available = false\n state.error = error\n }\n throw error\n })\n // Retainers always await the CURRENT init: re-arm state.ready so nobody is\n // left waiting on a superseded generation's settled promise. Both promises\n // get a handled branch so a rejected init never surfaces as an unhandled\n // rejection while awaiters still see it.\n run.catch(() => {})\n const armed = run.then(() => {})\n armed.catch(() => {})\n state.ready = armed\n return run\n}\n\nfunction teardown(state: SharedState, ctx: Context): Promise<void> {\n if (!state.closing) {\n state.closed = true\n const root = ctx.root as unknown as RootLike\n state.closing = (async () => {\n // A still-opening init settles here so its domain/fiber are disposed.\n await state.scopeInit?.catch(() => {})\n await state.tail.catch(() => {})\n try { await state.fiber?.dispose() } catch (error) {\n createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false }).fault(\n 'auxiliary-model.dispose-failed',\n 'Shared auxiliary model owner fiber failed to dispose.',\n error,\n )\n // A failed dispose may leave the domain open; log loudly so the next\n // open's 'already open' error is attributable instead of mysterious.\n try { (ctx as unknown as { logger?: { warn(...args: unknown[]): void } }).logger?.warn('auxiliary-model: fiber dispose failed', error) }\n catch { /* no logger */ }\n }\n try {\n if (root.get(AUXILIARY_MODEL_SERVICE, false) === state) state.stopUnprovide?.()\n } catch { /* root already gone */ }\n state.stopUnprovide = undefined\n })()\n }\n return state.closing\n}\n\nfunction createSharedState(ctx: Context): SharedState {\n const root = ctx.root as unknown as RootLike\n const state: SharedState = {\n users: 0, closed: false, available: false, generation: 0, tail: Promise.resolve(),\n data: { revision: 0, model: null },\n snapshot() {\n // A persisted-open failure propagates: falling back to another model\n // after a storage error would silently switch what the user pays for.\n if (this.error) throw this.error\n // While the owner fiber re-initializes after a dependency remount, the\n // stored default is unreadable: reads fail instead of serving stale data.\n if (!this.available) throw new Error('Auxiliary model store is unavailable; retry in a moment')\n return this.data\n },\n ready: Promise.resolve(),\n teardown() { return teardown(this, ctx) },\n }\n // Strict requirement: every service must already be ACTIVE. Callers are\n // plugins declaring these in inject — Cordis only runs their apply once the\n // deps are active, so a missing or still-pending service is a hard error,\n // not something to wait on. Retry is the caller's Cordis lifecycle, not ours.\n const inactive = INJECT.filter(service => {\n try { return root.get(service, true) === undefined } catch { return true }\n })\n if (inactive.length) {\n throw new Error(`retainAuxiliaryModel requires active ${inactive.join(', ')}; declare them in inject`)\n }\n state.fiber = root.inject(INJECT, scope => { state.scopeInit = initScope(state, ctx, scope) })\n state.ready = Promise.resolve(state.fiber).then(() => {\n // The owner fiber ran to settle: its scope must have started init, and\n // that init must complete before the store reads as ready. A dep dropping\n // between the strict check and inject leaves the fiber PENDING; Cordis\n // re-runs it when the dep returns — never a silent half-initialized read.\n if (!state.scopeInit) throw recoverable(new Error('Auxiliary model owner fiber settled without initializing its scope'))\n return state.scopeInit\n })\n // A failed open must not leave a live-looking state behind: mark it\n // closed and tear it down so the next retain reinstalls. A rejection\n // tagged with a superseded generation describes a scope this state has\n // already moved past — it must not tear down a newer, live scope.\n state.ready.catch(error => {\n const tag = (error as { auxGeneration?: number }).auxGeneration\n if (tag !== undefined && tag !== state.generation) return\n state.error = state.error ?? error\n state.closed = true\n void state.teardown()\n })\n // Publish only after construction completes: another retainer can now\n // either reuse this state or watch its closed flag.\n state.stopUnprovide = root.provide(AUXILIARY_MODEL_SERVICE, state)\n return state\n}\n\nasync function obtainShared(ctx: Context): Promise<SharedState> {\n const root = ctx.root as unknown as RootLike\n for (;;) {\n const existing = root.get(AUXILIARY_MODEL_SERVICE, false) as SharedState | undefined\n if (existing) {\n if (!existing.closed) return existing\n // closing always settles; wait for it, then re-lookup.\n await existing.closing\n continue\n }\n return createSharedState(ctx)\n }\n}\n\n/**\n * Retain the shared auxiliary-model store for the calling plugin.\n *\n * Resolves once storage is open and the read/write surfaces are live, with a\n * release function; register it as `ctx.effect(() => release)` so the store\n * unloads with the plugin. The first participant opens the shared owner\n * fiber; `await release()` drains pending writes and fully tears the owner\n * down when the last participant leaves. Requires `storageDomain`,\n * `webServer` and `connection` to exist on the host.\n *\n * Concurrent retainers share one owner; a retain arriving during teardown\n * waits for the closing owner and then reinstalls — it never inherits a\n * half-torn-down state.\n */\nexport async function retainAuxiliaryModel(ctx: Context): Promise<() => Promise<void>> {\n for (;;) {\n const state = await obtainShared(ctx)\n // One await, one answer: a failed or superseded init rejects the retainer\n // once. The caller's plugin lifecycle (inject refresh) drives any retry —\n // no same-state spin here.\n await state.ready\n if (state.closed) continue\n state.users += 1\n let released = false\n return async () => {\n if (released) return\n released = true\n state.users -= 1\n if (state.users <= 0) await state.teardown()\n else await state.tail.catch(() => {})\n }\n }\n}\n\n/**\n * Re-exported here so consumers reading the shared default use the same\n * subpath as the store lifecycle: '@klarkxy/dsh-plugin-kit/auxiliary-model'.\n */\nexport { getAuxiliaryModel } from './contracts.ts'\n"],"mappings":"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"}
package/lib/cli-media.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createReadStream, createWriteStream } from "node:fs";
2
+ import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
3
  import { lstat, mkdir, open, readFile, realpath, rename, writeFile } from "node:fs/promises";
3
4
  import { pipeline } from "node:stream/promises";
4
- import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  //#region src/cli-media.ts
7
7
  var CliMediaError = class extends Error {
@@ -1,3 +1,4 @@
1
+ import { d as PluginLogger } from "./log-shared2.js";
1
2
  import { spawn } from "node:child_process";
2
3
  //#region src/cli-process.d.ts
3
4
  /** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */
@@ -32,6 +33,7 @@ type CliOptions = {
32
33
  maxLineBytes?: number;
33
34
  onLine?: (line: string) => void | Promise<void>;
34
35
  spawnChild?: typeof spawn;
36
+ logger?: PluginLogger;
35
37
  };
36
38
  /** Own one process tree until it exits, including caller cancellation and protocol failure. */
37
39
  declare function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput>;
@@ -58,10 +58,16 @@ var CliProcessError = class extends Error {
58
58
  /** Own one process tree until it exits, including caller cancellation and protocol failure. */
59
59
  async function runCli(executable, args, options) {
60
60
  options.signal.throwIfAborted();
61
+ const started = Date.now();
62
+ const log = options.logger;
61
63
  const timed = AbortSignal.timeout(options.timeoutMs ?? 9e5);
62
64
  const signal = AbortSignal.any([options.signal, timed]);
63
65
  const maxBytes = options.maxOutputBytes ?? 16777216;
64
66
  const maxLine = options.maxLineBytes ?? 8388608;
67
+ log?.debug("cli.started", "CLI process starting.", {
68
+ argCount: args.length,
69
+ timeoutMs: options.timeoutMs ?? 9e5
70
+ });
65
71
  let child;
66
72
  try {
67
73
  child = (options.spawnChild ?? spawn)(executable.command, [...executable.args ?? [], ...args], {
@@ -76,7 +82,8 @@ async function runCli(executable, args, options) {
76
82
  "pipe"
77
83
  ]
78
84
  });
79
- } catch {
85
+ } catch (spawnErr) {
86
+ log?.error("cli.unavailable", "Unable to start CLI process.", spawnErr, { argCount: args.length });
80
87
  throw new CliProcessError("unavailable", "Unable to start the official CLI.");
81
88
  }
82
89
  let failure, total = 0, stderr = "", stdout = "", pending = Buffer.alloc(0);
@@ -194,8 +201,35 @@ async function runCli(executable, args, options) {
194
201
  stderr += stderrDecoder.end();
195
202
  if (pending.length && !failure) acceptLine(pending.toString("utf8"));
196
203
  await lineWork;
197
- if (failure) throw failure;
204
+ if (failure) {
205
+ const durationMs = Date.now() - started;
206
+ if (failure instanceof CliProcessError && failure.code === "cancelled") log?.debug("cli.cancelled", "CLI process cancelled.", {
207
+ durationMs,
208
+ outputBytes: total
209
+ });
210
+ else if (failure instanceof CliProcessError && failure.code === "timeout") log?.warn("cli.timeout", "CLI process timed out.", failure, {
211
+ durationMs,
212
+ outputBytes: total
213
+ });
214
+ else log?.error("cli.failed", "CLI process failed.", failure, {
215
+ code: failure instanceof CliProcessError ? failure.code : null,
216
+ durationMs,
217
+ outputBytes: total
218
+ });
219
+ throw failure;
220
+ }
198
221
  signal.throwIfAborted();
222
+ const durationMs = Date.now() - started;
223
+ if (exitCode === 0) log?.debug("cli.completed", "CLI process exited cleanly.", {
224
+ exitCode,
225
+ durationMs,
226
+ outputBytes: total
227
+ });
228
+ else log?.warn("cli.nonzero-exit", "CLI process exited with a non-zero code.", void 0, {
229
+ exitCode,
230
+ durationMs,
231
+ outputBytes: total
232
+ });
199
233
  return {
200
234
  stdout,
201
235
  stderr,
@@ -1 +1 @@
1
- {"version":3,"file":"cli-process.js","names":[],"sources":["../src/cli-process.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { StringDecoder } from 'node:string_decoder'\n\n/** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */\nexport function cliEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const allowed = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR', ...extra.map(key => key.toUpperCase())])\n return Object.fromEntries(Object.entries(parent).filter(([key, value]) => value !== undefined && !key.toUpperCase().startsWith('DSH_') && allowed.has(key.toUpperCase())))\n}\n\n/** Run a JavaScript CLI with process.execPath even when the host is Electron.\n * Set this explicitly: desktop hosts may already have removed it from their own env.\n * Native executables continue to use cliEnvironment without this Node-mode override.\n */\nexport function cliNodeEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const env = cliEnvironment(parent, extra)\n // Windows environment names are case-insensitive; avoid conflicting spellings.\n for (const key of Object.keys(env)) if (key.toUpperCase() === 'ELECTRON_RUN_AS_NODE') delete env[key]\n return { ...env, ELECTRON_RUN_AS_NODE: '1' }\n}\n\n/** A resolved executable, never a shell command or an argument string. */\nexport type CliExecutable = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; displayPath: string }\nexport type CliOutput = { stdout: string; stderr: string; exitCode: number }\nexport class CliProcessError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliProcessError' }\n}\nexport type CliOptions = {\n cwd: string; signal: AbortSignal; input?: string; timeoutMs?: number;\n maxOutputBytes?: number; maxLineBytes?: number;\n onLine?: (line: string) => void | Promise<void>;\n spawnChild?: typeof spawn;\n}\n\n/** Own one process tree until it exits, including caller cancellation and protocol failure. */\nexport async function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput> {\n options.signal.throwIfAborted()\n const timed = AbortSignal.timeout(options.timeoutMs ?? 900_000)\n const signal = AbortSignal.any([options.signal, timed])\n const maxBytes = options.maxOutputBytes ?? 16 * 1024 * 1024\n const maxLine = options.maxLineBytes ?? 8 * 1024 * 1024\n let child: ChildProcessWithoutNullStreams\n try {\n child = (options.spawnChild ?? spawn)(executable.command, [...(executable.args ?? []), ...args], {\n cwd: options.cwd, env: executable.env ?? cliEnvironment(), shell: false,\n windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n } catch { throw new CliProcessError('unavailable', 'Unable to start the official CLI.') }\n let failure: unknown, total = 0, stderr = '', stdout = '', pending = Buffer.alloc(0)\n const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8')\n let exited = false, exitCode = -1, stopping: Promise<void> | undefined\n let lineWork = Promise.resolve()\n const closed = new Promise<void>(resolve => {\n child.once('close', code => { exited = true; exitCode = code ?? -1; resolve() })\n })\n const stop = () => stopping ??= (async () => {\n if (exited) return\n const pid = child.pid\n if (pid && process.platform === 'win32') {\n await new Promise<void>(resolve => {\n const killer = spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { env: cliEnvironment(), windowsHide: true, shell: false, stdio: 'ignore' })\n let timer: ReturnType<typeof setTimeout>\n const done = () => { clearTimeout(timer); resolve() }\n killer.once('error', () => { child.kill(); done() })\n killer.once('close', done)\n timer = setTimeout(() => { killer.kill(); child.kill(); resolve() }, 5_000)\n })\n } else if (pid) {\n try { process.kill(-pid, 'SIGTERM') } catch { child.kill() }\n } else child.kill()\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, 2_000) })])\n if (timer) clearTimeout(timer)\n if (!exited) {\n try { pid && process.platform !== 'win32' ? process.kill(-pid, 'SIGKILL') : child.kill('SIGKILL') } catch { /* already exited */ }\n }\n await closed\n })()\n const fail = (error: unknown) => { failure ??= error; void stop() }\n const abort = () => fail(new CliProcessError(timed.aborted && !options.signal.aborted ? 'timeout' : 'cancelled', timed.aborted && !options.signal.aborted ? 'The CLI request timed out.' : 'The CLI request was cancelled.'))\n signal.addEventListener('abort', abort, { once: true })\n child.on('error', () => fail(new CliProcessError('unavailable', 'Unable to start the official CLI.')))\n child.stdin.on('error', () => { if (!exited) fail(new CliProcessError('process-io', 'The CLI input stream closed unexpectedly.')) })\n child.stdout.on('error', () => fail(new CliProcessError('process-io', 'The CLI output stream failed.')))\n child.stderr.on('error', () => fail(new CliProcessError('process-io', 'The CLI error stream failed.')))\n const count = (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) { fail(new CliProcessError('output-limit', 'The CLI output exceeded its limit.')); return false }\n return true\n }\n const acceptLine = (line: string) => {\n if (!options.onLine || !line.trim()) return\n child.stdout.pause()\n lineWork = lineWork.then(async () => { if (!failure) await options.onLine!(line) }).catch(fail).finally(() => child.stdout.resume())\n }\n child.stdout.on('data', (chunk: Buffer) => {\n if (failure || !count(chunk)) return\n if (!options.onLine) { stdout += stdoutDecoder.write(chunk); return }\n pending = Buffer.concat([pending, chunk])\n for (;;) {\n const end = pending.indexOf(10)\n if (end < 0) break\n if (end > maxLine) { fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.')); return }\n acceptLine(pending.subarray(0, end).toString('utf8'))\n pending = pending.subarray(end + 1)\n }\n if (pending.length > maxLine) fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.'))\n })\n child.stderr.on('data', (chunk: Buffer) => { if (count(chunk)) stderr = (stderr + stderrDecoder.write(chunk)).slice(-16_384) })\n try {\n if (signal.aborted) abort()\n if (!failure) child.stdin.end(options.input ?? '')\n await closed\n stdout += stdoutDecoder.end(); stderr += stderrDecoder.end()\n if (pending.length && !failure) acceptLine(pending.toString('utf8'))\n await lineWork\n if (failure) throw failure\n signal.throwIfAborted()\n return { stdout, stderr, exitCode }\n } finally {\n signal.removeEventListener('abort', abort)\n if (!exited) await stop()\n if (stopping) await stopping\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-process.js","names":[],"sources":["../src/cli-process.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { StringDecoder } from 'node:string_decoder'\nimport type { PluginLogger } from './log-shared.ts'\n\n/** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */\nexport function cliEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const allowed = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR', ...extra.map(key => key.toUpperCase())])\n return Object.fromEntries(Object.entries(parent).filter(([key, value]) => value !== undefined && !key.toUpperCase().startsWith('DSH_') && allowed.has(key.toUpperCase())))\n}\n\n/** Run a JavaScript CLI with process.execPath even when the host is Electron.\n * Set this explicitly: desktop hosts may already have removed it from their own env.\n * Native executables continue to use cliEnvironment without this Node-mode override.\n */\nexport function cliNodeEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const env = cliEnvironment(parent, extra)\n // Windows environment names are case-insensitive; avoid conflicting spellings.\n for (const key of Object.keys(env)) if (key.toUpperCase() === 'ELECTRON_RUN_AS_NODE') delete env[key]\n return { ...env, ELECTRON_RUN_AS_NODE: '1' }\n}\n\n/** A resolved executable, never a shell command or an argument string. */\nexport type CliExecutable = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; displayPath: string }\nexport type CliOutput = { stdout: string; stderr: string; exitCode: number }\nexport class CliProcessError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliProcessError' }\n}\nexport type CliOptions = {\n cwd: string; signal: AbortSignal; input?: string; timeoutMs?: number;\n maxOutputBytes?: number; maxLineBytes?: number;\n onLine?: (line: string) => void | Promise<void>;\n spawnChild?: typeof spawn;\n logger?: PluginLogger;\n}\n\n/** Own one process tree until it exits, including caller cancellation and protocol failure. */\nexport async function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput> {\n options.signal.throwIfAborted()\n const started = Date.now()\n const log = options.logger\n const timed = AbortSignal.timeout(options.timeoutMs ?? 900_000)\n const signal = AbortSignal.any([options.signal, timed])\n const maxBytes = options.maxOutputBytes ?? 16 * 1024 * 1024\n const maxLine = options.maxLineBytes ?? 8 * 1024 * 1024\n log?.debug('cli.started', 'CLI process starting.', {\n argCount: args.length,\n timeoutMs: options.timeoutMs ?? 900_000,\n })\n let child: ChildProcessWithoutNullStreams\n try {\n child = (options.spawnChild ?? spawn)(executable.command, [...(executable.args ?? []), ...args], {\n cwd: options.cwd, env: executable.env ?? cliEnvironment(), shell: false,\n windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n } catch (spawnErr) {\n log?.error('cli.unavailable', 'Unable to start CLI process.', spawnErr, { argCount: args.length })\n throw new CliProcessError('unavailable', 'Unable to start the official CLI.')\n }\n let failure: unknown, total = 0, stderr = '', stdout = '', pending = Buffer.alloc(0)\n const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8')\n let exited = false, exitCode = -1, stopping: Promise<void> | undefined\n let lineWork = Promise.resolve()\n const closed = new Promise<void>(resolve => {\n child.once('close', code => { exited = true; exitCode = code ?? -1; resolve() })\n })\n const stop = () => stopping ??= (async () => {\n if (exited) return\n const pid = child.pid\n if (pid && process.platform === 'win32') {\n await new Promise<void>(resolve => {\n const killer = spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { env: cliEnvironment(), windowsHide: true, shell: false, stdio: 'ignore' })\n let timer: ReturnType<typeof setTimeout>\n const done = () => { clearTimeout(timer); resolve() }\n killer.once('error', () => { child.kill(); done() })\n killer.once('close', done)\n timer = setTimeout(() => { killer.kill(); child.kill(); resolve() }, 5_000)\n })\n } else if (pid) {\n try { process.kill(-pid, 'SIGTERM') } catch { child.kill() }\n } else child.kill()\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, 2_000) })])\n if (timer) clearTimeout(timer)\n if (!exited) {\n try { pid && process.platform !== 'win32' ? process.kill(-pid, 'SIGKILL') : child.kill('SIGKILL') } catch { /* already exited */ }\n }\n await closed\n })()\n const fail = (error: unknown) => { failure ??= error; void stop() }\n const abort = () => fail(new CliProcessError(timed.aborted && !options.signal.aborted ? 'timeout' : 'cancelled', timed.aborted && !options.signal.aborted ? 'The CLI request timed out.' : 'The CLI request was cancelled.'))\n signal.addEventListener('abort', abort, { once: true })\n child.on('error', () => fail(new CliProcessError('unavailable', 'Unable to start the official CLI.')))\n child.stdin.on('error', () => { if (!exited) fail(new CliProcessError('process-io', 'The CLI input stream closed unexpectedly.')) })\n child.stdout.on('error', () => fail(new CliProcessError('process-io', 'The CLI output stream failed.')))\n child.stderr.on('error', () => fail(new CliProcessError('process-io', 'The CLI error stream failed.')))\n const count = (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) { fail(new CliProcessError('output-limit', 'The CLI output exceeded its limit.')); return false }\n return true\n }\n const acceptLine = (line: string) => {\n if (!options.onLine || !line.trim()) return\n child.stdout.pause()\n lineWork = lineWork.then(async () => { if (!failure) await options.onLine!(line) }).catch(fail).finally(() => child.stdout.resume())\n }\n child.stdout.on('data', (chunk: Buffer) => {\n if (failure || !count(chunk)) return\n if (!options.onLine) { stdout += stdoutDecoder.write(chunk); return }\n pending = Buffer.concat([pending, chunk])\n for (;;) {\n const end = pending.indexOf(10)\n if (end < 0) break\n if (end > maxLine) { fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.')); return }\n acceptLine(pending.subarray(0, end).toString('utf8'))\n pending = pending.subarray(end + 1)\n }\n if (pending.length > maxLine) fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.'))\n })\n child.stderr.on('data', (chunk: Buffer) => { if (count(chunk)) stderr = (stderr + stderrDecoder.write(chunk)).slice(-16_384) })\n try {\n if (signal.aborted) abort()\n if (!failure) child.stdin.end(options.input ?? '')\n await closed\n stdout += stdoutDecoder.end(); stderr += stderrDecoder.end()\n if (pending.length && !failure) acceptLine(pending.toString('utf8'))\n await lineWork\n if (failure) {\n const durationMs = Date.now() - started\n if (failure instanceof CliProcessError && failure.code === 'cancelled') {\n log?.debug('cli.cancelled', 'CLI process cancelled.', { durationMs, outputBytes: total })\n } else if (failure instanceof CliProcessError && failure.code === 'timeout') {\n log?.warn('cli.timeout', 'CLI process timed out.', failure, { durationMs, outputBytes: total })\n } else {\n log?.error('cli.failed', 'CLI process failed.', failure, {\n code: failure instanceof CliProcessError ? failure.code : null,\n durationMs,\n outputBytes: total,\n })\n }\n throw failure\n }\n signal.throwIfAborted()\n const durationMs = Date.now() - started\n if (exitCode === 0) {\n log?.debug('cli.completed', 'CLI process exited cleanly.', { exitCode, durationMs, outputBytes: total })\n } else {\n log?.warn('cli.nonzero-exit', 'CLI process exited with a non-zero code.', undefined, { exitCode, durationMs, outputBytes: total })\n }\n return { stdout, stderr, exitCode }\n } finally {\n signal.removeEventListener('abort', abort)\n if (!exited) await stop()\n if (stopping) await stopping\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { C as truncateLogText, S as sanitizePluginLogText, _ as normalizePluginLogSource, a as PluginLogContext, b as sanitizePluginLogContext, c as PluginLogInput, d as PluginLogger, f as buildPluginLogEntry, g as normalizePluginLogEvent, h as mergePluginLogContext, i as PLUGIN_LOG_MAX_MESSAGE_CHARS, l as PluginLogLevel, m as isPluginLogLevel, n as PLUGIN_LOG_LEVELS, o as PluginLogEntry, p as createChildPluginLogger, r as PLUGIN_LOG_MAX_CONTEXT_CHARS, s as PluginLogError, t as DEFAULT_PLUGIN_LOG_LEVEL, u as PluginLogScalar, v as parsePluginLogLevel, x as sanitizePluginLogError, y as pluginLogLevelEnabled } from "./log-shared2.js";
2
+ //#region src/client-logger.d.ts
3
+ declare const CLIENT_PLUGIN_LOG_GLOBAL = "__KLARKXY_PLUGIN_LOGS__";
4
+ declare const CLIENT_PLUGIN_LOG_LIMIT = 500;
5
+ interface ClientPluginLoggerOptions {
6
+ minimumLevel?: PluginLogLevel;
7
+ now?: () => Date;
8
+ bufferLimit?: number;
9
+ forwardToHostLogger?: boolean;
10
+ loadedContext?: PluginLogContext;
11
+ }
12
+ declare function readClientPluginLogs(source?: string): PluginLogEntry[];
13
+ declare function clearClientPluginLogs(): void;
14
+ declare function createClientPluginLogger(source: string, ctx?: unknown, options?: ClientPluginLoggerOptions): PluginLogger;
15
+ declare function installClientPluginLogger(ctx: unknown, source: string, options?: ClientPluginLoggerOptions): PluginLogger;
16
+ declare function clientLogStep<T>(logger: PluginLogger, step: string, context: PluginLogContext | undefined, action: () => Promise<T>, completedLevel?: PluginLogLevel): Promise<T>;
17
+ //#endregion
18
+ export { CLIENT_PLUGIN_LOG_GLOBAL, CLIENT_PLUGIN_LOG_LIMIT, ClientPluginLoggerOptions, DEFAULT_PLUGIN_LOG_LEVEL, PLUGIN_LOG_LEVELS, PLUGIN_LOG_MAX_CONTEXT_CHARS, PLUGIN_LOG_MAX_MESSAGE_CHARS, PluginLogContext, PluginLogEntry, PluginLogError, PluginLogInput, PluginLogLevel, PluginLogScalar, PluginLogger, buildPluginLogEntry, clearClientPluginLogs, clientLogStep, createChildPluginLogger, createClientPluginLogger, installClientPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, readClientPluginLogs, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText };
19
+ //# sourceMappingURL=client-logger.d.ts.map
@@ -0,0 +1,153 @@
1
+ import { DEFAULT_PLUGIN_LOG_LEVEL, PLUGIN_LOG_LEVELS, PLUGIN_LOG_MAX_CONTEXT_CHARS, PLUGIN_LOG_MAX_MESSAGE_CHARS, buildPluginLogEntry, createChildPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText } from "./log-shared.js";
2
+ //#region src/client-logger.ts
3
+ const CLIENT_PLUGIN_LOG_GLOBAL = "__KLARKXY_PLUGIN_LOGS__";
4
+ const CLIENT_PLUGIN_LOG_LIMIT = 500;
5
+ function readStorageFlag(key) {
6
+ try {
7
+ const value = (globalThis.window?.localStorage)?.getItem?.(key);
8
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
9
+ } catch {
10
+ return;
11
+ }
12
+ }
13
+ function sharedClientBuffer(limit) {
14
+ const carrier = globalThis;
15
+ if (!Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"])) try {
16
+ carrier[CLIENT_PLUGIN_LOG_GLOBAL] = [];
17
+ } catch {
18
+ return [];
19
+ }
20
+ const list = carrier[CLIENT_PLUGIN_LOG_GLOBAL];
21
+ const overflow = list.length - limit;
22
+ if (overflow > 0) list.splice(0, overflow);
23
+ return list;
24
+ }
25
+ function readClientPluginLogs(source) {
26
+ const carrier = globalThis;
27
+ const list = Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"]) ? carrier[CLIENT_PLUGIN_LOG_GLOBAL] : [];
28
+ if (!source) return list.slice();
29
+ const normalized = normalizePluginLogSource(source);
30
+ return list.filter((entry) => entry.source === normalized);
31
+ }
32
+ function clearClientPluginLogs() {
33
+ const carrier = globalThis;
34
+ if (Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"])) carrier[CLIENT_PLUGIN_LOG_GLOBAL].length = 0;
35
+ }
36
+ function createClientPluginLogger(source, ctx, options = {}) {
37
+ const normalized = normalizePluginLogSource(source);
38
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
39
+ const limit = Math.max(10, options.bufferLimit ?? 500);
40
+ const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
41
+ const resolveMinimum = () => {
42
+ if (options.minimumLevel) return options.minimumLevel;
43
+ return parsePluginLogLevel(readStorageFlag("dsh-plugin-log-level"), normalized, DEFAULT_PLUGIN_LOG_LEVEL);
44
+ };
45
+ const logger = {
46
+ source: normalized,
47
+ file: null,
48
+ lastWriteError: null,
49
+ debug: (event, message, context) => logger.write({
50
+ level: "debug",
51
+ event,
52
+ message,
53
+ ...context ? { context } : {}
54
+ }),
55
+ info: (event, message, context) => logger.write({
56
+ level: "info",
57
+ event,
58
+ message,
59
+ ...context ? { context } : {}
60
+ }),
61
+ warn: (event, message, error, context) => logger.write({
62
+ level: "warn",
63
+ event,
64
+ message,
65
+ ...error !== void 0 ? { error } : {},
66
+ ...context ? { context } : {}
67
+ }),
68
+ error: (event, message, error, context) => logger.write({
69
+ level: "error",
70
+ event,
71
+ message,
72
+ ...error !== void 0 ? { error } : {},
73
+ ...context ? { context } : {}
74
+ }),
75
+ fault: (event, message, error, context) => logger.write({
76
+ level: "fault",
77
+ event,
78
+ message,
79
+ ...error !== void 0 ? { error } : {},
80
+ ...context ? { context } : {}
81
+ }),
82
+ child: (context) => createChildPluginLogger(logger, context),
83
+ write(input) {
84
+ try {
85
+ if (!pluginLogLevelEnabled(input.level, resolveMinimum())) return false;
86
+ const entry = buildPluginLogEntry(input, {
87
+ at: now(),
88
+ source: normalized,
89
+ surface: "client"
90
+ });
91
+ const buffer = sharedClientBuffer(limit);
92
+ buffer.push(entry);
93
+ const overflow = buffer.length - limit;
94
+ if (overflow > 0) buffer.splice(0, overflow);
95
+ if (options.forwardToHostLogger !== false && browserCtx?.logger) {
96
+ const summary = sanitizePluginLogText(`${normalized} [${entry.event}]: ${entry.message}`, 500);
97
+ if (entry.level === "warn" && typeof browserCtx.logger.warn === "function") browserCtx.logger.warn(summary);
98
+ else if ((entry.level === "error" || entry.level === "fault") && typeof browserCtx.logger.error === "function") browserCtx.logger.error(summary);
99
+ }
100
+ if (readStorageFlag("dsh-plugin-log-console") === "1") {
101
+ const con = globalThis.console;
102
+ (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);
103
+ }
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ };
110
+ return logger;
111
+ }
112
+ function installClientPluginLogger(ctx, source, options = {}) {
113
+ const logger = createClientPluginLogger(source, ctx, options);
114
+ logger.info("client.loaded", "Client plugin initialized.", options.loadedContext);
115
+ const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
116
+ if (typeof browserCtx?.effect === "function") try {
117
+ browserCtx.effect(() => () => {
118
+ logger.debug("client.disposed", "Client plugin disposed.");
119
+ }, `${logger.source}.client-logger`);
120
+ } catch {}
121
+ return logger;
122
+ }
123
+ async function clientLogStep(logger, step, context, action, completedLevel = "debug") {
124
+ const started = Date.now();
125
+ const base = {
126
+ ...context,
127
+ step
128
+ };
129
+ logger.debug("step.started", "Client step started.", base);
130
+ try {
131
+ const result = await action();
132
+ logger.write({
133
+ level: completedLevel,
134
+ event: "step.completed",
135
+ message: "Client step completed.",
136
+ context: {
137
+ ...base,
138
+ durationMs: Date.now() - started
139
+ }
140
+ });
141
+ return result;
142
+ } catch (error) {
143
+ logger.error("step.failed", "Client step failed.", error, {
144
+ ...base,
145
+ durationMs: Date.now() - started
146
+ });
147
+ throw error;
148
+ }
149
+ }
150
+ //#endregion
151
+ export { CLIENT_PLUGIN_LOG_GLOBAL, CLIENT_PLUGIN_LOG_LIMIT, DEFAULT_PLUGIN_LOG_LEVEL, PLUGIN_LOG_LEVELS, PLUGIN_LOG_MAX_CONTEXT_CHARS, PLUGIN_LOG_MAX_MESSAGE_CHARS, buildPluginLogEntry, clearClientPluginLogs, clientLogStep, createChildPluginLogger, createClientPluginLogger, installClientPluginLogger, isPluginLogLevel, mergePluginLogContext, normalizePluginLogEvent, normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled, readClientPluginLogs, sanitizePluginLogContext, sanitizePluginLogError, sanitizePluginLogText, truncateLogText };
152
+
153
+ //# sourceMappingURL=client-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-logger.js","names":[],"sources":["../src/client-logger.ts"],"sourcesContent":["import {\n DEFAULT_PLUGIN_LOG_LEVEL, buildPluginLogEntry, createChildPluginLogger,\n normalizePluginLogSource, parsePluginLogLevel, pluginLogLevelEnabled,\n sanitizePluginLogText,\n type PluginLogContext, type PluginLogEntry, type PluginLogInput,\n type PluginLogLevel, type PluginLogger,\n} from './log-shared.ts'\n\nexport * from './log-shared.ts'\n\nexport const CLIENT_PLUGIN_LOG_GLOBAL = '__KLARKXY_PLUGIN_LOGS__'\nexport const CLIENT_PLUGIN_LOG_LIMIT = 500\n\nexport interface ClientPluginLoggerOptions {\n minimumLevel?: PluginLogLevel\n now?: () => Date\n bufferLimit?: number\n forwardToHostLogger?: boolean\n loadedContext?: PluginLogContext\n}\n\ntype BrowserContextLike = {\n effect?: (callback: () => () => void, label?: string) => unknown\n logger?: {\n warn?: (...args: unknown[]) => void\n error?: (...args: unknown[]) => void\n }\n}\n\ntype GlobalLogCarrier = {\n [CLIENT_PLUGIN_LOG_GLOBAL]?: PluginLogEntry[]\n window?: { localStorage?: { getItem?: (key: string) => string | null } }\n console?: {\n debug?: (...args: unknown[]) => void\n info?: (...args: unknown[]) => void\n warn?: (...args: unknown[]) => void\n error?: (...args: unknown[]) => void\n }\n}\n\nfunction readStorageFlag(key: string): string | undefined {\n try {\n const win = (globalThis as unknown as GlobalLogCarrier).window\n const storage = win?.localStorage\n const value = storage?.getItem?.(key)\n return typeof value === 'string' && value.trim() ? value.trim() : undefined\n } catch {\n return undefined\n }\n}\n\nfunction sharedClientBuffer(limit: number): PluginLogEntry[] {\n const carrier = globalThis as unknown as GlobalLogCarrier\n if (!Array.isArray(carrier[CLIENT_PLUGIN_LOG_GLOBAL])) {\n try {\n carrier[CLIENT_PLUGIN_LOG_GLOBAL] = []\n } catch {\n return []\n }\n }\n const list = carrier[CLIENT_PLUGIN_LOG_GLOBAL]!\n const overflow = list.length - limit\n if (overflow > 0) list.splice(0, overflow)\n return list\n}\n\nexport function readClientPluginLogs(source?: string): PluginLogEntry[] {\n const carrier = globalThis as unknown as GlobalLogCarrier\n const list = Array.isArray(carrier[CLIENT_PLUGIN_LOG_GLOBAL]) ? carrier[CLIENT_PLUGIN_LOG_GLOBAL]! : []\n if (!source) return list.slice()\n const normalized = normalizePluginLogSource(source)\n return list.filter(entry => entry.source === normalized)\n}\n\nexport function clearClientPluginLogs(): void {\n const carrier = globalThis as unknown as GlobalLogCarrier\n if (Array.isArray(carrier[CLIENT_PLUGIN_LOG_GLOBAL])) {\n carrier[CLIENT_PLUGIN_LOG_GLOBAL]!.length = 0\n }\n}\n\nexport function createClientPluginLogger(\n source: string,\n ctx?: unknown,\n options: ClientPluginLoggerOptions = {},\n): PluginLogger {\n const normalized = normalizePluginLogSource(source)\n const now = options.now ?? (() => new Date())\n const limit = Math.max(10, options.bufferLimit ?? CLIENT_PLUGIN_LOG_LIMIT)\n const browserCtx = ctx && typeof ctx === 'object' ? ctx as BrowserContextLike : undefined\n\n const resolveMinimum = (): PluginLogLevel => {\n if (options.minimumLevel) return options.minimumLevel\n return parsePluginLogLevel(readStorageFlag('dsh-plugin-log-level'), normalized, DEFAULT_PLUGIN_LOG_LEVEL)\n }\n\n const logger: PluginLogger = {\n source: normalized,\n file: null,\n lastWriteError: null,\n debug: (event, message, context) => logger.write({ level: 'debug', event, message, ...(context ? { context } : {}) }),\n info: (event, message, context) => logger.write({ level: 'info', event, message, ...(context ? { context } : {}) }),\n warn: (event, message, error, context) => logger.write({\n level: 'warn', event, message,\n ...(error !== undefined ? { error } : {}),\n ...(context ? { context } : {}),\n }),\n error: (event, message, error, context) => logger.write({\n level: 'error', event, message,\n ...(error !== undefined ? { error } : {}),\n ...(context ? { context } : {}),\n }),\n fault: (event, message, error, context) => logger.write({\n level: 'fault', event, message,\n ...(error !== undefined ? { error } : {}),\n ...(context ? { context } : {}),\n }),\n child: context => createChildPluginLogger(logger, context),\n write(input: PluginLogInput): boolean {\n try {\n if (!pluginLogLevelEnabled(input.level, resolveMinimum())) return false\n const entry = buildPluginLogEntry(input, {\n at: now(),\n source: normalized,\n surface: 'client',\n })\n const buffer = sharedClientBuffer(limit)\n buffer.push(entry)\n const overflow = buffer.length - limit\n if (overflow > 0) buffer.splice(0, overflow)\n\n if (options.forwardToHostLogger !== false && browserCtx?.logger) {\n const summary = sanitizePluginLogText(`${normalized} [${entry.event}]: ${entry.message}`, 500)\n if (entry.level === 'warn' && typeof browserCtx.logger.warn === 'function') {\n browserCtx.logger.warn(summary)\n } else if ((entry.level === 'error' || entry.level === 'fault') && typeof browserCtx.logger.error === 'function') {\n browserCtx.logger.error(summary)\n }\n }\n if (readStorageFlag('dsh-plugin-log-console') === '1') {\n const con = (globalThis as unknown as GlobalLogCarrier).console\n const fn = entry.level === 'debug' ? con?.debug\n : entry.level === 'info' ? con?.info\n : entry.level === 'warn' ? con?.warn\n : con?.error\n fn?.call(con, `[${normalized}] ${entry.event}: ${entry.message}`, entry)\n }\n return true\n } catch {\n return false\n }\n },\n }\n return logger\n}\n\nexport function installClientPluginLogger(\n ctx: unknown,\n source: string,\n options: ClientPluginLoggerOptions = {},\n): PluginLogger {\n const logger = createClientPluginLogger(source, ctx, options)\n logger.info('client.loaded', 'Client plugin initialized.', options.loadedContext)\n const browserCtx = ctx && typeof ctx === 'object' ? ctx as BrowserContextLike : undefined\n if (typeof browserCtx?.effect === 'function') {\n try {\n browserCtx.effect(() => () => {\n logger.debug('client.disposed', 'Client plugin disposed.')\n }, `${logger.source}.client-logger`)\n } catch {\n // Non-Cordis mock context.\n }\n }\n return logger\n}\n\nexport async function clientLogStep<T>(\n logger: PluginLogger,\n step: string,\n context: PluginLogContext | undefined,\n action: () => Promise<T>,\n completedLevel: PluginLogLevel = 'debug',\n): Promise<T> {\n const started = Date.now()\n const base = { ...context, step }\n logger.debug('step.started', 'Client step started.', base)\n try {\n const result = await action()\n logger.write({\n level: completedLevel,\n event: 'step.completed',\n message: 'Client step completed.',\n context: { ...base, durationMs: Date.now() - started },\n })\n return result\n } catch (error) {\n logger.error('step.failed', 'Client step failed.', error, {\n ...base,\n durationMs: Date.now() - started,\n })\n throw error\n }\n}\n"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
+ import { d as PluginLogger } from "./log-shared2.js";
1
2
  import { ReactNode } from "react";
2
3
  import { Context } from "@deepseek-ai/cordis";
3
4
  //#region src/client-utils.d.ts
4
5
  /** Fixed pause for failing background reads; manual work is never replayed. */
5
- declare function createPluginReadGate(now?: () => number): {
6
+ declare function createPluginReadGate(now?: () => number, logger?: PluginLogger): {
6
7
  canRead: () => boolean;
7
8
  reset: () => void;
8
9
  run<T>(read: () => Promise<T>): Promise<T>;