dsh-perm-gate 2.1.2 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.ja.md +108 -0
  2. package/CHANGELOG.ko.md +104 -0
  3. package/CHANGELOG.md +236 -0
  4. package/INSTALL.ja.md +41 -21
  5. package/INSTALL.ko.md +37 -19
  6. package/INSTALL.md +46 -23
  7. package/INSTALL.zh.md +33 -17
  8. package/README.ja.md +52 -12
  9. package/README.ko.md +53 -13
  10. package/README.md +68 -10
  11. package/README.zh.md +41 -10
  12. package/assets/allow1.png +0 -0
  13. package/assets/allow2.png +0 -0
  14. package/assets/gateoff.png +0 -0
  15. package/assets/llms.png +0 -0
  16. package/assets/permlist.png +0 -0
  17. package/assets/setting-head.png +0 -0
  18. package/assets/setting-rules.png +0 -0
  19. package/cordis.patch.yml +11 -8
  20. package/docs/baseline-guard.md +56 -0
  21. package/docs/plan-rules-to-settings.md +217 -0
  22. package/docs/rules-format.en.md +294 -0
  23. package/docs/rules-format.md +279 -0
  24. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  25. package/dsh.plugin.json +2 -2
  26. package/examples/permissions.example.yaml +16 -1
  27. package/lib/allowlist.d.ts +27 -0
  28. package/lib/allowlist.js +103 -1
  29. package/lib/cli.js +11 -14
  30. package/lib/client.js +484 -6
  31. package/lib/client.js.map +1 -1
  32. package/lib/command-dispatcher.d.ts +22 -0
  33. package/lib/command-dispatcher.js +97 -0
  34. package/lib/command-semantics.d.ts +62 -0
  35. package/lib/command-semantics.js +6 -0
  36. package/lib/compiler.d.ts +11 -0
  37. package/lib/compiler.js +16 -0
  38. package/lib/config.d.ts +73 -12
  39. package/lib/config.js +56 -14
  40. package/lib/dry-run.d.ts +87 -0
  41. package/lib/dry-run.js +110 -0
  42. package/lib/engine.js +52 -2
  43. package/lib/evaluate.js +76 -2
  44. package/lib/events.d.ts +56 -2
  45. package/lib/events.js +92 -0
  46. package/lib/index.js +106 -26
  47. package/lib/parsers/git.d.ts +43 -0
  48. package/lib/parsers/git.js +404 -0
  49. package/lib/parsers/shell-cmds.d.ts +32 -0
  50. package/lib/parsers/shell-cmds.js +293 -0
  51. package/lib/preset.d.ts +13 -0
  52. package/lib/preset.js +17 -0
  53. package/lib/risk.d.ts +17 -3
  54. package/lib/risk.js +17 -3
  55. package/lib/rule-chain.js +13 -22
  56. package/lib/rule-dims.d.ts +35 -0
  57. package/lib/rule-dims.js +36 -0
  58. package/lib/rule.d.ts +36 -1
  59. package/lib/rule.js +56 -8
  60. package/lib/rules-view.d.ts +52 -0
  61. package/lib/rules-view.js +126 -0
  62. package/lib/runtime.d.ts +121 -7
  63. package/lib/runtime.js +204 -25
  64. package/lib/shell.d.ts +14 -0
  65. package/lib/shell.js +17 -0
  66. package/package.json +19 -2
  67. package/scripts/patch-permission-glyph.mjs +225 -42
package/lib/runtime.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import { readFileSync } from 'node:fs';
12
12
  import { makeEntry, MemoryAuditMirror } from './audit.js';
13
13
  import { resolvePermissiveStrategies } from './config.js';
14
- import { appendAllowCommand, listAllowCommands, replaceAllowCommands } from './allowlist.js';
14
+ import { appendAllowCommand, listAllowCommands, listAllowFromRulesDoc, replaceAllowCommands } from './allowlist.js';
15
15
  import { decide, CONTENT_ARG_KEYS } from './engine.js';
16
16
  import { EventLog, saveEventSnapshots } from './events.js';
17
17
  import { decideRules } from './evaluate.js';
@@ -21,13 +21,34 @@ import { ArtifactRegistry } from './path.js';
21
21
  import { classifyRisk, classifyRiskWith } from './risk.js';
22
22
  import { completeViaHost, DEFAULT_HOST_MODEL } from './host-llm.js';
23
23
  import { chatCompletion } from './classifier.js';
24
- import { compileDocument, documentHash, extractPathCandidates, parsePermissionsDocument } from './rule.js';
24
+ import { compileDocument, compileRulesObject, documentHash, extractPathCandidates, parsePermissionsDocument, } from './rule.js';
25
25
  import { decomposeShellCommand } from './shell.js';
26
26
  import { extractAgentCandidates } from './agent-identity.js';
27
27
  import { resolveRuleChain } from './rule-chain.js';
28
28
  import { DEFAULT_DENY_KEYWORDS } from './deny-defaults.js';
29
29
  import { permissionPresetOf, presetInScope } from './preset.js';
30
30
  import { resolveGatePresets } from './config.js';
31
+ /** Locate a rule by its chain-wide index (deny entries first, then allow, then ask). */
32
+ function findRuleByIndex(ruleset, index) {
33
+ return (ruleset.deny.find((entry) => entry.index === index) ??
34
+ ruleset.allow.find((entry) => entry.index === index) ??
35
+ ruleset.ask.find((entry) => entry.index === index));
36
+ }
37
+ /** Which layer of the chain produced a decision, from its stage. */
38
+ function sourceOfStage(stage, reason) {
39
+ switch (stage) {
40
+ case 'hard-deny':
41
+ return 'hard-deny';
42
+ case 'grant':
43
+ return 'grant';
44
+ case 'rule':
45
+ return 'rule';
46
+ case 'classifier':
47
+ return 'classifier';
48
+ default:
49
+ return reason.startsWith('permissive') ? 'permissive' : 'default';
50
+ }
51
+ }
31
52
  /**
32
53
  * One execution's session event log, or `undefined` when the host exposes none.
33
54
  *
@@ -209,6 +230,12 @@ export class PermGateRuntime {
209
230
  * frozen events, which would otherwise re-fold on every tool call.
210
231
  */
211
232
  presetCache;
233
+ /**
234
+ * Last stand-down announced per session (sessionId → the out-of-scope preset
235
+ * name, `''` when the session records none). Bounded by the session count and
236
+ * only read on the stand-down path — the in-scope path never touches it.
237
+ */
238
+ standDownAnnounced = new Map();
212
239
  learning;
213
240
  events;
214
241
  /** Learning candidates awaiting human approval + execution (call fingerprint → candidate). */
@@ -252,6 +279,24 @@ export class PermGateRuntime {
252
279
  deny: [], allow: [], ask: [],
253
280
  caseInsensitivePaths: options.caseInsensitivePaths ?? true,
254
281
  };
282
+ // Settings-first (dual-source): a configured `dsh-perm-gate-rules`
283
+ // namespace overrides every file path — the gate then loads with zero
284
+ // filesystem dependency. Bare defaults (nothing configured) fall through
285
+ // to the file sources below. Compile errors propagate: the constructor
286
+ // fails loud, and `reload()` keeps the previous rules on a hot change.
287
+ const settingsDoc = options.readRulesDocument?.();
288
+ if (settingsDoc !== undefined) {
289
+ const hash = documentHash(JSON.stringify(settingsDoc));
290
+ const hit = this.cache.get(hash);
291
+ if (hit !== undefined)
292
+ return hit;
293
+ const compiled = compileRulesObject(settingsDoc, {
294
+ maxGlobStars: 2,
295
+ caseInsensitivePaths: options.caseInsensitivePaths ?? true,
296
+ });
297
+ this.cache.set(hash, compiled);
298
+ return compiled;
299
+ }
255
300
  // Chain mode: when searchUp is enabled, use the multi-file chain resolver.
256
301
  if (options.searchUp === true) {
257
302
  const cwd = options.cwd ?? process.cwd();
@@ -589,9 +634,38 @@ export class PermGateRuntime {
589
634
  * `danger-full-access` means "full access without approval prompts", where a
590
635
  * forwarded ask can only fail (the approval seam rejects before any answerer
591
636
  * runs) and a hard-deny would silently overrule the tier the user chose.
637
+ *
638
+ * P0 is therefore monotonic only WITHIN the gate's scope, not across every
639
+ * preset. {@link announceStandDown} makes that visible instead of silent.
592
640
  */
593
- gateActive(exec) {
594
- return presetInScope(this.presetOf(exec), this.gatePresets);
641
+ gateActive(preset) {
642
+ return presetInScope(preset, this.gatePresets);
643
+ }
644
+ /**
645
+ * Record one `stand-down` event the first time a session is observed with a
646
+ * preset outside {@link gatePresets}, and again whenever that preset changes.
647
+ *
648
+ * Per-call silence is what makes a stand-down dangerous: the tool call looks
649
+ * exactly like a call the gate inspected and allowed. One event per
650
+ * transition is enough to say otherwise, and keeps the feed (and the
651
+ * `events.jsonl` append) proportional to preset changes rather than to call
652
+ * volume.
653
+ *
654
+ * The event is a NOTICE, not a decision: it carries no allow/deny meaning,
655
+ * and the call it was observed on is still settled entirely by the selected
656
+ * tier. `verdict: 'stand-down'` labels it for the history view.
657
+ */
658
+ announceStandDown(exec, preset) {
659
+ const sessionId = sessionIdOf(exec);
660
+ const announced = preset ?? '';
661
+ if (this.standDownAnnounced.get(sessionId) === announced)
662
+ return;
663
+ this.standDownAnnounced.set(sessionId, announced);
664
+ const scope = this.gatePresets.includes('*') ? 'every preset' : this.gatePresets.join(', ');
665
+ const named = preset === undefined ? 'no permission/preset recorded' : `"${preset}"`;
666
+ this.recordEvent(exec, 'stand-down', `perm-gate is INACTIVE: the session permission preset is ${named} and the gate scope is ${scope}. `
667
+ + 'No P0 hard-deny, no rule, no ask and no allow is evaluated for any call in this session — '
668
+ + 'the selected tier\'s own policy governs. Select a preset in the gate scope to re-arm it.', { verdict: 'stand-down' });
595
669
  }
596
670
  /**
597
671
  * Whether an `ask` produced while the gate is active can actually reach a
@@ -808,10 +882,16 @@ export class PermGateRuntime {
808
882
  */
809
883
  decideExecution(exec) {
810
884
  // The gate is scoped to its own tier(s): anywhere else it stands down
811
- // entirely and records nothing (the session's selected tier owns the call).
812
- if (!this.gateActive(exec))
885
+ // entirely and decides nothing (the session's selected tier owns the call).
886
+ // The stand-down is announced ONCE per (session, preset) transition — the
887
+ // call's outcome is unchanged, but the user must be able to see that the
888
+ // gate — including P0 hard-deny — is inactive; a silent stand-down reads as
889
+ // "the gate looked at this and allowed it".
890
+ const preset = this.presetOf(exec);
891
+ if (!this.gateActive(preset)) {
892
+ this.announceStandDown(exec, preset);
813
893
  return undefined;
814
- const ctx = this.ctxFor(exec);
894
+ }
815
895
  const callId = randomId();
816
896
  // Preset deny-keyword layer (deny wins over allow): a dangerous-keyword hit
817
897
  // vetoes before grants / rules / LLM. Additive deny only — P0 semantics are
@@ -823,13 +903,9 @@ export class PermGateRuntime {
823
903
  this.recordEvent(exec, 'deny', reason, { verdict: 'deny-keyword' });
824
904
  return { kind: 'deny', reason };
825
905
  }
826
- const grantResolver = (tool, args) => {
827
- if (exec.parentAuthorized === false)
828
- return 'no-match';
829
- return this.grants.decide(tool, args);
830
- };
831
- const raw = decide(ctx, { decide: (c) => decideRules(this.ruleset, c) }, grantResolver, this.autoAllowExtra);
832
- const decision = this.applyPermissive(raw);
906
+ // The same chain `explainCall` reports, evaluated once here so the two views
907
+ // cannot drift; this caller adds the side effects and the session layers.
908
+ const decision = this.applyPermissive(this.rawPolicy(exec));
833
909
  // Deterministic cleanup pre-screen: if a deletion command targets a
834
910
  // regenerable/temporary artifact inside the workspace, allow it directly.
835
911
  // This runs BEFORE the ask/passthrough logic so it works even under
@@ -898,6 +974,83 @@ export class PermGateRuntime {
898
974
  this.clearCall(exec, decision.reason, source, source);
899
975
  return undefined;
900
976
  }
977
+ /**
978
+ * The **pure policy** outcome for one call: the same chain
979
+ * {@link decideExecution} runs, with every side effect and every session-state
980
+ * layer removed.
981
+ *
982
+ * Side effects removed — no audit entry, no event, no ask tracking, no call
983
+ * clearance, no learning. A rule-test panel must be able to run on every
984
+ * keystroke without writing to the live decision feed.
985
+ *
986
+ * Session-state layers removed — the preset stand-down and the
987
+ * `approval: never` ask degradation both describe a session a session-less
988
+ * caller does not have. Including them is not harmless: the degradation turns
989
+ * every "the rules would ask a human" into a reported **allow**, which is the
990
+ * opposite of the truth for exactly the rules someone opens the panel to
991
+ * review (measured on the live host: `shell ls -la` reported `allow` while the
992
+ * rule layer said `ask`).
993
+ *
994
+ * `reason` is never collapsed: "allow" has causes worth naming (a rule, a
995
+ * grant, cleanup-safety, the permissive default).
996
+ */
997
+ explainCall(exec) {
998
+ const keyword = this.denyKeywordHit(exec);
999
+ if (keyword !== undefined) {
1000
+ return {
1001
+ action: 'deny',
1002
+ reason: `deny-keyword: matches preset blacklist entry "${keyword}"`,
1003
+ source: 'deny-keyword',
1004
+ };
1005
+ }
1006
+ const decision = this.applyPermissive(this.rawPolicy(exec));
1007
+ if (decision.action === 'ask') {
1008
+ const cleanupSafe = isCleanupSafe(exec);
1009
+ if (cleanupSafe !== undefined) {
1010
+ return { action: 'allow', reason: `cleanup-safe: ${cleanupSafe}`, source: 'cleanup-safe' };
1011
+ }
1012
+ }
1013
+ return {
1014
+ action: decision.action,
1015
+ reason: decision.reason,
1016
+ source: sourceOfStage(decision.stage, decision.reason),
1017
+ ...(decision.ruleIndex !== undefined ? { ruleIndex: decision.ruleIndex } : {}),
1018
+ };
1019
+ }
1020
+ /**
1021
+ * P0 hard-deny → P1 session grant → P2 rules → P4 ask, as a value. Shared by
1022
+ * the host-facing decision and {@link explainCall} so the two cannot drift.
1023
+ */
1024
+ rawPolicy(exec) {
1025
+ const grantResolver = (tool, args) => {
1026
+ if (exec.parentAuthorized === false)
1027
+ return 'no-match';
1028
+ return this.grants.decide(tool, args);
1029
+ };
1030
+ return decide(this.ctxFor(exec), { decide: (c) => decideRules(this.ruleset, c) }, grantResolver, this.autoAllowExtra);
1031
+ }
1032
+ /**
1033
+ * Rule-layer explanation for one would-be call: what the `permissions` chain
1034
+ * decides on its own, before P0 hard-deny, P1 session grants, the P3
1035
+ * classifier and the P4 ask ever get a say.
1036
+ *
1037
+ * This is the read-only half of a dry-run. It appends nothing to the audit
1038
+ * mirror, records no event, mints no grant and touches no learning store, so a
1039
+ * "rule test" panel may call it on every keystroke without leaving a trace in
1040
+ * the decision feed. It answers a narrower question than
1041
+ * {@link decideExecution} and says so: `ruleIndex` identifies a rule in the
1042
+ * chain, which only this layer can attribute.
1043
+ */
1044
+ explainRules(exec) {
1045
+ const decision = decideRules(this.ruleset, this.ctxFor(exec));
1046
+ return {
1047
+ action: decision.action,
1048
+ reason: decision.reason,
1049
+ ruleIndex: decision.ruleIndex,
1050
+ rule: decision.ruleIndex === undefined ? undefined : findRuleByIndex(this.ruleset, decision.ruleIndex),
1051
+ defaultAction: this.ruleset.defaultAction,
1052
+ };
1053
+ }
901
1054
  /**
902
1055
  * Apply the Permissive tier to a raw decision. Hard-deny and minted-grant
903
1056
  * outcomes are never touched (fail-closed). `alwaysConfirm` escalates a
@@ -1004,13 +1157,22 @@ export class PermGateRuntime {
1004
1157
  if (risk.kind === 'risky') {
1005
1158
  if (risk.category !== 'neutral') {
1006
1159
  // Hard category (deletion / credential / remote / system / bulk) or an
1007
- // off-protocol output graded hard: auto-deny. The operation is clearly
1008
- // dangerous or sensitive — no popup, no learning, just block.
1009
- const reason = `${decision.reason} [llm-assist risky:${risk.category} → auto-deny]`;
1010
- this.audit.append(makeEntry({ callId: randomId(), tool: exec.name, outcome: 'deny', source: 'classifier', reason, at: Date.now() }));
1011
- this.recordEvent(exec, 'deny', reason, { risk: risk.category, verdict: 'llm-deny', category: risk.category });
1012
- this.untrackAsk(exec);
1013
- return { kind: 'deny', reason };
1160
+ // off-protocol output graded hard: KEEP THE ASK.
1161
+ //
1162
+ // Denying here would make a probabilistic verdict the source of an
1163
+ // unappealable block: the gate would be "sure enough" to refuse on the
1164
+ // model's word alone, with no popup to appeal to and no grant to retry
1165
+ // with — measured live on a benign `git commit -F …` the grader called
1166
+ // `remote`. Deny stays with the deterministic layers (P0 hard-deny, the
1167
+ // deny-keyword blacklist, explicit `deny:` rules); everything the
1168
+ // classifier merely *suspects* is negotiated with the human instead.
1169
+ //
1170
+ // Hard categories stay distinct from `neutral` in one way that matters:
1171
+ // they are NEVER learnable, so repeated confirmations cannot sediment
1172
+ // them into an auto-allow.
1173
+ const hardReason = `${decision.reason} [llm-assist risky:${risk.category} → ask]`;
1174
+ this.trackAsk(exec, hardReason, undefined);
1175
+ return { kind: 'ask', reason: hardReason };
1014
1176
  }
1015
1177
  const learning = this.liveRiskLearning();
1016
1178
  const fp = operationFingerprint(exec.name, exec.arguments ?? {}, exec.commandText);
@@ -1249,27 +1411,44 @@ export class PermGateRuntime {
1249
1411
  }
1250
1412
  /**
1251
1413
  * Grant "allow every occurrence of this command" (the always-confirm panel's
1252
- * second extended allow button): persist the command into the rules file's
1253
- * `allow` whitelist and reload. Returns the reload result.
1414
+ * second extended allow button): persist the command into the rules' `allow`
1415
+ * whitelist and reload. Prefers the settings-backed writer (the namespace's
1416
+ * watch triggers the reload once the write lands); falls back to the rules
1417
+ * file when no settings scope is wired. Returns the write verdict.
1254
1418
  */
1255
1419
  approveAllowEverywhere(commandWord, reason = 'permissive allow-everywhere') {
1420
+ const writer = this.options.allowlistWriter;
1421
+ if (writer !== undefined && writer.append(commandWord, reason))
1422
+ return true;
1256
1423
  if (!this.options.rulesFile)
1257
1424
  return false;
1258
1425
  if (!appendAllowCommand(this.options.rulesFile, commandWord, reason))
1259
1426
  return false;
1260
1427
  return this.reload();
1261
1428
  }
1262
- /** Read-only view of the current allow-list command patterns (whitelist). */
1429
+ /**
1430
+ * Read-only view of the current allow-list command patterns (whitelist).
1431
+ * Reads the settings document when the namespace is configured, else the
1432
+ * rules file.
1433
+ */
1263
1434
  allowlist() {
1435
+ const doc = this.options.readRulesDocument?.();
1436
+ if (doc !== undefined)
1437
+ return listAllowFromRulesDoc(doc);
1264
1438
  if (!this.options.rulesFile)
1265
1439
  return [];
1266
1440
  return listAllowCommands(this.options.rulesFile);
1267
1441
  }
1268
1442
  /**
1269
1443
  * Replace the whitelist with exactly the given command patterns and reload.
1270
- * Returns the reload result (false when no rulesFile or the write failed).
1444
+ * Prefers the settings-backed writer; falls back to the rules file when no
1445
+ * settings scope is wired. Returns the write verdict (false when neither
1446
+ * sink is available or the write failed).
1271
1447
  */
1272
1448
  setAllowlist(patterns) {
1449
+ const writer = this.options.allowlistWriter;
1450
+ if (writer !== undefined && writer.replace(patterns, 'permissive allowlist'))
1451
+ return true;
1273
1452
  if (!this.options.rulesFile)
1274
1453
  return false;
1275
1454
  if (!replaceAllowCommands(this.options.rulesFile, patterns))
package/lib/shell.d.ts CHANGED
@@ -19,6 +19,20 @@ export interface SimpleCommand {
19
19
  readonly redirects: readonly string[];
20
20
  }
21
21
  export type CmdFlag = 'recursive' | 'force';
22
+ /**
23
+ * Reconstruct a simple command's full argv as one string.
24
+ *
25
+ * `SimpleCommand` splits the command into `command` + `args`, so any consumer
26
+ * that matches on a *whole command line* (the `argv.pipeline` dimension, the
27
+ * `branch` dimension's git parser) needs the parts joined back together —
28
+ * feeding only `command` loses every subcommand and flag (`git push --force
29
+ * origin main` degrades to `git`, which parses as "not a git command").
30
+ *
31
+ * Redirect targets are appended so a pattern naming the redirect target can
32
+ * match, and tokens containing whitespace are quoted so the reconstruction
33
+ * stays tokenizable.
34
+ */
35
+ export declare function commandArgv(cmd: SimpleCommand): string;
22
36
  /**
23
37
  * Tokenize a shell string into tokens, honoring single/double quotes and a few
24
38
  * common escapes. Missing/EOF quotes throw so the caller can route an
package/lib/shell.js CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Reconstruct a simple command's full argv as one string.
3
+ *
4
+ * `SimpleCommand` splits the command into `command` + `args`, so any consumer
5
+ * that matches on a *whole command line* (the `argv.pipeline` dimension, the
6
+ * `branch` dimension's git parser) needs the parts joined back together —
7
+ * feeding only `command` loses every subcommand and flag (`git push --force
8
+ * origin main` degrades to `git`, which parses as "not a git command").
9
+ *
10
+ * Redirect targets are appended so a pattern naming the redirect target can
11
+ * match, and tokens containing whitespace are quoted so the reconstruction
12
+ * stays tokenizable.
13
+ */
14
+ export function commandArgv(cmd) {
15
+ const quote = (token) => (/\s/.test(token) ? JSON.stringify(token) : token);
16
+ return [cmd.command, ...cmd.args, ...cmd.redirects].map(quote).join(' ');
17
+ }
1
18
  const WRAPPER_WORDS = new Set(['env', 'sudo', 'command', 'nohup', 'xargs']);
2
19
  const COMMAND_WORDS = new Set(['sh', 'bash', 'zsh', 'dash', 'busybox', 'pwsh', 'powershell', 'cmd']);
3
20
  /**
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "2.1.2",
3
+ "version": "2.6.1",
4
+ "publishConfig": {
5
+ "registry": "https://registry.npmjs.org",
6
+ "access": "public",
7
+ "tag": "latest"
8
+ },
4
9
  "description": "DSH permission-gate for the DeepSeek Harness 0.1.2+ line: a single self-sufficient, deterministic-first, fail-closed gate covering P0 hard-deny -> P1 session grant -> P2 static rule (allow/deny) chain -> P3 optional LLM semantic classifier -> P4 ask, with command whitelist/blacklist.",
5
10
  "type": "module",
6
11
  "files": [
@@ -9,6 +14,7 @@
9
14
  "cordis.patch.yml",
10
15
  "dsh.plugin.json",
11
16
  "assets",
17
+ "docs",
12
18
  "examples",
13
19
  "README.md",
14
20
  "README.zh.md",
@@ -64,7 +70,17 @@
64
70
  "dsh": ">=0.1.2-alpha.1 <0.2.0-0"
65
71
  },
66
72
  "peerDependencies": {
67
- "@deepseek-ai/cordis": "^4.0.1"
73
+ "@deepseek-ai/cordis": "^4.0.1",
74
+ "@deepseek-ai/dsh-client-locale": ">=0.1.2-alpha.1 <0.2.0-0",
75
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.2-alpha.1 <0.2.0-0",
76
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.2-alpha.1 <0.2.0-0",
77
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.2-alpha.1 <0.2.0-0"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "@deepseek-ai/dsh-client-locale": { "optional": true },
81
+ "@deepseek-ai/dsh-client-ui-settings": { "optional": true },
82
+ "@deepseek-ai/dsh-client-ui-renderer": { "optional": true },
83
+ "@deepseek-ai/dsh-client-ui-conversation": { "optional": true }
68
84
  },
69
85
  "dependencies": {
70
86
  "@deepseek-ai/schemastery": "^3.18.0",
@@ -97,6 +113,7 @@
97
113
  "patch:glyph:check": "node scripts/patch-permission-glyph.mjs --check",
98
114
  "verify:line": "node scripts/verify-line.mjs new",
99
115
  "verify:lines": "node scripts/verify-line.mjs both",
116
+ "check:line": "node scripts/sync-0.1.5-line.mjs check",
100
117
  "release:latest": "npm publish --tag latest",
101
118
  "release:2x": "npm publish --tag 2.x",
102
119
  "release:legacy": "npm publish --tag legacy"