@matamt-dbg/dsh-studio 0.2.2 → 0.2.8

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 (6) hide show
  1. package/README.md +9 -4
  2. package/client.js +499 -219
  3. package/index.js +340 -64
  4. package/package.json +1 -1
  5. package/policy.js +135 -12
  6. package/roles.js +25 -3
package/index.js CHANGED
@@ -56,12 +56,14 @@ var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
56
56
  function currentPluginName(name) {
57
57
  return name === LEGACY_PACKAGE_NAME || name.startsWith(`${LEGACY_PACKAGE_NAME}/`) ? PACKAGE_NAME + name.slice(LEGACY_PACKAGE_NAME.length) : name;
58
58
  }
59
+ var STUDIO_VERSION = "0.2.8";
59
60
 
60
61
  // src/delegation.ts
61
62
  var ROLE_TOOL = "subagent_role";
62
63
  var LEGACY_ROLE_TOOL = "studio_role";
63
64
  var GENERIC_DELEGATION_TOOLS = ["subagent", "subagent_fork", "list_subagent_models"];
64
65
  var SUBAGENT_PLUGIN = "@deepseek-ai/dsh-tool-subagent";
66
+ var isDelegationPlugin = (name) => name === SUBAGENT_PLUGIN || name === ROLE_PLUGIN;
65
67
  var WITHDRAWN_TOOLS = ["task_run"];
66
68
  function delegationTools(tools, hasRoles = false) {
67
69
  const delegates = hasRoles || tools.some((name) => name === ROLE_TOOL || name === LEGACY_ROLE_TOOL || GENERIC_DELEGATION_TOOLS.includes(name));
@@ -69,7 +71,7 @@ function delegationTools(tools, hasRoles = false) {
69
71
  return delegates ? [...rest, ROLE_TOOL] : rest;
70
72
  }
71
73
  function withoutDelegationPlugins(rows) {
72
- return rows.filter((row) => row.name !== SUBAGENT_PLUGIN && row.name !== ROLE_PLUGIN).map((row) => row.group && Array.isArray(row.config) ? { ...row, config: withoutDelegationPlugins(row.config) } : row);
74
+ return rows.filter((row) => !isDelegationPlugin(row.name)).map((row) => row.group && Array.isArray(row.config) ? { ...row, config: withoutDelegationPlugins(row.config) } : row);
73
75
  }
74
76
  function renameLegacyRoleKey(entries) {
75
77
  if (!entries || !Object.hasOwn(entries, LEGACY_ROLE_TOOL)) return entries;
@@ -91,7 +93,31 @@ function migratePolicyConfig(config) {
91
93
  // src/optimized-preset.ts
92
94
  var OPTIMIZED_PRESET_ID = "studio-optimized";
93
95
  var BUILTIN_TOOL_PRESETS = ["standard", "ptc", "minimal", "cordis"];
94
- var excluded = /* @__PURE__ */ new Set(["get_goal", "create_goal", "update_goal", "todo_write", "task_run", "cordis_inspect_list", "cordis_inspect_query", "plugin_manager"]);
96
+ var OPTIMIZED_TOOLS = [
97
+ "read_batch",
98
+ "read",
99
+ "write",
100
+ "edit",
101
+ "glob",
102
+ "grep",
103
+ "job_output",
104
+ "job_list",
105
+ "job_kill",
106
+ "skill",
107
+ "ask_user_question",
108
+ "web_search",
109
+ "web_fetch",
110
+ "present",
111
+ "bash",
112
+ "read_image",
113
+ "exit_plan_mode",
114
+ "send_message",
115
+ "interrupt_agent",
116
+ "list_agents",
117
+ "workflow",
118
+ "subagent_role"
119
+ ];
120
+ var ALLOWED_TOOLS = /* @__PURE__ */ new Set([...OPTIMIZED_TOOLS, ...GENERIC_DELEGATION_TOOLS, LEGACY_ROLE_TOOL]);
95
121
  var OPTIMIZED_PROMPT = `Use glob to discover paths, grep to search contents, and read to inspect files before editing. Prefer targeted edits.
96
122
  Batch independent read-only inspections with read_batch. Start independent tool calls together; keep calls that depend on earlier results sequential. Do not guess unknown arguments.
97
123
  Wait for required work to finish before validating it. Use background execution only for independent work or intentionally long-lived services. Do not duplicate running work or busy-poll it.
@@ -99,7 +125,7 @@ Track every job you start. Before reporting completion, collect all relevant res
99
125
  Delegate self-contained independent work when useful and collect the result before relying on it.
100
126
  Keep changes within the requested scope and preserve user work. Use the smallest sufficient checks for the requested outcome and risk, then stop when the work is complete.`;
101
127
  function optimizedTemplate(tools) {
102
- const selected = delegationTools(tools.filter((name) => !excluded.has(name)));
128
+ const selected = delegationTools(tools.filter((name) => ALLOWED_TOOLS.has(name)));
103
129
  const request = {
104
130
  preset: "studio-master",
105
131
  providers: ["@deepseek-ai/dsh-persona"],
@@ -358,7 +384,7 @@ var MasterExtensions = class {
358
384
  };
359
385
 
360
386
  // src/host/shell.ts
361
- import { dirname as dirname3, join as join2 } from "node:path";
387
+ import { dirname as dirname3, join as join3 } from "node:path";
362
388
  import { randomUUID as randomUUID4 } from "node:crypto";
363
389
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
364
390
 
@@ -378,7 +404,8 @@ var toolPromptSections = {
378
404
  web_search: ["tool:web_search"],
379
405
  get_goal: ["tool:goal"],
380
406
  create_goal: ["tool:goal"],
381
- update_goal: ["tool:goal"]
407
+ update_goal: ["tool:goal"],
408
+ workflow: ["tool:workflow"]
382
409
  };
383
410
 
384
411
  // src/preset-catalog.ts
@@ -491,6 +518,8 @@ function parsePreviewRequest(value) {
491
518
  if (row.providerConfigs !== void 0 && (!record4(row.providerConfigs) || Object.values(row.providerConfigs).some((v) => !record4(v)))) throw new Error("Invalid provider configs");
492
519
  if (row.configOverrides !== void 0) validateConfigOverrides(row.configOverrides);
493
520
  if (row.execution !== void 0) validateExecutionDefaults(row.execution);
521
+ const filter = row.subagentToolFilter;
522
+ if (filter !== void 0 && (!record4(filter) || !strings(filter.deny) || filter.deny.some((name) => !name))) throw new Error("Invalid subagent tool filter");
494
523
  return {
495
524
  ...row.configOverrides !== void 0 ? { configOverrides: row.configOverrides } : {},
496
525
  ...row.execution !== void 0 ? { execution: row.execution } : {},
@@ -502,7 +531,8 @@ function parsePreviewRequest(value) {
502
531
  ...row.persona !== void 0 ? { persona: row.persona } : {},
503
532
  ...row.sections !== void 0 ? { sections: row.sections } : {},
504
533
  ...row.toolOverrides !== void 0 ? { toolOverrides: row.toolOverrides } : {},
505
- ...row.providerConfigs !== void 0 ? { providerConfigs: row.providerConfigs } : {}
534
+ ...row.providerConfigs !== void 0 ? { providerConfigs: row.providerConfigs } : {},
535
+ ...filter !== void 0 && filter.deny.length ? { subagentToolFilter: { deny: [...new Set(filter.deny)] } } : {}
506
536
  };
507
537
  }
508
538
  function promptProviders(rows) {
@@ -537,12 +567,26 @@ function parseComposition(content) {
537
567
  function findProviders(rows) {
538
568
  return rows.flatMap((row) => row.disabled === true ? [] : row.group && Array.isArray(row.config) ? findProviders(row.config) : promptProviderModules.includes(row.name) ? [row] : []);
539
569
  }
570
+ function normalizeRetention(config, overrides) {
571
+ if (overrides?.retainTokens !== void 0 && overrides.retainRatio !== void 0) {
572
+ throw new Error("Compaction retainRatio and retainTokens are mutually exclusive");
573
+ }
574
+ if (overrides?.retainTokens !== void 0) delete config.retainRatio;
575
+ if (overrides?.retainRatio !== void 0) delete config.retainTokens;
576
+ }
540
577
  async function compositionPlan(ctx, input) {
541
578
  const request = parsePreviewRequest(input);
542
- const base = request.preset === MASTER_PRESET_ID ? MASTER_PRESET_ID : "minimal";
579
+ const base = request.preset;
543
580
  if (request.configOverrides && base !== MASTER_PRESET_ID) throw new Error("Config overrides require Studio Master");
544
- const minimal = applyConfigOverrides(parseComposition((await ctx.agentPresets.readDocument(base)).content), request.configOverrides ?? {});
545
- const available = new Map(findProviders(minimal).map((row) => [row.name, row]));
581
+ const source = applyConfigOverrides(parseComposition((await ctx.agentPresets.readDocument(base)).content), request.configOverrides ?? {});
582
+ if (request.configOverrides && Object.keys(request.configOverrides).length) {
583
+ for (const { id, row } of configEntries(source)) {
584
+ if (row.name === "@deepseek-ai/dsh-compaction-basic") {
585
+ normalizeRetention(row.config, request.configOverrides[id]);
586
+ }
587
+ }
588
+ }
589
+ const available = new Map(findProviders(source).map((row) => [row.name, row]));
546
590
  const presets = await ctx.agentPresets.list();
547
591
  for (const preset of presets) {
548
592
  if (base === MASTER_PRESET_ID || !["minimal", "standard", "ptc", "cordis"].includes(preset.id)) continue;
@@ -572,33 +616,21 @@ async function compositionPlan(ctx, input) {
572
616
  if (request.persona !== void 0) config.prefix = request.persona;
573
617
  }
574
618
  if (name === "@deepseek-ai/dsh-compaction-basic") {
575
- if (overrides?.retainTokens !== void 0 && overrides.retainRatio === void 0) delete config.retainRatio;
576
- if (overrides?.retainRatio !== void 0 && overrides.retainTokens === void 0) delete config.retainTokens;
619
+ normalizeRetention(config, overrides);
577
620
  }
578
621
  return { ...row, id: `studio-provider-${index}`, config };
579
622
  });
580
623
  if (request.persona !== void 0 && !request.providers.includes("@deepseek-ai/dsh-persona")) throw new Error("Select persona before editing its prefix");
581
624
  for (const name of Object.keys(request.providerConfigs ?? {})) if (!request.providers.includes(name)) throw new Error(`Unselected provider override: ${name}`);
582
- const globalNames = new Set(ctx.tools.schemas().map((tool) => tool.name));
583
- const missing = request.tools.filter((name) => !globalNames.has(name));
584
- if (missing.length || base === MASTER_PRESET_ID) {
585
- let source;
586
- const candidates = base === MASTER_PRESET_ID ? [MASTER_PRESET_ID] : ["minimal", ...presets.map((p) => p.id).filter((id) => id !== "minimal" && !id.startsWith("studio-"))];
587
- for (const id of candidates) {
588
- let lease;
589
- try {
590
- lease = await ctx.agentPresets.acquireScope(id);
591
- const names2 = new Set(ctx.tools.schemas(lease.key).map((tool) => tool.name));
592
- if (missing.every((name) => names2.has(name))) {
593
- source = id === base ? minimal : parseComposition((await ctx.agentPresets.readDocument(id)).content);
594
- break;
595
- }
596
- } catch {
597
- } finally {
598
- await lease?.[Symbol.asyncDispose]();
599
- }
625
+ if (request.tools.length || base === MASTER_PRESET_ID) {
626
+ const lease = await ctx.agentPresets.acquireScope(base);
627
+ try {
628
+ const names2 = new Set(ctx.tools.schemas(lease.key).map((tool) => tool.name));
629
+ const missing = request.tools.filter((name) => !names2.has(name));
630
+ if (missing.length) throw new Error(`Requested preset ${base} does not supply tools: ${missing.join(", ")}. Tools from other source scopes cannot be substituted safely.`);
631
+ } finally {
632
+ await lease[Symbol.asyncDispose]();
600
633
  }
601
- if (!source) throw new Error(`No compatible installed tool composition supplies: ${missing.join(", ")}. Tools from incompatible source scopes cannot be combined safely.`);
602
634
  plugins.push(...selectPromptProviders(source, []));
603
635
  }
604
636
  const compaction = plugins.filter((row) => ["@deepseek-ai/dsh-compaction-basic", "@deepseek-ai/dsh-compaction-tool-result-pruner"].includes(row.name));
@@ -608,7 +640,7 @@ async function compositionPlan(ctx, input) {
608
640
  }
609
641
 
610
642
  // src/host/preset-policy.ts
611
- import { scopeOf } from "@deepseek-ai/dsh-scope";
643
+ import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
612
644
  var POLICY_SECTION_ORDERS = Object.freeze({
613
645
  "tool:jobs": "TOOL_JOBS",
614
646
  "tool:bash": "TOOL_BASH",
@@ -620,7 +652,8 @@ var POLICY_SECTION_ORDERS = Object.freeze({
620
652
  "tool:grep": "TOOL_GREP",
621
653
  "tool:web_fetch": "TOOL_WEB_FETCH",
622
654
  "tool:web_search": "TOOL_WEB_SEARCH",
623
- "tool:goal": "TOOL_GOAL"
655
+ "tool:goal": "TOOL_GOAL",
656
+ "tool:workflow": "TOOL_WORKFLOW"
624
657
  });
625
658
  var own2 = (object3, key) => Object.prototype.hasOwnProperty.call(object3, key);
626
659
  var record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
@@ -638,6 +671,11 @@ function validatePolicy(config) {
638
671
  for (const [name, text] of Object.entries(config.sections)) {
639
672
  if (!own2(POLICY_SECTION_ORDERS, name) || typeof text !== "string") throw new Error(`Unsupported policy section: ${name}`);
640
673
  }
674
+ if (config.childToolFilter !== void 0) {
675
+ const deny = record3(config.childToolFilter) ? config.childToolFilter.deny : void 0;
676
+ if (!Array.isArray(deny) || deny.some((name) => typeof name !== "string" || !name)) throw new Error("Invalid subagent tool filter");
677
+ for (const name of deny) if (!config.tools.includes(name)) throw new Error(`Subagent tool filter requires selected tool: ${name}`);
678
+ }
641
679
  }
642
680
  function patchParameter(parameters, path, description) {
643
681
  const segments = path.split(".");
@@ -651,11 +689,20 @@ function patchParameter(parameters, path, description) {
651
689
  function installPolicy(ctx, scopeKey, config) {
652
690
  validatePolicy(config);
653
691
  if (scopeOf(ctx) !== scopeKey) throw new Error("Studio policy requires its owning scoped context");
692
+ if (config.tools.includes("run_code") || ctx.tools.get("run_code", scopeKey) !== void 0) {
693
+ throw new Error("Studio policy does not support PTC presentation or the reserved run_code tool; configure this preset with native tool presentation");
694
+ }
654
695
  const missing = config.tools.filter((name) => ctx.tools.get(name, scopeKey) === void 0);
655
- if (missing.length) console.warn(`Studio policy: ${missing.length} selected tool(s) unavailable on this host, skipped: ${missing.join(", ")}`);
656
- const available = config.tools.filter((name) => !missing.includes(name));
657
- const selected = new Set(available);
658
- const definitions = available.map((name) => {
696
+ if (missing.length) throw new Error(`Studio policy selected tools unavailable: ${missing.join(", ")}`);
697
+ const restoreProbe = ctx.tools.restrict({ allow: [] });
698
+ try {
699
+ const local = ctx.tools.schemas(scopeKey);
700
+ if (local.length) throw new Error(`Studio policy does not support own-scope tools: ${local.map((tool) => tool.name).join(", ")}; register providers in an ancestor preset scope. Public API cannot hide late own-scope schemas; dynamic own-scope providers are unsupported`);
701
+ } finally {
702
+ restoreProbe();
703
+ }
704
+ const selected = new Set(config.tools);
705
+ const definitions = config.tools.map((name) => {
659
706
  const definition = ctx.tools.get(name, scopeKey);
660
707
  const override = config.toolOverrides[name];
661
708
  const defaults = config.execution?.defaults[name];
@@ -689,9 +736,7 @@ function installPolicy(ctx, scopeKey, config) {
689
736
  for (const release of disposers.splice(0).reverse()) release();
690
737
  };
691
738
  try {
692
- disposers.push(ctx.tools.restrict({ allow: available }));
693
- const excess = ctx.tools.schemas(scopeKey).filter((tool) => !selected.has(tool.name));
694
- if (excess.length) throw new Error(`Unselected scoped tools must be removed from preset plugins: ${excess.map((tool) => tool.name).join(", ")}`);
739
+ disposers.push(ctx.tools.restrict({ allow: [...selected] }));
695
740
  disposers.push(ctx.tools.guard((execution) => selected.has(execution.name) ? void 0 : "Tool excluded by Studio preset policy"));
696
741
  for (const definition of definitions) disposers.push(ctx.tools.register(definition));
697
742
  for (const [name, text] of Object.entries(config.sections)) disposers.push(ctx.systemPrompt.section({ name, order: ctx.systemPrompt.getSectionOrder(POLICY_SECTION_ORDERS[name]), text, interpolate: false }));
@@ -758,6 +803,7 @@ function executionCapabilities(tools) {
758
803
  }
759
804
 
760
805
  // src/host/preset-store.ts
806
+ import { createScope as createScope2 } from "@deepseek-ai/dsh-scope";
761
807
  import { randomUUID as randomUUID3 } from "node:crypto";
762
808
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rename as rename2 } from "node:fs/promises";
763
809
  import { dirname as dirname2, join } from "node:path";
@@ -787,11 +833,27 @@ function parseRoles(input) {
787
833
  if (!/^[a-zA-Z0-9_-]+$/.test(reasoning)) throw new Error("Role reasoning must be inherit or an effort identifier");
788
834
  if (spawn !== "fresh" && spawn !== "fork") throw new Error("Role spawn must be fresh or fork");
789
835
  seen.add(role);
790
- return { name, role, model, reasoning, spawn, persona, ..."overrides" in value ? { overrides: structuredClone(value.overrides) } : {} };
836
+ const filter = value.toolFilter;
837
+ if (filter !== void 0 && (!object2(filter) || !Array.isArray(filter.deny) || filter.deny.some((name2) => typeof name2 !== "string" || !name2) || filter.mode !== void 0 && filter.mode !== "extend" && filter.mode !== "replace")) throw new Error("Invalid role tool filter");
838
+ return {
839
+ name,
840
+ role,
841
+ model,
842
+ reasoning,
843
+ spawn,
844
+ persona,
845
+ ...filter !== void 0 ? { toolFilter: { mode: filter.mode ?? "replace", deny: [...new Set(filter.deny)] } } : {},
846
+ ..."overrides" in value ? { overrides: structuredClone(value.overrides) } : {}
847
+ };
791
848
  });
792
849
  }
793
850
  function executableRoles(roles) {
794
- return roles.map(({ name, role, model, reasoning, spawn, persona }) => ({ name, role, model, reasoning, spawn, persona }));
851
+ return roles.map(({ name, role, model, reasoning, spawn, persona, toolFilter }) => ({ name, role, model, reasoning, spawn, persona, ...toolFilter ? { toolFilter } : {} }));
852
+ }
853
+ function roleToolFilter(role, presetDefault = []) {
854
+ const own3 = role?.toolFilter;
855
+ if (!own3) return [...presetDefault];
856
+ return own3.mode === "replace" ? [...own3.deny] : [.../* @__PURE__ */ new Set([...presetDefault, ...own3.deny])];
795
857
  }
796
858
 
797
859
  // src/host/role-validation.ts
@@ -821,7 +883,7 @@ async function validateRoleModel(llm, role, signal) {
821
883
 
822
884
  // src/host/preset-store.ts
823
885
  var owned = (id) => id === OPTIMIZED_PRESET_ID || /^studio-user-[a-f0-9-]{36}$/.test(id);
824
- var warning = "Work convenience settings are saved but not applied yet. Legacy role overrides are stored only; roles apply only their supported settings.";
886
+ var retiredLegacyWarning = "Work convenience settings are saved but not applied yet. Legacy role overrides are stored only; roles apply only their supported settings.";
825
887
  function migrateLegacyNames(row) {
826
888
  const request = row.request;
827
889
  if (request.toolOverrides) {
@@ -864,6 +926,7 @@ var PresetStore = class {
864
926
  if (!owned(row.id)) throw new Error("Invalid Studio preset ownership");
865
927
  parsePreviewRequest(row.request);
866
928
  migrateLegacyNames(row);
929
+ if (row.applyWarning === retiredLegacyWarning) delete row.applyWarning;
867
930
  }
868
931
  } catch (error) {
869
932
  if (error.code !== "ENOENT") throw error;
@@ -879,10 +942,32 @@ var PresetStore = class {
879
942
  })();
880
943
  }
881
944
  async mount(id, definition) {
882
- const dispose = await this.ctx.agentPresets.register({ id, name: definition.name, description: definition.description, plugins: parseComposition(definition.plugins) });
945
+ const plugins = parseComposition(definition.plugins);
946
+ const dispose = await this.ctx.agentPresets.register({ id, name: definition.name, description: definition.description, plugins });
883
947
  try {
884
948
  const preset = await this.ctx.agentPresets.resolve(id);
885
949
  if (preset.broken) throw new Error(preset.broken);
950
+ const policies = (rows) => rows.flatMap((row) => row.disabled === true ? [] : row.group && Array.isArray(row.config) ? policies(row.config) : row.name === POLICY_PLUGIN ? [row.config] : []);
951
+ const configs = policies(plugins);
952
+ if (configs.length) {
953
+ const lease = await this.ctx.agentPresets.acquireScope(id);
954
+ try {
955
+ const key = {};
956
+ const child = createScope2(this.ctx, key, { parent: lease.key });
957
+ const releases = [];
958
+ try {
959
+ for (const config of configs) releases.push(installPolicy(child.ctx, key, config));
960
+ } finally {
961
+ try {
962
+ for (const release of releases.reverse()) release();
963
+ } finally {
964
+ await child.dispose();
965
+ }
966
+ }
967
+ } finally {
968
+ await lease[Symbol.asyncDispose]();
969
+ }
970
+ }
886
971
  this.disposers.set(id, dispose);
887
972
  } catch (error) {
888
973
  await dispose();
@@ -909,27 +994,72 @@ var PresetStore = class {
909
994
  const { applied, ...value } = row;
910
995
  return structuredClone({ ...value, ...row.id === OPTIMIZED_PRESET_ID ? { template: true, nonDeletable: true } : {} });
911
996
  }
912
- /** Seed once. Later startups retain user edits and explicit unapplied state. */
997
+ /** Compose the Optimized applied snapshot: the template request over the current base composition. */
998
+ optimizedApplied(request, plugins) {
999
+ const delegates = request.tools.includes(ROLE_TOOL);
1000
+ const base = applyConfigOverrides(plugins, request.configOverrides ?? {});
1001
+ return dump([...delegates ? [...withoutDelegationPlugins(base), { id: "studio-roles", name: ROLE_PLUGIN, config: { roles: [] } }] : base, { id: "studio-optimized-policy", name: POLICY_PLUGIN, config: { tools: request.tools, toolOverrides: request.toolOverrides, sections: {}, prompt: request.prompt, execution: request.execution } }], { schema: entryListSchema2, noRefs: true });
1002
+ }
1003
+ /**
1004
+ * Seed the built-in on first run, then keep it aligned with the shipped template. Optimized is
1005
+ * locked against editing, so its stored request carries no user content — only the applied or
1006
+ * unapplied choice is the user's, and that survives. Without the alignment a profile plugin that
1007
+ * registers a new tool would enlarge the preset forever after its first seed.
1008
+ */
913
1009
  ensureOptimized(tools, plugins) {
914
1010
  return this.serial(async () => {
915
- const existing = this.rows.find((row2) => row2.id === OPTIMIZED_PRESET_ID);
916
- if (existing) return this.public(existing);
917
1011
  const template = optimizedTemplate(tools);
918
- const request = template.request;
919
- const delegates = request.tools.includes(ROLE_TOOL);
920
- const base = applyConfigOverrides(plugins, request.configOverrides ?? {});
921
- const applied = { name: template.name, description: template.description, plugins: dump([...delegates ? [...withoutDelegationPlugins(base), { id: "studio-roles", name: ROLE_PLUGIN, config: { roles: [] } }] : base, { id: "studio-optimized-policy", name: POLICY_PLUGIN, config: { tools: request.tools, toolOverrides: request.toolOverrides, sections: {}, prompt: request.prompt, execution: request.execution } }], { schema: entryListSchema2, noRefs: true }) };
1012
+ const existing = this.rows.find((row) => row.id === OPTIMIZED_PRESET_ID);
1013
+ if (existing && JSON.stringify(existing.request) === JSON.stringify(template.request)) return this.public(existing);
922
1014
  const now = (/* @__PURE__ */ new Date()).toISOString();
923
- const row = { id: template.id, name: template.name, description: template.description, request: template.request, conveniences: {}, agents: [], updatedAt: now, appliedAt: now, status: "applied", applied, ...template.applyWarning ? { applyWarning: template.applyWarning } : {} };
924
- await this.mount(row.id, applied);
1015
+ const buildApplied = () => ({ name: template.name, description: template.description, plugins: this.optimizedApplied(template.request, plugins) });
1016
+ if (!existing) {
1017
+ const applied = buildApplied();
1018
+ const row = { id: template.id, name: template.name, description: template.description, request: template.request, conveniences: {}, agents: [], updatedAt: now, appliedAt: now, status: "applied", applied, ...template.applyWarning ? { applyWarning: template.applyWarning } : {} };
1019
+ await this.mount(row.id, applied);
1020
+ try {
1021
+ await this.persist([...this.rows, row]);
1022
+ } catch (error) {
1023
+ await this.disposers.get(row.id)?.();
1024
+ this.disposers.delete(row.id);
1025
+ throw error;
1026
+ }
1027
+ return this.public(row);
1028
+ }
1029
+ const refresh = { ...existing, name: template.name, description: template.description, request: template.request, updatedAt: now };
1030
+ if (template.applyWarning) refresh.applyWarning = template.applyWarning;
1031
+ else delete refresh.applyWarning;
1032
+ if (existing.applied) {
1033
+ const applied = buildApplied();
1034
+ await this.disposers.get(existing.id)?.();
1035
+ this.disposers.delete(existing.id);
1036
+ try {
1037
+ await this.mount(existing.id, applied);
1038
+ } catch (error) {
1039
+ try {
1040
+ await this.mount(existing.id, existing.applied);
1041
+ } catch {
1042
+ }
1043
+ throw error;
1044
+ }
1045
+ refresh.applied = applied;
1046
+ refresh.appliedAt = now;
1047
+ refresh.status = "applied";
1048
+ }
925
1049
  try {
926
- await this.persist([...this.rows, row]);
1050
+ await this.persist(this.rows.map((row) => row.id === existing.id ? refresh : row));
927
1051
  } catch (error) {
928
- await this.disposers.get(row.id)?.();
929
- this.disposers.delete(row.id);
1052
+ await this.disposers.get(existing.id)?.();
1053
+ this.disposers.delete(existing.id);
1054
+ if (existing.applied) {
1055
+ try {
1056
+ await this.mount(existing.id, existing.applied);
1057
+ } catch {
1058
+ }
1059
+ }
930
1060
  throw error;
931
1061
  }
932
- return this.public(row);
1062
+ return this.public(refresh);
933
1063
  });
934
1064
  }
935
1065
  list() {
@@ -974,21 +1104,25 @@ var PresetStore = class {
974
1104
  for (const role of roles) await validateRoleModel(this.ctx.get("llm"), role);
975
1105
  const delegates = tools.includes(ROLE_TOOL);
976
1106
  const keep = (entries) => Object.fromEntries(Object.entries(entries).filter(([name]) => tools.includes(name)));
977
- const rolePlugins = delegates ? [{ id: "studio-roles", name: ROLE_PLUGIN, config: { roles } }] : [];
1107
+ const presetHidden = old.request.subagentToolFilter?.deny ?? [];
1108
+ const lists = [presetHidden, ...roles.map((role) => roleToolFilter(role, presetHidden))];
1109
+ const unknownHidden = [...new Set(lists.flat().filter((name) => !tools.includes(name) || name === ROLE_TOOL))];
1110
+ if (delegates && unknownHidden.length) throw new Error(`Subagent Tool Filter may only list tools selected in this preset: ${unknownHidden.join(", ")}`);
1111
+ const hideable = delegates ? [...new Set(lists.flat())] : [];
1112
+ const childToolFilter = hideable.length ? { deny: hideable } : void 0;
1113
+ const rolePlugins = delegates ? [{ id: "studio-roles", name: ROLE_PLUGIN, config: { roles, ...presetHidden.length ? { toolFilter: { deny: [...presetHidden] } } : {} } }] : [];
978
1114
  const execution = request.execution && {
979
1115
  ...request.execution,
980
1116
  defaults: keep(request.execution.defaults),
981
1117
  ...request.execution.batch && !tools.includes(request.execution.batch.tool) ? { batch: void 0 } : {}
982
1118
  };
983
- const applied = { name: old.name, description: old.description, plugins: dump([...withoutDelegationPlugins(plugins), ...rolePlugins, { id: "studio-policy", name: POLICY_PLUGIN, config: { tools: [...new Set(tools)], toolOverrides: keep(request.toolOverrides ?? {}), sections: request.sections ?? {}, prompt: request.prompt, ...execution ? { execution } : {} } }], { schema: entryListSchema2, noRefs: true }) };
1119
+ const applied = { name: old.name, description: old.description, plugins: dump([...withoutDelegationPlugins(plugins), ...rolePlugins, { id: "studio-policy", name: POLICY_PLUGIN, config: { tools: [...new Set(tools)], toolOverrides: keep(request.toolOverrides ?? {}), sections: request.sections ?? {}, prompt: request.prompt, ...execution ? { execution } : {}, ...childToolFilter ? { childToolFilter } : {} } }], { schema: entryListSchema2, noRefs: true }) };
984
1120
  await this.disposers.get(id)?.();
985
1121
  this.disposers.delete(id);
986
1122
  try {
987
1123
  await this.mount(id, applied);
988
- const hasLegacySettings = old.conveniences != null && Object.keys(Object(old.conveniences)).length > 0 || Array.isArray(old.agents) && old.agents.some((role) => role && Object.hasOwn(role, "overrides"));
989
1124
  const row = { ...old, applied, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "applied" };
990
- if (hasLegacySettings) row.applyWarning = warning;
991
- else delete row.applyWarning;
1125
+ delete row.applyWarning;
992
1126
  await this.persist(this.rows.map((item) => item.id === id ? row : item));
993
1127
  return this.public(row);
994
1128
  } catch (error) {
@@ -1037,9 +1171,142 @@ var PresetStore = class {
1037
1171
  }
1038
1172
  };
1039
1173
 
1174
+ // src/host/studio-update.ts
1175
+ import { readFile as readFile3 } from "node:fs/promises";
1176
+ import { join as join2 } from "node:path";
1177
+ import { evaluatePluginCompatibility, getDshRuntimeVersion } from "@deepseek-ai/dsh-app-boot";
1178
+
1179
+ // src/update-check.ts
1180
+ var STABLE = /^(\d+)\.(\d+)\.(\d+)$/;
1181
+ function compareStable(a, b) {
1182
+ const x = STABLE.exec(a);
1183
+ const y = STABLE.exec(b);
1184
+ if (!x || !y) return Number(Boolean(x)) - Number(Boolean(y));
1185
+ for (let i = 1; i <= 3; i++) {
1186
+ const diff = Number(x[i]) - Number(y[i]);
1187
+ if (diff) return Math.sign(diff);
1188
+ }
1189
+ return 0;
1190
+ }
1191
+ function isNewer(candidate, current) {
1192
+ return STABLE.test(candidate) && compareStable(candidate, current.split("-")[0]) > 0;
1193
+ }
1194
+ function pickUpdate(document, current, compatible) {
1195
+ const latest = document["dist-tags"]?.latest;
1196
+ const candidates = Object.entries(document.versions ?? {}).filter(([version, manifest]) => isNewer(version, current) && !manifest.deprecated).sort(([a], [b]) => compareStable(b, a));
1197
+ const available = candidates.find(([, manifest]) => compatible(manifest))?.[0];
1198
+ const incompatibleLatest = latest !== void 0 && isNewer(latest, current) && latest !== available && candidates.some(([version]) => version === latest) ? latest : void 0;
1199
+ return { ...latest ? { latest } : {}, ...available ? { available } : {}, ...incompatibleLatest ? { incompatibleLatest } : {} };
1200
+ }
1201
+ function packumentUrl(registry, name) {
1202
+ return `${registry.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`;
1203
+ }
1204
+
1205
+ // src/host/studio-update.ts
1206
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1207
+ var CACHE_MS = 10 * 6e4;
1208
+ var StudioUpdater = class {
1209
+ constructor(ctx, fetcher = globalThis.fetch) {
1210
+ this.ctx = ctx;
1211
+ this.fetcher = fetcher;
1212
+ }
1213
+ cached;
1214
+ installing;
1215
+ manager() {
1216
+ return this.ctx.get("pluginManager");
1217
+ }
1218
+ /** Read the current status, reusing a recent registry answer unless `force`. */
1219
+ status(force = false) {
1220
+ if (!force && this.cached && Date.now() - this.cached.at < CACHE_MS) return this.cached.status;
1221
+ const status = this.check();
1222
+ this.cached = { at: Date.now(), status };
1223
+ void status.catch(() => {
1224
+ if (this.cached?.status === status) this.cached = void 0;
1225
+ });
1226
+ return status;
1227
+ }
1228
+ async check() {
1229
+ const result = { current: STUDIO_VERSION };
1230
+ let runtimeVersion;
1231
+ try {
1232
+ runtimeVersion = getDshRuntimeVersion();
1233
+ result.runtimeVersion = runtimeVersion;
1234
+ } catch {
1235
+ }
1236
+ const { source, installed } = await this.profilePackage();
1237
+ if (source) result.source = source;
1238
+ if (installed && installed !== STUDIO_VERSION) result.installed = installed;
1239
+ const baseline = installed && isNewer(installed, STUDIO_VERSION) ? installed : STUDIO_VERSION;
1240
+ const manager = this.manager();
1241
+ if (!manager) result.blocked = "no-plugin-manager";
1242
+ else if (!source) result.blocked = "not-profile-dependency";
1243
+ try {
1244
+ const registries = manager ? await manager.registries().catch(() => void 0) : void 0;
1245
+ const registry = registries?.registry ?? registries?.resolved ?? DEFAULT_REGISTRY;
1246
+ const response = await this.fetcher(packumentUrl(registry, PACKAGE_NAME), {
1247
+ headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" },
1248
+ signal: AbortSignal.timeout(15e3)
1249
+ });
1250
+ if (!response.ok) throw new Error(`Registry answered ${response.status}`);
1251
+ const document = await response.json();
1252
+ Object.assign(result, pickUpdate(document, baseline, (manifest) => {
1253
+ try {
1254
+ return evaluatePluginCompatibility({ name: PACKAGE_NAME, version: "0.0.0", ...manifest }, {}, runtimeVersion) === void 0;
1255
+ } catch {
1256
+ return false;
1257
+ }
1258
+ }));
1259
+ } catch (error) {
1260
+ result.error = error instanceof Error ? error.message : String(error);
1261
+ }
1262
+ return result;
1263
+ }
1264
+ /** How the profile declares Studio and which version its files hold now. */
1265
+ async profilePackage() {
1266
+ const dir = this.ctx.get("profileContext")?.dir ?? process.env.DSH_PROFILE_DIR;
1267
+ if (!dir) return {};
1268
+ const json2 = async (path) => {
1269
+ try {
1270
+ return JSON.parse(await readFile3(path, "utf8"));
1271
+ } catch {
1272
+ return void 0;
1273
+ }
1274
+ };
1275
+ const manifest = await json2(join2(dir, "package.json"));
1276
+ const source = manifest?.dependencies?.[PACKAGE_NAME];
1277
+ const installed = (await json2(join2(dir, "node_modules", ...PACKAGE_NAME.split("/"), "package.json")))?.version;
1278
+ return { ...source ? { source } : {}, ...typeof installed === "string" ? { installed } : {} };
1279
+ }
1280
+ /**
1281
+ * Install one exact version offered by {@link status}. The running Studio keeps its code until DSH restarts.
1282
+ * @param version Exact version; must equal the current offer so arbitrary specs never reach pnpm.
1283
+ */
1284
+ update(version) {
1285
+ if (this.installing) return this.installing;
1286
+ const run = (async () => {
1287
+ const status = await this.status();
1288
+ if (status.available !== version) throw new Error(`Version ${version} is not the offered update`);
1289
+ if (status.blocked) throw new Error(`Studio cannot update this install (${status.blocked})`);
1290
+ const manager = this.manager();
1291
+ const result = await manager.installBundle(`${PACKAGE_NAME}@${version}`, { enabled: true });
1292
+ const installed = result.version ?? version;
1293
+ if (result.application === "failed" || result.application === "cancelled") {
1294
+ const detail = result.error?.diagnostic ?? result.error?.code ?? result.packageResult?.output?.trim().split("\n").slice(-3).join("\n");
1295
+ return { status: result.application, version: installed, ...detail ? { message: detail } : {} };
1296
+ }
1297
+ this.cached = void 0;
1298
+ return { status: result.application === "applied" ? "applied" : "restart-required", version: installed };
1299
+ })();
1300
+ this.installing = run;
1301
+ return run.finally(() => {
1302
+ this.installing = void 0;
1303
+ });
1304
+ }
1305
+ };
1306
+
1040
1307
  // src/host/shell.ts
1041
- var _preview_dec, _saveExtensions_dec, _readExtensions_dec, _read_dec, _deletePreset_dec, _roleModels_dec, _unapplyPreset_dec, _applyPreset_dec, _savePreset_dec, _listPresets_dec, _a, _init;
1042
- var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec = [Remote], _savePreset_dec = [Remote], _applyPreset_dec = [Remote], _unapplyPreset_dec = [Remote], _roleModels_dec = [Remote], _deletePreset_dec = [Remote], _read_dec = [Remote], _readExtensions_dec = [Remote], _saveExtensions_dec = [Remote], _preview_dec = [Remote], _a) {
1308
+ var _preview_dec, _saveExtensions_dec, _readExtensions_dec, _read_dec, _deletePreset_dec, _updateStudio_dec, _updateStatus_dec, _roleModels_dec, _unapplyPreset_dec, _applyPreset_dec, _savePreset_dec, _listPresets_dec, _a, _init;
1309
+ var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec = [Remote], _savePreset_dec = [Remote], _applyPreset_dec = [Remote], _unapplyPreset_dec = [Remote], _roleModels_dec = [Remote], _updateStatus_dec = [Remote], _updateStudio_dec = [Remote], _deletePreset_dec = [Remote], _read_dec = [Remote], _readExtensions_dec = [Remote], _saveExtensions_dec = [Remote], _preview_dec = [Remote], _a) {
1043
1310
  constructor(ctx) {
1044
1311
  super(ctx, "studioCatalog");
1045
1312
  __runInitializers(_init, 5, this);
@@ -1049,8 +1316,9 @@ var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec =
1049
1316
  __publicField(this, "stopped", false);
1050
1317
  __publicField(this, "extensions");
1051
1318
  __publicField(this, "changes", Promise.resolve());
1319
+ __publicField(this, "updater", new StudioUpdater(this.ctx));
1052
1320
  const storePath = studioStorePath(process.env, ctx.get("profileContext"));
1053
- this.extensions = new MasterExtensions(join2(dirname3(storePath), "master-extensions.json"));
1321
+ this.extensions = new MasterExtensions(join3(dirname3(storePath), "master-extensions.json"));
1054
1322
  this.store = new PresetStore(ctx, storePath);
1055
1323
  this.snapshot = new Promise((resolve2) => {
1056
1324
  setImmediate(resolve2);
@@ -1086,6 +1354,12 @@ var StudioCatalog = class extends (_a = TypertRemoteService, _listPresets_dec =
1086
1354
  if (!service) throw new Error("Model catalog API unavailable");
1087
1355
  return service.modelCatalog();
1088
1356
  }
1357
+ async updateStatus(force) {
1358
+ return this.updater.status(force === "force");
1359
+ }
1360
+ async updateStudio(version) {
1361
+ return this.updater.update(version);
1362
+ }
1089
1363
  async deletePreset(id) {
1090
1364
  return this.store.delete(id);
1091
1365
  }
@@ -1186,6 +1460,8 @@ __decorateElement(_init, 1, "savePreset", _savePreset_dec, StudioCatalog);
1186
1460
  __decorateElement(_init, 1, "applyPreset", _applyPreset_dec, StudioCatalog);
1187
1461
  __decorateElement(_init, 1, "unapplyPreset", _unapplyPreset_dec, StudioCatalog);
1188
1462
  __decorateElement(_init, 1, "roleModels", _roleModels_dec, StudioCatalog);
1463
+ __decorateElement(_init, 1, "updateStatus", _updateStatus_dec, StudioCatalog);
1464
+ __decorateElement(_init, 1, "updateStudio", _updateStudio_dec, StudioCatalog);
1189
1465
  __decorateElement(_init, 1, "deletePreset", _deletePreset_dec, StudioCatalog);
1190
1466
  __decorateElement(_init, 1, "read", _read_dec, StudioCatalog);
1191
1467
  __decorateElement(_init, 1, "readExtensions", _readExtensions_dec, StudioCatalog);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matamt-dbg/dsh-studio",
3
- "version": "0.2.2",
3
+ "version": "0.2.8",
4
4
  "description": "DSH Studio: build, edit and apply DeepSeek Harness agent presets — tools, prompts, Configs and role-based subagents — from the web GUI.",
5
5
  "keywords": [
6
6
  "deepseek-harness",