dsh-theone 0.3.9 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +77 -53
  2. package/README.zh.md +82 -58
  3. package/cordis.patch.yml +0 -0
  4. package/dist/catalog-types.d.ts +0 -0
  5. package/dist/catalog-types.js +0 -0
  6. package/dist/client-locales.d.ts +20 -0
  7. package/dist/client-locales.js +40 -0
  8. package/dist/client-navigation.d.ts +0 -0
  9. package/dist/client-navigation.js +0 -0
  10. package/dist/client.d.ts +0 -0
  11. package/dist/client.js +375 -11
  12. package/dist/gateway-compaction.d.ts +0 -0
  13. package/dist/gateway-compaction.js +0 -0
  14. package/dist/history-catalog.d.ts +0 -0
  15. package/dist/history-catalog.js +0 -0
  16. package/dist/index.d.ts +17 -3
  17. package/dist/index.js +107 -23
  18. package/dist/linkage.d.ts +0 -0
  19. package/dist/linkage.js +0 -0
  20. package/dist/llm-router.d.ts +0 -0
  21. package/dist/llm-router.js +0 -0
  22. package/dist/model-json.d.ts +0 -0
  23. package/dist/model-json.js +0 -0
  24. package/dist/router.d.ts +0 -0
  25. package/dist/router.js +0 -0
  26. package/dist/routing-policy.d.ts +0 -0
  27. package/dist/routing-policy.js +0 -0
  28. package/dist/run.d.ts +0 -0
  29. package/dist/run.js +0 -0
  30. package/dist/settings-types.d.ts +0 -0
  31. package/dist/settings-types.js +0 -0
  32. package/dist/settings.d.ts +0 -0
  33. package/dist/settings.js +0 -0
  34. package/dist/store.d.ts +0 -0
  35. package/dist/store.js +0 -0
  36. package/dist/types.d.ts +0 -0
  37. package/dist/types.js +0 -0
  38. package/dist/update.d.ts +55 -0
  39. package/dist/update.js +121 -0
  40. package/docs/plugin-v0.1.md +0 -0
  41. package/docs/plugin-v0.1.zh.md +0 -0
  42. package/docs/plugin-v0.2.md +0 -0
  43. package/docs/plugin-v0.2.zh.md +0 -0
  44. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19,10 +19,26 @@ import { EDITABLE_SETTINGS_KEYS, RESTART_SETTINGS_KEYS } from "./settings-types.
19
19
  import { validateSettings } from "./settings.js";
20
20
  import { RESTART_CODE, WorkerRun } from "./run.js";
21
21
  import { buildBriefing, LINK_SIGNAL, relatedTopics } from "./linkage.js";
22
+ import { PACKAGE_NAME, Updater } from "./update.js";
22
23
  function redactDescriptor(text) {
23
24
  return text.replace(/\bsk-[a-zA-Z0-9_-]{16,}|\bBearer\s+\S+/gi, '[REDACTED]')
24
25
  .replace(/((?:api[_ -]?key|password|密码|密钥)\s*[:=:]\s*)\S+/gi, '$1[REDACTED]');
25
26
  }
27
+ /**
28
+ * Main chat's model entries besides plain "TheOne": "TheOne · <model>" still answers through TheOne,
29
+ * with that model doing the routing and the background work.
30
+ */
31
+ const VIA = 'via:';
32
+ export function viaModel(selection) { return `${VIA}${selection.provider}/${selection.model}`; }
33
+ export function parseVia(id) {
34
+ if (!id?.startsWith(VIA))
35
+ return undefined;
36
+ const rest = id.slice(VIA.length);
37
+ const slash = rest.indexOf('/');
38
+ if (slash <= 0 || slash === rest.length - 1 || rest.slice(0, slash) === 'theone')
39
+ return undefined;
40
+ return { provider: rest.slice(0, slash), model: rest.slice(slash + 1) };
41
+ }
26
42
  /** Gateway provider delegates each accepted input to its context's DSH worker. */
27
43
  class GatewayAdapter extends LlmAdapter {
28
44
  service;
@@ -31,15 +47,11 @@ class GatewayAdapter extends LlmAdapter {
31
47
  this.service = service;
32
48
  }
33
49
  providerInfo(provider) { return { id: provider, name: 'TheOne' }; }
34
- async listModels(provider) {
35
- // The entry accepts whatever the backing model accepts (e.g. images).
36
- const info = await this.service.gatewayModelInfo(provider).catch(() => undefined);
37
- return [{ provider, id: 'gateway', name: 'TheOne', inputModalities: [...(info?.inputModalities ?? ['text'])] }];
38
- }
50
+ listModels(provider) { return this.service.gatewayModels(provider); }
39
51
  resolveModel(provider, model, signal) {
40
- if (model !== 'gateway')
41
- throw new Error('TheOne only exposes the gateway model');
42
- return this.service.gatewayModelInfo(provider, signal);
52
+ if (model !== 'gateway' && !parseVia(model))
53
+ throw new Error('TheOne only exposes its gateway models');
54
+ return this.service.gatewayModelInfo(provider, signal, model);
43
55
  }
44
56
  providerRetryPolicy() { return resolveRetryPolicy({ mode: 'normal', maxRetries: 0 }, 'theone.retry'); }
45
57
  stream(options) { return this.service.answer(options); }
@@ -85,6 +97,28 @@ function reasonLabel(reason) {
85
97
  function settingsConfig(values) {
86
98
  return { ...values, workerProvider: values.workerProvider ?? undefined, workerModel: values.workerModel ?? undefined, contextsPath: values.contextsPath ?? undefined };
87
99
  }
100
+ /**
101
+ * Switch an installed bundle off and on so DSH loads its new version. This disposes the running
102
+ * TheOne, so it runs after the current request has answered and outlives this instance.
103
+ */
104
+ function reloadBundle(manager, bundle) {
105
+ setTimeout(() => {
106
+ void (async () => {
107
+ const off = await manager.setBundleEnabled(bundle, false);
108
+ if (off.application === 'failed' || off.application === 'cancelled')
109
+ return;
110
+ await manager.setBundleEnabled(bundle, true);
111
+ })().catch(error => console.warn('TheOne could not reload itself after updating; restart DSH to apply the update.', error));
112
+ }, 300);
113
+ }
114
+ function readOwnVersion() {
115
+ try {
116
+ return String(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version ?? '0.0.0');
117
+ }
118
+ catch {
119
+ return '0.0.0';
120
+ }
121
+ }
88
122
  function readDescriptors(path) {
89
123
  const value = JSON.parse(readFileSync(path, 'utf8'));
90
124
  if (!Array.isArray(value))
@@ -132,6 +166,9 @@ export default class TheOne extends Service {
132
166
  closing = new Map();
133
167
  /** A model the user picked in main chat's own model selector; it answers through the Workers. */
134
168
  pickedModel;
169
+ adapter;
170
+ /** Update checks and one-click install through DSH's plugin manager. */
171
+ updater;
135
172
  /** Sessions whose current step answers through TheOne; only these refuse to run tools themselves. */
136
173
  throughTheOne = new WeakMap();
137
174
  constructor(ctx, config) {
@@ -164,6 +201,18 @@ export default class TheOne extends Service {
164
201
  }
165
202
  descriptors ??= config.contextsPath ? readDescriptors(config.contextsPath) : [];
166
203
  this.gatewayDirectory = resolve(dirname(databasePath), 'gateway');
204
+ this.updater = new Updater(readOwnVersion(), () => {
205
+ // The profile's package.json records how this plugin was installed (GitHub, npm or a local path).
206
+ const dir = ctx.profileContext?.dir;
207
+ if (!dir)
208
+ return undefined;
209
+ try {
210
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies?.[PACKAGE_NAME];
211
+ }
212
+ catch {
213
+ return undefined;
214
+ }
215
+ });
167
216
  ctx.effect(() => async () => {
168
217
  try {
169
218
  await Promise.all([...this.workers.values()].map(handle => handle.dispose()));
@@ -183,7 +232,7 @@ export default class TheOne extends Service {
183
232
  this.catalog.start();
184
233
  }
185
234
  this.registerCatalogChannel();
186
- ctx.llm.registerAdapter(['theone'], new GatewayAdapter(this));
235
+ this.adapter = ctx.llm.registerAdapter(['theone'], new GatewayAdapter(this));
187
236
  ctx.on('session/event', (session, event) => {
188
237
  if (event.type === 'turn/end') {
189
238
  if (session.id === this.reservedGateway)
@@ -264,8 +313,8 @@ export default class TheOne extends Service {
264
313
  return config;
265
314
  }
266
315
  this.throughTheOne.set(payload.agent, true);
267
- // The resolved selection is authoritative; TheOne itself means "follow DSH's selected model".
268
- this.pickedModel = config.provider === 'theone' ? undefined : { provider: config.provider, model: config.model };
316
+ // The resolved selection is authoritative; plain TheOne means "follow DSH's selected model".
317
+ this.pickedModel = config.provider === 'theone' ? parseVia(config.model) : { provider: config.provider, model: config.model };
269
318
  return config.provider === 'theone' ? config : { ...config, provider: 'theone', model: 'gateway' };
270
319
  }, { prepend: true });
271
320
  ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
@@ -278,7 +327,7 @@ export default class TheOne extends Service {
278
327
  // Know main chat's model choice before routing, so this very message is classified with it.
279
328
  const pickedNow = selected?.signal === signal ? selected.model : undefined;
280
329
  if (provider === 'theone')
281
- this.pickedModel = undefined;
330
+ this.pickedModel = parseVia(pickedNow ?? agent.options.model);
282
331
  else if (provider && pickedNow)
283
332
  this.pickedModel = { provider, model: pickedNow };
284
333
  const users = decision.messages.filter(message => message.source.kind === 'user');
@@ -566,6 +615,18 @@ export default class TheOne extends Service {
566
615
  return Response.json({ error: code }, { status: code === 'GATEWAY_BUSY' ? 409 : 400 });
567
616
  }
568
617
  } }));
618
+ // Is a newer TheOne out, and install it with DSH's plugin manager (it loads after a restart).
619
+ child.effect(() => connection.fetch.register({ path: '/api/theone/update', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: async (request) => {
620
+ if (request.method === 'POST' && (this.active || this.reservedGateway))
621
+ return Response.json({ ...await this.updater.status(), error: 'GATEWAY_BUSY' }, { status: 409 });
622
+ const manager = this.ctx.get('pluginManager');
623
+ // With DSH's hot reload, switching the bundle off and on loads the new version without a DSH restart.
624
+ const live = !!this.ctx.get('hmr') && typeof manager?.setBundleEnabled === 'function';
625
+ const status = request.method === 'POST'
626
+ ? await this.updater.install(manager, live ? bundle => reloadBundle(manager, bundle) : undefined)
627
+ : await this.updater.status(new URL(request.url).searchParams.has('force'));
628
+ return Response.json(status, { headers: { 'cache-control': 'no-store' } });
629
+ } }));
569
630
  child.effect(() => connection.fetch.register({ path: '/api/theone/sessions', methods: ['GET'], requestBody: 'buffered', fetch: async () => Response.json({ sessions: await this.attachableSessions() }, { headers: { 'cache-control': 'no-store' } }) }));
570
631
  // The user's corrections to topic linking always take precedence over what was learned.
571
632
  child.effect(() => connection.fetch.register({ path: '/api/theone/links', methods: ['POST'], requestBody: 'buffered', fetch: async (request) => {
@@ -701,7 +762,7 @@ export default class TheOne extends Service {
701
762
  const defaultSelection = this.ctx.agentDefaultModel.currentSelection();
702
763
  const selection = c.workerProvider && c.workerModel
703
764
  ? { provider: c.workerProvider, model: c.workerModel }
704
- : defaultSelection.provider !== 'theone' ? defaultSelection : this.store.rememberedModel(c.gatewayKey);
765
+ : defaultSelection.provider !== 'theone' ? defaultSelection : parseVia(defaultSelection.model) ?? this.store.rememberedModel(c.gatewayKey);
705
766
  let model = selection ? { provider: selection.provider, model: selection.model } : null;
706
767
  let modelUnavailable = !model;
707
768
  if (selection) {
@@ -729,10 +790,7 @@ export default class TheOne extends Service {
729
790
  savedValues = validateSettings(saved.values, { contextsPath: c.contextsPath ?? null });
730
791
  }
731
792
  catch { /* Keep the current usable form. */ }
732
- // A provider that cannot list its models in time offers none here.
733
- const listed = await Promise.all(this.ctx.llm.listProviders().filter(provider => provider.id !== 'theone').map(provider => Promise.race([this.ctx.llm.listModels(provider.id), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000).unref())])
734
- .then(models => models.map(model => ({ provider: provider.id, id: model.id, name: model.name })), () => [])));
735
- const models = listed.flat();
793
+ const models = (await this.offeredModels()).map(model => ({ provider: model.provider, id: model.id, name: model.name }));
736
794
  return { values, model, models, modelUnavailable, savedValues, revision: saved?.revision ?? 0,
737
795
  restartRequired: RESTART_SETTINGS_KEYS.some(key => savedValues[key] !== activeEditable[key]) };
738
796
  }
@@ -744,13 +802,21 @@ export default class TheOne extends Service {
744
802
  const { historyCatalog: _catalog, catalogIntervalMs: _interval, ...live } = settingsConfig(values);
745
803
  if (live.routerMode !== this.config.routerMode)
746
804
  this.router = this.routerFor(live.routerMode);
805
+ const modelChanged = live.workerProvider !== this.config.workerProvider || live.workerModel !== this.config.workerModel;
747
806
  this.config = { ...this.config, ...live };
807
+ // Main chat's reasoning levels and image input are the background model's; DSH re-reads them on this signal.
808
+ if (modelChanged)
809
+ try {
810
+ this.adapter?.replace(['theone']);
811
+ }
812
+ catch { /* Released during shutdown. */ }
748
813
  this.store.seed(descriptors);
749
814
  }
750
815
  /** Capture before Web saves the gateway itself as DSH's new default. */
751
816
  captureDefaultModel() {
752
- const selection = this.ctx.agentDefaultModel.currentSelection();
753
- if (selection.provider && selection.model && selection.provider !== 'theone')
817
+ const current = this.ctx.agentDefaultModel.currentSelection();
818
+ const selection = current.provider === 'theone' ? parseVia(current.model) : current;
819
+ if (selection?.provider && selection.model)
754
820
  this.store.rememberModel(this.config.gatewayKey, selection);
755
821
  }
756
822
  backingModel() {
@@ -764,12 +830,28 @@ export default class TheOne extends Service {
764
830
  throw new RouterFailure('ROUTER_MODEL_MISSING');
765
831
  return selection;
766
832
  }
767
- /** The entry has exactly the configured backing model's capacity, including DSH overrides. */
768
- async gatewayModelInfo(provider = 'theone', signal) {
769
- const info = { provider, id: 'gateway', name: 'TheOne Gateway', inputModalities: ['text'] };
833
+ /** Every model DSH offers besides TheOne; a provider that cannot list its models in time offers none. */
834
+ async offeredModels() {
835
+ const listed = await Promise.all(this.ctx.llm.listProviders().filter(provider => provider.id !== 'theone').map(provider => Promise.race([this.ctx.llm.listModels(provider.id), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000).unref())])
836
+ .catch(() => [])));
837
+ return listed.flat();
838
+ }
839
+ /**
840
+ * Main chat's model menu has one TheOne entry; the background model is chosen with the button
841
+ * beside it. Selections of the older "TheOne · <model>" entries still resolve.
842
+ */
843
+ async gatewayModels(provider) {
844
+ // The entry accepts whatever the backing model accepts (e.g. images).
845
+ const info = await this.gatewayModelInfo(provider).catch(() => undefined);
846
+ return [{ provider, id: 'gateway', name: 'TheOne', inputModalities: [...(info?.inputModalities ?? ['text'])] }];
847
+ }
848
+ /** The entry has exactly the backing model's capacity, including DSH overrides. */
849
+ async gatewayModelInfo(provider = 'theone', signal, model = 'gateway') {
850
+ const chosen = parseVia(model);
851
+ const info = { provider, id: model, name: 'TheOne Gateway', inputModalities: ['text'] };
770
852
  let selection;
771
853
  try {
772
- selection = this.backingModel();
854
+ selection = chosen ?? this.backingModel();
773
855
  }
774
856
  catch (error) {
775
857
  if (error instanceof RouterFailure && error.code === 'ROUTER_MODEL_MISSING')
@@ -777,6 +859,8 @@ export default class TheOne extends Service {
777
859
  throw error;
778
860
  }
779
861
  const backing = await this.ctx.llm.resolveModelInfo(selection.provider, selection.model, signal);
862
+ if (chosen)
863
+ info.name = `TheOne · ${backing.name}`;
780
864
  // Image input and the thinking-effort choices are the backing model's, so main chat offers the same controls.
781
865
  return { ...info, ...(backing.inputModalities ? { inputModalities: [...backing.inputModalities] } : {}),
782
866
  ...(backing.reasoning ? { reasoning: backing.reasoning } : {}),
package/dist/linkage.d.ts CHANGED
File without changes
package/dist/linkage.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/router.d.ts CHANGED
File without changes
package/dist/router.js CHANGED
File without changes
File without changes
File without changes
package/dist/run.d.ts CHANGED
File without changes
package/dist/run.js CHANGED
File without changes
File without changes
File without changes
File without changes
package/dist/settings.js CHANGED
File without changes
package/dist/store.d.ts CHANGED
File without changes
package/dist/store.js CHANGED
File without changes
package/dist/types.d.ts CHANGED
File without changes
package/dist/types.js CHANGED
File without changes
@@ -0,0 +1,55 @@
1
+ export declare const REPOSITORY = "YunongDai2005/dsh-theone";
2
+ export declare const PACKAGE_NAME = "dsh-theone";
3
+ /** Where this copy was installed from, which decides where its updates come from. */
4
+ export type UpdateSource = 'github' | 'npm' | 'other';
5
+ export interface UpdateStatus {
6
+ current: string;
7
+ latest?: string;
8
+ /** A newer version exists. */
9
+ available: boolean;
10
+ /** It can be installed from here (otherwise reinstall by hand). */
11
+ installable: boolean;
12
+ source: UpdateSource;
13
+ /** installing → reloading (TheOne restarts itself; DSH keeps running), restart (applies after DSH restarts) or failed. */
14
+ state?: 'installing' | 'reloading' | 'restart' | 'failed';
15
+ error?: string;
16
+ }
17
+ /** The DSH plugin manager's install call, as TheOne uses it. */
18
+ export interface PluginInstaller {
19
+ installBundle(spec: string, options?: {
20
+ enabled?: boolean;
21
+ }): Promise<{
22
+ application: string;
23
+ bundle?: string;
24
+ error?: {
25
+ code?: string;
26
+ message?: string;
27
+ } | unknown;
28
+ }>;
29
+ }
30
+ /** Compare dotted versions numerically; a pre-release sorts before its release. */
31
+ export declare function compareVersions(a: string, b: string): number;
32
+ /** Read the dependency spec DSH saved for this plugin: GitHub, npm, or something else (a local path, a tarball). */
33
+ export declare function installSource(spec: string | undefined): UpdateSource;
34
+ /** Checks for a newer TheOne and installs it through DSH's own plugin manager. */
35
+ export declare class Updater {
36
+ readonly current: string;
37
+ private readonly spec;
38
+ private readonly fetcher;
39
+ private readonly now;
40
+ private checked?;
41
+ private state?;
42
+ private error?;
43
+ private pending?;
44
+ constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number);
45
+ get source(): UpdateSource;
46
+ /** The latest known status; checks again at most every six hours (or now, when forced). */
47
+ status(force?: boolean): Promise<UpdateStatus>;
48
+ /**
49
+ * Install the newer version. With `reload`, TheOne then restarts itself so the new version runs
50
+ * without restarting DSH; otherwise it is loaded the next time DSH starts.
51
+ */
52
+ install(installer: PluginInstaller | undefined, reload?: (bundle: string) => void): Promise<UpdateStatus>;
53
+ private snapshot;
54
+ private check;
55
+ }
package/dist/update.js ADDED
@@ -0,0 +1,121 @@
1
+ export const REPOSITORY = 'YunongDai2005/dsh-theone';
2
+ export const PACKAGE_NAME = 'dsh-theone';
3
+ /** Compare dotted versions numerically; a pre-release sorts before its release. */
4
+ export function compareVersions(a, b) {
5
+ const parse = (value) => {
6
+ const [core, pre] = value.trim().replace(/^v/, '').split('-', 2);
7
+ return { parts: core.split('.').map(part => Number.parseInt(part, 10) || 0), pre };
8
+ };
9
+ const x = parse(a), y = parse(b);
10
+ for (let index = 0; index < Math.max(x.parts.length, y.parts.length); index++) {
11
+ const difference = (x.parts[index] ?? 0) - (y.parts[index] ?? 0);
12
+ if (difference)
13
+ return Math.sign(difference);
14
+ }
15
+ if (x.pre === y.pre)
16
+ return 0;
17
+ return x.pre === undefined ? 1 : y.pre === undefined ? -1 : x.pre < y.pre ? -1 : 1;
18
+ }
19
+ /** Read the dependency spec DSH saved for this plugin: GitHub, npm, or something else (a local path, a tarball). */
20
+ export function installSource(spec) {
21
+ if (!spec)
22
+ return 'other';
23
+ if (spec.toLowerCase().includes(REPOSITORY.toLowerCase()))
24
+ return 'github';
25
+ if (/^(?:npm:dsh-theone@)?[\^~>=<\s]*\d|^latest$|^\*$/.test(spec))
26
+ return 'npm';
27
+ return 'other';
28
+ }
29
+ const HOUR = 3600000;
30
+ /** Checks for a newer TheOne and installs it through DSH's own plugin manager. */
31
+ export class Updater {
32
+ current;
33
+ spec;
34
+ fetcher;
35
+ now;
36
+ checked;
37
+ state;
38
+ error;
39
+ pending;
40
+ constructor(current, spec, fetcher = fetch, now = Date.now) {
41
+ this.current = current;
42
+ this.spec = spec;
43
+ this.fetcher = fetcher;
44
+ this.now = now;
45
+ }
46
+ get source() { return installSource(this.spec()); }
47
+ /** The latest known status; checks again at most every six hours (or now, when forced). */
48
+ async status(force = false) {
49
+ const age = this.checked ? this.now() - this.checked.at : Infinity;
50
+ if (this.state !== 'installing' && (force || age > (this.checked?.error ? HOUR / 2 : 6 * HOUR))) {
51
+ this.pending ??= this.check().finally(() => { this.pending = undefined; });
52
+ await this.pending;
53
+ }
54
+ return this.snapshot();
55
+ }
56
+ /**
57
+ * Install the newer version. With `reload`, TheOne then restarts itself so the new version runs
58
+ * without restarting DSH; otherwise it is loaded the next time DSH starts.
59
+ */
60
+ async install(installer, reload) {
61
+ const status = await this.status();
62
+ if (!status.available || !status.installable || !installer || this.state === 'installing' || this.state === 'reloading' || this.state === 'restart')
63
+ return this.snapshot();
64
+ const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${this.checked.latest}`;
65
+ this.state = 'installing';
66
+ this.error = undefined;
67
+ try {
68
+ const result = await installer.installBundle(spec, { enabled: true });
69
+ if (result.application === 'applied' || result.application === 'restart-required') {
70
+ this.state = reload ? 'reloading' : 'restart';
71
+ reload?.(result.bundle ?? PACKAGE_NAME);
72
+ }
73
+ else {
74
+ this.state = 'failed';
75
+ const error = result.error;
76
+ this.error = String(error?.code ?? error?.message ?? result.application).slice(0, 120);
77
+ }
78
+ }
79
+ catch (error) {
80
+ this.state = 'failed';
81
+ this.error = (error instanceof Error ? error.message : String(error)).slice(0, 120);
82
+ }
83
+ return this.snapshot();
84
+ }
85
+ snapshot() {
86
+ const latest = this.checked?.latest;
87
+ const source = this.source;
88
+ const available = !!latest && compareVersions(latest, this.current) > 0;
89
+ return { current: this.current, ...(latest ? { latest } : {}), available, source,
90
+ installable: available && (source === 'npm' || (source === 'github' && !!this.checked?.sha)),
91
+ ...(this.state ? { state: this.state } : {}), ...(this.error ?? this.checked?.error ? { error: this.error ?? this.checked?.error } : {}) };
92
+ }
93
+ async check() {
94
+ const signal = AbortSignal.timeout(10000);
95
+ try {
96
+ if (this.source === 'npm') {
97
+ const response = await this.fetcher(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal });
98
+ if (!response.ok)
99
+ throw new Error(`npm ${response.status}`);
100
+ this.checked = { at: this.now(), latest: String((await response.json()).version ?? '') || undefined };
101
+ return;
102
+ }
103
+ // A GitHub install updates to the exact commit whose version was read.
104
+ const commit = await this.fetcher(`https://api.github.com/repos/${REPOSITORY}/commits/main`, { signal, headers: { accept: 'application/vnd.github.sha' } });
105
+ if (!commit.ok)
106
+ throw new Error(`GitHub ${commit.status}`);
107
+ const sha = (await commit.text()).trim();
108
+ if (!/^[0-9a-f]{40}$/.test(sha))
109
+ throw new Error('GitHub returned no commit');
110
+ const manifest = await this.fetcher(`https://raw.githubusercontent.com/${REPOSITORY}/${sha}/package.json`, { signal });
111
+ if (!manifest.ok)
112
+ throw new Error(`GitHub ${manifest.status}`);
113
+ const latest = String((await manifest.json()).version ?? '') || undefined;
114
+ this.checked = { at: this.now(), latest, sha };
115
+ }
116
+ catch (error) {
117
+ // Offline or rate-limited: keep what was known and try again later.
118
+ this.checked = { ...this.checked, at: this.now(), error: (error instanceof Error ? error.message : String(error)).slice(0, 120) };
119
+ }
120
+ }
121
+ }
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theone",
3
- "version": "0.3.9",
3
+ "version": "0.3.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Automatic context routing for DeepSeek Harness",