dsh-webdav-sync 1.0.2 → 1.0.4

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/lib/relay.js CHANGED
@@ -584,8 +584,10 @@ export class RelayServer {
584
584
  *
585
585
  * The configured ones are the reason the forwarder exists, and the client's own
586
586
  * are the protocol headers its request is made of, so a collision goes to the
587
- * client. `authorization` is the one header that never comes from the client:
588
- * it was addressed to this forwarder, not to the server, and the server is given
587
+ * client — except an explicitly configured `user-agent`, which must identify
588
+ * every forwarded client as the upstream expects. With no configured value,
589
+ * the client's user agent passes through. `authorization` never comes from the
590
+ * client: it was addressed to this forwarder, not to the server, which is given
589
591
  * the sync's own credential — or, when the configuration names an
590
592
  * `Authorization` line of its own, exactly that line, since a hand-written one
591
593
  * is the more specific instruction.
@@ -612,6 +614,8 @@ export class RelayServer {
612
614
  // for a continue that is not coming from the same connection.
613
615
  if (lower === 'expect')
614
616
  continue;
617
+ if (lower === 'user-agent' && taken.has(lower))
618
+ continue;
615
619
  taken.set(lower, { name, value });
616
620
  }
617
621
  if (!taken.has('authorization') && (config.user !== '' || config.password !== '')) {
package/lib/state.js CHANGED
@@ -38,11 +38,19 @@ export function defaultConfig() {
38
38
  passphrase: '',
39
39
  autoEvery: AUTO_MANUAL,
40
40
  models: true,
41
- defaults: true,
42
41
  plugins: true,
43
42
  patch: true,
43
+ // On by default: the profile names where its API keys come from, so a setup
44
+ // that travels without them arrives with every provider unauthenticated. The
45
+ // switch exists so a person can keep secrets off the server; see
46
+ // `docs/decisions/0054-sync-api-keys.md`.
47
+ keys: true,
44
48
  dirs: [],
45
- dirsEnabled: true,
49
+ // Off by default: a directory travels whole, and a fresh setup that has not
50
+ // named one yet has nothing to carry. The list is kept independently, so
51
+ // switching this on or off never forgets a path — see
52
+ // `docs/decisions/0056-drop-default-model-part.md`.
53
+ dirsEnabled: false,
46
54
  rememberPassword: true,
47
55
  headers: '',
48
56
  relayEnabled: false,
@@ -211,13 +219,11 @@ export function syncsPart(config, part) {
211
219
  switch (part) {
212
220
  case 'models':
213
221
  return config.models;
214
- case 'defaults':
215
- return config.defaults;
216
222
  case 'plugins':
217
223
  return config.plugins;
218
224
  case 'patch':
219
225
  return config.patch;
220
- // The extra directories carry a switch of their own, like the four parts above:
226
+ // The extra directories carry a switch of their own, like the three parts above:
221
227
  // the list of paths says *which* directories, the switch says whether this
222
228
  // computer carries them at all. Off keeps the list — the paths are not
223
229
  // forgotten — and changes nothing anywhere else: the part is carried through
@@ -225,6 +231,13 @@ export function syncsPart(config, part) {
225
231
  // others.
226
232
  case 'dirs':
227
233
  return config.dirsEnabled;
234
+ // The API keys are the one part that is a secret, so they are the one part
235
+ // whose switch a person may genuinely want off: the server holds the sealed
236
+ // file, and a sealed file still sits on someone else's disk. Off leaves every
237
+ // key here and carries the server's copy of the part through untouched, so
238
+ // nothing is deleted anywhere either.
239
+ case 'keys':
240
+ return config.keys;
228
241
  }
229
242
  }
230
243
  /** Fill in defaults and coerce the several fields that come from a browser. */
@@ -249,9 +262,9 @@ export function normaliseConfig(raw) {
249
262
  passphrase: text('passphrase'),
250
263
  autoEvery,
251
264
  models: flag('models'),
252
- defaults: flag('defaults'),
253
265
  plugins: flag('plugins'),
254
266
  patch: flag('patch'),
267
+ keys: flag('keys'),
255
268
  dirs: Array.isArray(raw.dirs) ? normaliseDirs(raw.dirs) : base.dirs,
256
269
  dirsEnabled: flag('dirsEnabled'),
257
270
  rememberPassword: flag('rememberPassword'),
package/lib/store.d.ts CHANGED
@@ -8,16 +8,20 @@
8
8
  * everything else — MCP servers, permission presets, UI settings, inserts — is
9
9
  * the rest of them. `package.json` names the installed plugins.
10
10
  *
11
+ * The default-model row is read here only to be kept out of the sync: it is one of
12
+ * the rows a sync never carries and never writes, like this plugin's own row. See
13
+ * `docs/decisions/0056-drop-default-model-part.md`.
14
+ *
11
15
  * Writes go through the `yaml` Document API rather than a stringify of a parsed
12
16
  * object, so the file's own comments and layout survive a sync that only meant to
13
17
  * change one provider.
14
18
  */
15
19
  import type { Document } from 'yaml';
16
- import { type DefaultsPart, type ModelsPart, type PatchEntry, type PatchPart, type PluginsPart } from './wire.js';
20
+ import { type KeysPart, type ModelsPart, type PatchEntry, type PatchPart, type PluginsPart } from './wire.js';
17
21
  import type { ResolvedPaths } from './paths.js';
18
22
  /** The profile patch row holding the model catalog. */
19
23
  export declare const MODELS_ID = "llm-pi-ai";
20
- /** The profile patch row holding the default model. */
24
+ /** The profile patch row holding the default model; kept out of every part. */
21
25
  export declare const DEFAULTS_ID = "agent-default-model";
22
26
  /** Package whose patch row the plugin adds for itself; never carried as a part. */
23
27
  export declare const SELF_ID = "dsh-webdav-sync";
@@ -50,9 +54,15 @@ export declare function readManifest(paths: ResolvedPaths): Promise<ManifestFile
50
54
  /** Every part of a profile this plugin can carry, as collected from disk. */
51
55
  export interface Collected {
52
56
  models?: ModelsPart;
53
- defaults?: DefaultsPart;
54
57
  plugins?: PluginsPart;
55
58
  patch?: PatchPart;
59
+ /**
60
+ * The keys, added by `take` rather than here: they are the one part that does
61
+ * not come from a profile file. Left out entirely when this computer holds
62
+ * nothing for any name the profile uses — the same "no opinion" an absent
63
+ * `models` carries, so the server's copy is carried through untouched.
64
+ */
65
+ keys?: KeysPart;
56
66
  }
57
67
  /**
58
68
  * Gather the parts from one profile.
@@ -73,14 +83,14 @@ export declare function missingPlugins(plugins: PluginsPart | undefined, install
73
83
  /**
74
84
  * Rewrite the profile patch so it holds the merged parts.
75
85
  *
76
- * Model and default rows are replaced in place when they exist and appended when
77
- * they do not; the remaining rows are replaced wholesale by the merged list, with
78
- * the model rows kept where they were. A no-op merge leaves the bytes untouched,
79
- * so a sync that changed nothing does not rewrite the file.
86
+ * The model row is replaced in place when it exists and appended when it does not;
87
+ * the remaining rows are replaced wholesale by the merged list, with the model, the
88
+ * default-model and this plugin's own rows kept where they were. A no-op merge
89
+ * leaves the bytes untouched, so a sync that changed nothing does not rewrite the
90
+ * file.
80
91
  */
81
92
  export declare function applyPatch(patch: PatchFile, parts: {
82
93
  models?: ModelsPart | null;
83
- defaults?: DefaultsPart | null;
84
94
  patch?: PatchPart;
85
95
  }): string;
86
96
  /**
package/lib/store.js CHANGED
@@ -8,6 +8,10 @@
8
8
  * everything else — MCP servers, permission presets, UI settings, inserts — is
9
9
  * the rest of them. `package.json` names the installed plugins.
10
10
  *
11
+ * The default-model row is read here only to be kept out of the sync: it is one of
12
+ * the rows a sync never carries and never writes, like this plugin's own row. See
13
+ * `docs/decisions/0056-drop-default-model-part.md`.
14
+ *
11
15
  * Writes go through the `yaml` Document API rather than a stringify of a parsed
12
16
  * object, so the file's own comments and layout survive a sync that only meant to
13
17
  * change one provider.
@@ -20,7 +24,7 @@ import { hashJson } from './crypto.js';
20
24
  import { BACKUP_EXT, } from './wire.js';
21
25
  /** The profile patch row holding the model catalog. */
22
26
  export const MODELS_ID = 'llm-pi-ai';
23
- /** The profile patch row holding the default model. */
27
+ /** The profile patch row holding the default model; kept out of every part. */
24
28
  export const DEFAULTS_ID = 'agent-default-model';
25
29
  /** Package whose patch row the plugin adds for itself; never carried as a part. */
26
30
  export const SELF_ID = 'dsh-webdav-sync';
@@ -97,11 +101,6 @@ export function collect(patch, manifest) {
97
101
  const config = modelsRow.value.config;
98
102
  out.models = { config: isPlainObject(config) ? config : {} };
99
103
  }
100
- const defaultsRow = patch.rows.find((row) => row.key === `id:${DEFAULTS_ID}`);
101
- if (defaultsRow !== undefined) {
102
- const config = defaultsRow.value.config;
103
- out.defaults = { config: isPlainObject(config) ? config : {} };
104
- }
105
104
  const dependencies = manifest.value.dependencies;
106
105
  const bundles = readBundles(manifest.value);
107
106
  if (isPlainObject(dependencies) || bundles.length > 0) {
@@ -152,10 +151,11 @@ export function missingPlugins(plugins, installed) {
152
151
  /**
153
152
  * Rewrite the profile patch so it holds the merged parts.
154
153
  *
155
- * Model and default rows are replaced in place when they exist and appended when
156
- * they do not; the remaining rows are replaced wholesale by the merged list, with
157
- * the model rows kept where they were. A no-op merge leaves the bytes untouched,
158
- * so a sync that changed nothing does not rewrite the file.
154
+ * The model row is replaced in place when it exists and appended when it does not;
155
+ * the remaining rows are replaced wholesale by the merged list, with the model, the
156
+ * default-model and this plugin's own rows kept where they were. A no-op merge
157
+ * leaves the bytes untouched, so a sync that changed nothing does not rewrite the
158
+ * file.
159
159
  */
160
160
  export function applyPatch(patch, parts) {
161
161
  const document = patch.document;
@@ -165,9 +165,6 @@ export function applyPatch(patch, parts) {
165
165
  if (parts.models !== undefined && parts.models !== null) {
166
166
  rows.push({ id: MODELS_ID, name: '@deepseek-ai/dsh-llm-pi-ai', config: parts.models.config });
167
167
  }
168
- if (parts.defaults !== undefined && parts.defaults !== null) {
169
- rows.push({ id: DEFAULTS_ID, name: '@deepseek-ai/dsh-agent-default-model', config: parts.defaults.config });
170
- }
171
168
  if (parts.patch !== undefined)
172
169
  rows.push(...parts.patch.entries);
173
170
  const replacement = document.createNode(rows);
@@ -187,15 +184,13 @@ export function applyPatch(patch, parts) {
187
184
  const value = item.toJSON();
188
185
  byKey.set(keyOf(value), { node: item, index });
189
186
  });
190
- // 1. Replace or append the two id-targeted model rows.
191
- for (const [id, part] of [
192
- [MODELS_ID, parts.models],
193
- [DEFAULTS_ID, parts.defaults],
194
- ]) {
195
- if (part === undefined)
196
- continue;
197
- const existing = byKey.get(`id:${id}`);
198
- if (part === null) {
187
+ // 1. Replace or append the model row. It is the one row with a part of its own;
188
+ // the default-model row that sits beside it is left exactly as it is, since
189
+ // it is not carried at all.
190
+ const modelPart = parts.models;
191
+ if (modelPart !== undefined) {
192
+ const existing = byKey.get(`id:${MODELS_ID}`);
193
+ if (modelPart === null) {
199
194
  // The other computer took the row away, and that is the merge: a part that
200
195
  // was removed there has to leave here, or the two never converge.
201
196
  if (existing !== undefined) {
@@ -203,9 +198,8 @@ export function applyPatch(patch, parts) {
203
198
  if (at !== -1)
204
199
  contents.items.splice(at, 1);
205
200
  }
206
- continue;
207
201
  }
208
- if (existing !== undefined) {
202
+ else if (existing !== undefined) {
209
203
  // Replace the config node only when it actually differs, and then merge
210
204
  // into it rather than rebuilding: the catalog holds the user's own
211
205
  // comments about their providers. `keepScalar` keeps the node itself.
@@ -217,16 +211,16 @@ export function applyPatch(patch, parts) {
217
211
  // lacked the key does not gain an empty one.
218
212
  const config = existing.node.has('config') ? existing.node.get('config', true) : undefined;
219
213
  if (config === undefined) {
220
- if (Object.keys(part.config).length > 0) {
221
- existing.node.set('config', document.createNode(part.config));
214
+ if (Object.keys(modelPart.config).length > 0) {
215
+ existing.node.set('config', document.createNode(modelPart.config));
222
216
  }
223
217
  }
224
- else if (!sameJson(config, part.config)) {
225
- existing.node.set('config', mergeInto(document, config, part.config));
218
+ else if (!sameJson(config, modelPart.config)) {
219
+ existing.node.set('config', mergeInto(document, config, modelPart.config));
226
220
  }
227
221
  }
228
222
  else {
229
- const row = document.createNode({ id, name: defaultNameFor(id), config: part.config });
223
+ const row = document.createNode({ id: MODELS_ID, name: '@deepseek-ai/dsh-llm-pi-ai', config: modelPart.config });
230
224
  contents.items.push(row);
231
225
  }
232
226
  }
@@ -275,10 +269,6 @@ export function applyPatch(patch, parts) {
275
269
  return patch.text;
276
270
  return document.toString();
277
271
  }
278
- /** The loader `name` a row carries when this plugin has to create it. */
279
- function defaultNameFor(id) {
280
- return id === MODELS_ID ? '@deepseek-ai/dsh-llm-pi-ai' : '@deepseek-ai/dsh-agent-default-model';
281
- }
282
272
  /**
283
273
  * Rewrite the profile manifest with merged plugin declarations, two-space
284
274
  * indented.
package/lib/wire.d.ts CHANGED
@@ -62,8 +62,19 @@ export declare const LEGACY_REMOTE_FILE = "dsh-profile.dshsync";
62
62
  * second computer gets both the path and what is under it. Its empty value is a
63
63
  * real value (`{ dirs: [] }`), not an absent part, because "the list is now
64
64
  * empty" is a change that has to travel like any other.
65
+ *
66
+ * `keys` is the one part whose content is a secret, and the one part that lives
67
+ * outside the profile: the API keys this computer's credential store holds for its
68
+ * providers. It is on by default, because a setup that travels without its keys
69
+ * arrives with every provider unauthenticated, and it can be switched off on its
70
+ * own; see {@link KeysPart}.
71
+ *
72
+ * The default model is deliberately *not* a part: which model a new session starts
73
+ * on is changed several times a day, and every change would push a file for
74
+ * something a person sets on each computer anyway. Its row stays out of `patch`
75
+ * too, like this plugin's own row. See `docs/decisions/0056-drop-default-model-part.md`.
65
76
  */
66
- export declare const PARTS: readonly ["models", "defaults", "plugins", "patch", "dirs"];
77
+ export declare const PARTS: readonly ["models", "plugins", "patch", "dirs", "keys"];
67
78
  export type Part = (typeof PARTS)[number];
68
79
  /**
69
80
  * Most extra directories one configuration may name.
@@ -91,11 +102,6 @@ export interface ModelsPart {
91
102
  /** The whole `llm-pi-ai` entry config, so fields beyond `providers` travel too. */
92
103
  config: Record<string, unknown>;
93
104
  }
94
- /** The `defaults` part: which provider/model new sessions start on. */
95
- export interface DefaultsPart {
96
- /** The whole `agent-default-model` entry config. */
97
- config: Record<string, unknown>;
98
- }
99
105
  /** One other top-level row of the profile patch, carried by the `patch` part. */
100
106
  export interface PatchEntry {
101
107
  /** `id` of an id-targeted row, or `insert` for an insert block. */
@@ -150,6 +156,20 @@ export interface DirSpec {
150
156
  export interface DirsPart {
151
157
  dirs: DirSpec[];
152
158
  }
159
+ /**
160
+ * A credential this computer could not read or write, and why not.
161
+ *
162
+ * A store that cannot be read must not look like a store with nothing in it: an
163
+ * empty read would say "this computer has no keys", which is a real answer a sync
164
+ * acts on. This is how the two are told apart, the same way {@link DirProblem}
165
+ * keeps an unreadable directory from looking like an empty one.
166
+ */
167
+ export interface KeyProblem {
168
+ /** The reference name, as the profile asks for it; '' when the store itself failed. */
169
+ ref: string;
170
+ /** What went wrong, in a sentence the card can show. */
171
+ message: string;
172
+ }
153
173
  /** A configured directory this computer could not read, and why not. */
154
174
  export interface DirProblem {
155
175
  /** The configured path, as it is stored. */
@@ -167,6 +187,33 @@ export interface DirProblem {
167
187
  */
168
188
  missing?: boolean;
169
189
  }
190
+ /**
191
+ * The `keys` part: the API keys this computer holds for its providers.
192
+ *
193
+ * The profile names a credential instead of holding it — `apiKeyEnv: OFOX_API_KEY`
194
+ * on a provider route, `secretEnv` on a GitHub webhook — and the value behind that
195
+ * name lives in this computer's credential store (`$DSH_HOME/.credentials.yaml`).
196
+ * That is why a setup synced without this part arrives complete and still cannot
197
+ * talk to a single provider: it carries the names, and the names resolve to
198
+ * nothing here.
199
+ *
200
+ * A name is the whole address of a secret, so this part is a map and not a list:
201
+ * applying it puts each value back under the name the profile already asks for.
202
+ * Nothing here is ever deleted — a key removed on one computer stays on the others,
203
+ * because a credential a person stored by hand is not this plugin's to throw away.
204
+ * See `docs/decisions/0054-sync-api-keys.md`.
205
+ */
206
+ export interface KeysPart {
207
+ /**
208
+ * Secret values by the reference name that resolves them, exactly as the
209
+ * credential store holds them (`apiKeyEnv`/`secretEnv` values, verbatim).
210
+ *
211
+ * Only names this computer actually has a stored value for appear; a name that
212
+ * falls through to the environment is left out, since the environment is the
213
+ * other computer's own business.
214
+ */
215
+ refs: Record<string, string>;
216
+ }
170
217
  /** Everything one sealed backup carries. Field names are the wire format. */
171
218
  export interface Bundle {
172
219
  format: 'dsh-webdav-sync-backup';
@@ -176,10 +223,10 @@ export interface Bundle {
176
223
  /** Which app sealed it, for a file several tools might share a folder with. */
177
224
  app: string;
178
225
  models?: ModelsPart;
179
- defaults?: DefaultsPart;
180
226
  plugins?: PluginsPart;
181
227
  patch?: PatchPart;
182
228
  dirs?: DirsPart;
229
+ keys?: KeysPart;
183
230
  }
184
231
  /** The format word in the envelope; anything else on the server is not ours. */
185
232
  export declare const BUNDLE_FORMAT = "dsh-webdav-sync-backup";
@@ -209,10 +256,10 @@ export interface Notice {
209
256
  /** One side of the last completed sync, per part. */
210
257
  export interface PartHashes {
211
258
  models?: string;
212
- defaults?: string;
213
259
  plugins?: string;
214
260
  patch?: string;
215
261
  dirs?: string;
262
+ keys?: string;
216
263
  }
217
264
  /**
218
265
  * The variable half of the state: everything a completed sync records.
@@ -276,12 +323,20 @@ export interface SyncConfig {
276
323
  autoEvery: number;
277
324
  /** Whether the model catalog travels. */
278
325
  models: boolean;
279
- /** Whether the default-model row travels. */
280
- defaults: boolean;
281
326
  /** Whether the installed-plugin list travels. */
282
327
  plugins: boolean;
283
328
  /** Whether the remaining profile-patch rows travel. */
284
329
  patch: boolean;
330
+ /**
331
+ * Whether this computer's stored API keys travel.
332
+ *
333
+ * On by default: a provider route names its credential, so a setup synced
334
+ * without the keys arrives with every provider unauthenticated. Turning it off
335
+ * keeps this computer's keys off the server entirely — nothing is deleted
336
+ * anywhere, and the server's copy of the part is carried through untouched,
337
+ * exactly as for the three parts above.
338
+ */
339
+ keys: boolean;
285
340
  /**
286
341
  * Absolute paths of the extra directories that travel with the setup, in the
287
342
  * order the user added them. The list itself is the `dirs` part, so what is
@@ -292,10 +347,11 @@ export interface SyncConfig {
292
347
  /**
293
348
  * Whether the extra directories travel with this setup.
294
349
  *
295
- * The list above is kept either way: switching this off says "this computer does
296
- * not carry them", not "forget them". Nothing is deleted anywhere by turning it
297
- * off — the server's copy of the part is carried through untouched, exactly as
298
- * for the four parts above.
350
+ * Off is the resting state: a configuration that has not said otherwise carries
351
+ * no directory. The list above is kept either way: switching this off says "this
352
+ * computer does not carry them", not "forget them". Nothing is deleted anywhere
353
+ * by turning it off — the server's copy of the part is carried through
354
+ * untouched, exactly as for the other parts.
299
355
  */
300
356
  dirsEnabled: boolean;
301
357
  /** Whether the plugin's own address, user and password are kept for next time. */
@@ -339,9 +395,10 @@ export interface ConfigView {
339
395
  passphraseSet: boolean;
340
396
  autoEvery: number;
341
397
  models: boolean;
342
- defaults: boolean;
343
398
  plugins: boolean;
344
399
  patch: boolean;
400
+ /** Whether this computer's stored API keys travel. */
401
+ keys: boolean;
345
402
  /** The extra directories that travel, as stored. Not a secret. */
346
403
  dirs: string[];
347
404
  /** Whether the extra directories travel with the current configuration. */
@@ -412,6 +469,13 @@ export interface StatusResult {
412
469
  * this is how the card is able to say so.
413
470
  */
414
471
  dirProblems?: DirProblem[];
472
+ /**
473
+ * Credentials the configuration names that could not be read here, with the
474
+ * reason. Present exactly when there is at least one, for the same reason as
475
+ * {@link StatusResult.dirProblems}: a store that cannot be read is left as the
476
+ * server has it for this sync rather than read as empty.
477
+ */
478
+ keysProblems?: KeyProblem[];
415
479
  /** The profile declares plugins the installed tree does not have yet. */
416
480
  pendingInstall: boolean;
417
481
  /** Which plugins those are, present exactly when {@link pendingInstall} is true. */
@@ -427,9 +491,10 @@ export interface ConfigureBody {
427
491
  passphrase?: string;
428
492
  autoEvery?: number;
429
493
  models?: boolean;
430
- defaults?: boolean;
431
494
  plugins?: boolean;
432
495
  patch?: boolean;
496
+ /** Whether this computer's stored API keys travel. */
497
+ keys?: boolean;
433
498
  /** The whole list of extra directories, replacing the stored one. */
434
499
  dirs?: string[];
435
500
  /** Whether the extra directories travel with this setup. */
@@ -471,6 +536,13 @@ export interface SyncResult {
471
536
  pendingNames?: string[];
472
537
  /** The profile has to be reloaded for the applied configuration to take effect. */
473
538
  reloadNeeded: boolean;
539
+ /**
540
+ * Keys the merge brought here that this computer's credential store would not
541
+ * take, with the reason. Present exactly when there is at least one: a key that
542
+ * could not be stored leaves that provider unauthenticated, and a sync that
543
+ * reported success would hide it.
544
+ */
545
+ keysProblems?: KeyProblem[];
474
546
  }
475
547
  /** POST {@link ROUTE_CHECK} result. */
476
548
  export interface CheckResult {
package/lib/wire.js CHANGED
@@ -64,8 +64,19 @@ export const LEGACY_REMOTE_FILE = 'dsh-profile.dshsync';
64
64
  * second computer gets both the path and what is under it. Its empty value is a
65
65
  * real value (`{ dirs: [] }`), not an absent part, because "the list is now
66
66
  * empty" is a change that has to travel like any other.
67
+ *
68
+ * `keys` is the one part whose content is a secret, and the one part that lives
69
+ * outside the profile: the API keys this computer's credential store holds for its
70
+ * providers. It is on by default, because a setup that travels without its keys
71
+ * arrives with every provider unauthenticated, and it can be switched off on its
72
+ * own; see {@link KeysPart}.
73
+ *
74
+ * The default model is deliberately *not* a part: which model a new session starts
75
+ * on is changed several times a day, and every change would push a file for
76
+ * something a person sets on each computer anyway. Its row stays out of `patch`
77
+ * too, like this plugin's own row. See `docs/decisions/0056-drop-default-model-part.md`.
67
78
  */
68
- export const PARTS = ['models', 'defaults', 'plugins', 'patch', 'dirs'];
79
+ export const PARTS = ['models', 'plugins', 'patch', 'dirs', 'keys'];
69
80
  /**
70
81
  * Most extra directories one configuration may name.
71
82
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-webdav-sync",
3
- "version": "1.0.2",
4
- "description": "用 WebDAV 在多台电脑之间同步 DSH 的配置:模型供应商与模型、默认模型、已安装插件。整个备份在离开本机前用口令加密(AES-256-GCM + PBKDF2),以分片三方合并的方式在服务端保留一个加密文件。",
3
+ "version": "1.0.4",
4
+ "description": "用 WebDAV 在多台电脑之间同步 DSH 的配置:模型供应商与模型、已安装插件、其余 profile 配置、附加目录与 API 密钥。整个备份在离开本机前用口令加密(AES-256-GCM + PBKDF2),以分片三方合并的方式在服务端保留一个加密文件。",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "deepseek",