runwork 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +308 -90
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7381,7 +7381,7 @@ function createKeyboardListener() {
7381
7381
  }
7382
7382
 
7383
7383
  // src/generated/version.ts
7384
- var VERSION = "0.16.1";
7384
+ var VERSION = "0.17.0";
7385
7385
 
7386
7386
  // src/commands/dev.ts
7387
7387
  var exports_dev = {};
@@ -11118,6 +11118,33 @@ function formatCombinedDigest(sessions, opts = { days: 7 }) {
11118
11118
  // src/agents/utils/json-config.ts
11119
11119
  import { readFileSync as readFileSync22, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, existsSync as existsSync25 } from "fs";
11120
11120
  import { dirname as dirname5 } from "path";
11121
+
11122
+ // src/sync/hash.ts
11123
+ import { createHash as createHash2 } from "crypto";
11124
+ function contentHash(content) {
11125
+ return "sha256:" + createHash2("sha256").update(content).digest("hex");
11126
+ }
11127
+ function stableStringify(value) {
11128
+ return JSON.stringify(sortValue(value));
11129
+ }
11130
+ function sortValue(value) {
11131
+ if (Array.isArray(value))
11132
+ return value.map(sortValue);
11133
+ if (value !== null && typeof value === "object") {
11134
+ const obj = value;
11135
+ const sorted = {};
11136
+ for (const key of Object.keys(obj).sort()) {
11137
+ sorted[key] = sortValue(obj[key]);
11138
+ }
11139
+ return sorted;
11140
+ }
11141
+ return value;
11142
+ }
11143
+ function configHash(value) {
11144
+ return contentHash(stableStringify(value));
11145
+ }
11146
+
11147
+ // src/agents/utils/json-config.ts
11121
11148
  function isRunworkManagedKey(key) {
11122
11149
  return key === RUNWORK_WORKSPACE_MCP_NAME || key.startsWith(RUNWORK_MCP_PREFIX) || key.startsWith(RUNWORK_MCP_PREFIX_LEGACY);
11123
11150
  }
@@ -11156,6 +11183,14 @@ function removeRunworkMcpServers(filePath, topKey) {
11156
11183
  function mergeJsonMcpServers(filePath, servers, topKey) {
11157
11184
  const config = readJsonConfig(filePath);
11158
11185
  const existing = config[topKey] || {};
11186
+ const managedBefore = {};
11187
+ for (const key of Object.keys(existing)) {
11188
+ if (isRunworkManagedKey(key))
11189
+ managedBefore[key] = existing[key];
11190
+ }
11191
+ const changed = configHash(managedBefore) !== configHash(servers);
11192
+ if (!changed)
11193
+ return false;
11159
11194
  for (const key of Object.keys(existing)) {
11160
11195
  if (isRunworkManagedKey(key)) {
11161
11196
  delete existing[key];
@@ -11164,6 +11199,7 @@ function mergeJsonMcpServers(filePath, servers, topKey) {
11164
11199
  Object.assign(existing, servers);
11165
11200
  config[topKey] = existing;
11166
11201
  writeJsonConfig(filePath, config);
11202
+ return true;
11167
11203
  }
11168
11204
 
11169
11205
  // src/agents/utils/instruction-hint.ts
@@ -11487,6 +11523,19 @@ function resolveAgentDefaults(input) {
11487
11523
  };
11488
11524
  }
11489
11525
 
11526
+ // src/ui/verbosity.ts
11527
+ var verbose = false;
11528
+ function setVerbose(value) {
11529
+ verbose = value;
11530
+ }
11531
+ function isVerbose() {
11532
+ return verbose;
11533
+ }
11534
+ function vlog(...args) {
11535
+ if (verbose)
11536
+ console.log(...args);
11537
+ }
11538
+
11490
11539
  // src/agents/claude-code.ts
11491
11540
  var PLUGIN_NAME = "runwork";
11492
11541
  var PLUGIN_VERSION = "1.0.0";
@@ -11589,7 +11638,7 @@ class ClaudeCodeAdapter {
11589
11638
  ]
11590
11639
  };
11591
11640
  writeFileSync16(join21(hooksDir, "hooks.json"), JSON.stringify(hooksManifest, null, 2));
11592
- console.log(` [Claude Code] Installed SessionStart hook (${label}) -> ${scriptPath}`);
11641
+ vlog(` [Claude Code] Installed SessionStart hook (${label}) -> ${scriptPath}`);
11593
11642
  }
11594
11643
  async writeSkills(skills, scope) {
11595
11644
  const baseDir = scope === "project" ? join21(process.cwd(), ".claude", "skills") : join21(homedir5(), ".claude", "skills");
@@ -11622,6 +11671,7 @@ class ClaudeCodeAdapter {
11622
11671
  }
11623
11672
  this.registerPlugin(pluginDir);
11624
11673
  }
11674
+ return skills.length;
11625
11675
  }
11626
11676
  async writeBuiltInHooks(scope) {
11627
11677
  if (scope !== "user")
@@ -12116,7 +12166,7 @@ ${instructions}`;
12116
12166
  lastUpdated: new Date().toISOString()
12117
12167
  };
12118
12168
  writeJsonConfig(marketplacesPath, marketplaces);
12119
- console.log(` [Claude Code] ${wasRegistered ? "Refreshed" : "Registered"} plugin ` + `${pluginKey} v${PLUGIN_VERSION} -> ${installedPath}`);
12169
+ vlog(` [Claude Code] ${wasRegistered ? "Refreshed" : "Registered"} plugin ` + `${pluginKey} v${PLUGIN_VERSION} -> ${installedPath}`);
12120
12170
  const settingsPath = join21(homedir5(), ".claude", "settings.json");
12121
12171
  const settings = readJsonConfig(settingsPath);
12122
12172
  if (!settings["enabledPlugins"]) {
@@ -12416,12 +12466,12 @@ class ClaudeDesktopAdapter {
12416
12466
  if (rpm) {
12417
12467
  writePluginMetadata(rpm.pluginPath, getPluginMetadata());
12418
12468
  writePluginSkills(rpm.pluginPath, skills);
12419
- return;
12469
+ return skills.length;
12420
12470
  }
12421
12471
  const pluginsDir = findCoworkPluginsDir();
12422
12472
  if (!pluginsDir) {
12423
12473
  console.warn(" [Claude Desktop] Cowork plugins directory not found. Open Claude Desktop at least once first.");
12424
- return;
12474
+ return 0;
12425
12475
  }
12426
12476
  const cacheDir = join23(pluginsDir, "cache", "runwork", PLUGIN_NAME2, PLUGIN_VERSION2);
12427
12477
  const marketRoot = join23(pluginsDir, "marketplaces", "runwork");
@@ -12456,6 +12506,7 @@ class ClaudeDesktopAdapter {
12456
12506
  };
12457
12507
  writeJsonConfig(marketplacesPath, marketplaces);
12458
12508
  setCoworkPluginEnabled(pluginsDir, true);
12509
+ return skills.length;
12459
12510
  }
12460
12511
  async writeInstructionHint(hint, _scope) {
12461
12512
  for (const path2 of getCoworkMemoryClaudeMdPaths()) {
@@ -12946,7 +12997,7 @@ class CursorAdapter {
12946
12997
  }
12947
12998
  async writeSkills(skills, scope) {
12948
12999
  if (scope === "user")
12949
- return;
13000
+ return 0;
12950
13001
  const rulesDir = join24(process.cwd(), ".cursor", "rules");
12951
13002
  mkdirSync18(rulesDir, { recursive: true });
12952
13003
  for (const skill of skills) {
@@ -12958,6 +13009,7 @@ alwaysApply: false
12958
13009
  ${skill.content}`;
12959
13010
  writeFileSync19(join24(rulesDir, `${skill.filename}.mdc`), mdcContent);
12960
13011
  }
13012
+ return skills.length;
12961
13013
  }
12962
13014
  async writeInstructionHint(hint, scope) {
12963
13015
  const filePath = scope === "project" ? join24(process.cwd(), ".cursor", "rules", "runwork.mdc") : join24(homedir7(), ".cursor", "rules", "runwork.mdc");
@@ -13210,7 +13262,7 @@ class WindsurfAdapter {
13210
13262
  }
13211
13263
  async writeSkills(skills, scope) {
13212
13264
  if (scope === "user")
13213
- return;
13265
+ return 0;
13214
13266
  const rulesDir = join25(process.cwd(), ".windsurf", "rules");
13215
13267
  mkdirSync19(rulesDir, { recursive: true });
13216
13268
  for (const skill of skills) {
@@ -13221,6 +13273,7 @@ trigger: manual
13221
13273
  ${skill.content}`;
13222
13274
  writeFileSync20(join25(rulesDir, `${skill.filename}.md`), content);
13223
13275
  }
13276
+ return skills.length;
13224
13277
  }
13225
13278
  async writeTeamInstructions(instructions, scope) {
13226
13279
  const filePath = scope === "project" ? join25(process.cwd(), ".windsurf", "rules", "runwork-team.md") : join25(getWindsurfDataDir(), "rules", "runwork-team.md");
@@ -13468,7 +13521,7 @@ var AGENT_REGISTRY = [
13468
13521
  ]
13469
13522
  },
13470
13523
  launch: { app: { macos: "Codex", windows: "Codex" } },
13471
- logo: "openai",
13524
+ logo: "codex",
13472
13525
  downloadUrl: "https://openai.com/codex/",
13473
13526
  skillsPaths: { global: ".codex/skills", project: ".agents/skills" },
13474
13527
  instructionFile: { global: ".codex/instructions.md", project: "AGENTS.md" },
@@ -13493,7 +13546,7 @@ var AGENT_REGISTRY = [
13493
13546
  category: "cli",
13494
13547
  detection: { method: "binary", target: "codex" },
13495
13548
  launch: { cli: "codex", cliAcceptsPrompt: true },
13496
- logo: "openai",
13549
+ logo: "codex",
13497
13550
  downloadUrl: "https://github.com/openai/codex",
13498
13551
  skillsPaths: { global: ".codex/skills", project: ".agents/skills" },
13499
13552
  instructionFile: { global: ".codex/instructions.md", project: "AGENTS.md" },
@@ -14016,6 +14069,7 @@ class CodexAdapter {
14016
14069
  mkdirSync20(skillDir, { recursive: true });
14017
14070
  writeFileSync21(join28(skillDir, "SKILL.md"), buildSkillMd2(skill));
14018
14071
  }
14072
+ return skills.length;
14019
14073
  }
14020
14074
  async writeInstructionHint(hint, scope) {
14021
14075
  const filePath = scope === "project" ? join28(process.cwd(), "AGENTS.md") : join28(homedir11(), ".codex", "AGENTS.md");
@@ -14377,12 +14431,13 @@ class ClineAdapter {
14377
14431
  }
14378
14432
  async writeSkills(skills, scope) {
14379
14433
  if (scope === "user")
14380
- return;
14434
+ return 0;
14381
14435
  const rulesDir = join29(process.cwd(), ".clinerules");
14382
14436
  mkdirSync21(rulesDir, { recursive: true });
14383
14437
  for (const skill of skills) {
14384
14438
  writeFileSync22(join29(rulesDir, `${skill.filename}.md`), buildSkillMd2(skill));
14385
14439
  }
14440
+ return skills.length;
14386
14441
  }
14387
14442
  async writeInstructionHint(hint, scope) {
14388
14443
  if (scope === "user")
@@ -14507,6 +14562,7 @@ class GeminiAdapter {
14507
14562
  mkdirSync22(skillDir, { recursive: true });
14508
14563
  writeFileSync23(join30(skillDir, "SKILL.md"), buildSkillMd2(skill));
14509
14564
  }
14565
+ return skills.length;
14510
14566
  }
14511
14567
  async writeInstructionHint(hint, scope) {
14512
14568
  const filePath = scope === "project" ? join30(process.cwd(), "GEMINI.md") : join30(homedir13(), ".gemini", "GEMINI.md");
@@ -14606,13 +14662,13 @@ class GenericAgentAdapter {
14606
14662
  }
14607
14663
  async writeSkills(skills, scope) {
14608
14664
  if (!this.def.skillsPaths)
14609
- return;
14665
+ return 0;
14610
14666
  const pathTemplate = scope === "project" ? this.def.skillsPaths.project : this.def.skillsPaths.global;
14611
14667
  if (!pathTemplate)
14612
- return;
14668
+ return 0;
14613
14669
  const baseDir = resolveToAbsolute(pathTemplate, scope === "project" ? "project" : "global");
14614
14670
  if (!baseDir)
14615
- return;
14671
+ return 0;
14616
14672
  for (const skill of skills) {
14617
14673
  if (skill.name !== skill.filename) {
14618
14674
  const oldDir = join31(baseDir, skill.name);
@@ -14626,6 +14682,7 @@ class GenericAgentAdapter {
14626
14682
  mkdirSync23(skillDir, { recursive: true });
14627
14683
  writeFileSync24(join31(skillDir, "SKILL.md"), buildSkillMd2(skill));
14628
14684
  }
14685
+ return skills.length;
14629
14686
  }
14630
14687
  async writeInstructionHint(hint, scope) {
14631
14688
  if (!this.def.instructionFile)
@@ -14740,9 +14797,9 @@ function printNoAgentsMessage() {
14740
14797
  }
14741
14798
 
14742
14799
  // src/utils/insight-id.ts
14743
- import { createHash as createHash2 } from "node:crypto";
14800
+ import { createHash as createHash3 } from "node:crypto";
14744
14801
  function computeInsightId(userSeed, localKey) {
14745
- return createHash2("sha256").update(`${userSeed}:${localKey}`).digest("hex").slice(0, 22);
14802
+ return createHash3("sha256").update(`${userSeed}:${localKey}`).digest("hex").slice(0, 22);
14746
14803
  }
14747
14804
  function insightLocalKey(teaches, slug) {
14748
14805
  return `${teaches}:${slug}`.toLowerCase().replace(/[^a-z0-9:_-]+/g, "-").replace(/-+/g, "-");
@@ -16664,19 +16721,6 @@ async function collectTelemetryEvents(params) {
16664
16721
  const now = params.now ?? new Date().toISOString();
16665
16722
  const events = [];
16666
16723
  const adapterResults = [];
16667
- const previouslyReported = new Set(params.state.reportedAgentSlugs ?? []);
16668
- const newlyReportedAgents = [];
16669
- for (const slug of params.state.configuredAgents) {
16670
- if (!previouslyReported.has(slug)) {
16671
- events.push({
16672
- eventType: "local_agent.detected",
16673
- agentSlug: slug,
16674
- metadata: { installed: true },
16675
- timestamp: now
16676
- });
16677
- newlyReportedAgents.push(slug);
16678
- }
16679
- }
16680
16724
  for (const adapter2 of params.adapters) {
16681
16725
  let stats = "unsupported";
16682
16726
  let skills = "unsupported";
@@ -16768,11 +16812,12 @@ async function collectTelemetryEvents(params) {
16768
16812
  syncedMcpServers: params.syncedMcpServersCount,
16769
16813
  teamInstructionsApplied: params.teamInstructionsApplied,
16770
16814
  agentConfigsApplied: params.agentConfigsApplied,
16815
+ agents: [...params.state.configuredAgents],
16771
16816
  ...params.state.persona?.label ? { persona: params.state.persona.label } : {}
16772
16817
  },
16773
16818
  timestamp: now
16774
16819
  });
16775
- return { events, adapterResults, newlyReportedAgents, healthReported };
16820
+ return { events, adapterResults, healthReported };
16776
16821
  }
16777
16822
  function formatStatsLine(stats) {
16778
16823
  const parts = [];
@@ -16818,10 +16863,6 @@ function printTelemetryVerbose(result) {
16818
16863
  const errSuffix = r.error ? ` (error: ${r.error})` : "";
16819
16864
  console.log(` [${r.name}] stats: ${statsLabel}; skills: ${skillsLabel}${errSuffix}`);
16820
16865
  }
16821
- if (result.newlyReportedAgents.length > 0) {
16822
- console.log(`
16823
- First-time detections: ${result.newlyReportedAgents.join(", ")}`);
16824
- }
16825
16866
  if (result.healthReported) {
16826
16867
  console.log(" Health report: due (daily)");
16827
16868
  }
@@ -17451,12 +17492,6 @@ function buildSaveConversationSkill() {
17451
17492
  };
17452
17493
  }
17453
17494
 
17454
- // src/sync/hash.ts
17455
- import { createHash as createHash3 } from "crypto";
17456
- function contentHash(content) {
17457
- return "sha256:" + createHash3("sha256").update(content).digest("hex");
17458
- }
17459
-
17460
17495
  // src/sync/change-detect.ts
17461
17496
  function computeSyncPlan(input) {
17462
17497
  const { storedHashes, localSkills, remoteSkills } = input;
@@ -17488,6 +17523,16 @@ function computeSyncPlan(input) {
17488
17523
  const localChanged = localHash !== null && localHash !== stored.localHash;
17489
17524
  const remoteChanged = remoteHash !== stored.remoteHash;
17490
17525
  if (!local) {
17526
+ if (remote.source === "app" && !remoteChanged) {
17527
+ plan.skips.push({
17528
+ name: remote.name,
17529
+ action: "skip",
17530
+ source: remote.source,
17531
+ remoteId: remote.remoteId,
17532
+ appId: remote.appId
17533
+ });
17534
+ continue;
17535
+ }
17491
17536
  plan.pulls.push({
17492
17537
  name: remote.name,
17493
17538
  action: "pull",
@@ -17667,6 +17712,47 @@ function resolveConflictNonInteractive(prefer) {
17667
17712
  return prefer;
17668
17713
  }
17669
17714
 
17715
+ // src/sync/summary-format.ts
17716
+ function summarizeSkillPlan(plan) {
17717
+ return {
17718
+ added: plan.pulls.filter((p) => p.action === "new-remote").map((p) => p.name),
17719
+ updated: plan.pulls.filter((p) => p.action === "pull").map((p) => p.name),
17720
+ removed: plan.deletions.map((p) => p.name),
17721
+ pushed: plan.pushes.map((p) => p.name),
17722
+ unchanged: plan.skips.length,
17723
+ conflicts: plan.conflicts.length
17724
+ };
17725
+ }
17726
+ function formatNameList(names, cap2 = 6) {
17727
+ if (names.length <= cap2)
17728
+ return names.join(", ");
17729
+ return `${names.slice(0, cap2).join(", ")}, +${names.length - cap2} more`;
17730
+ }
17731
+ function formatSkillSummaryLine(c) {
17732
+ const parts = [];
17733
+ if (c.added.length)
17734
+ parts.push(`${c.added.length} added (${formatNameList(c.added)})`);
17735
+ if (c.updated.length)
17736
+ parts.push(`${c.updated.length} updated (${formatNameList(c.updated)})`);
17737
+ if (c.removed.length)
17738
+ parts.push(`${c.removed.length} removed (${formatNameList(c.removed)})`);
17739
+ if (c.pushed.length)
17740
+ parts.push(`${c.pushed.length} pushed (${formatNameList(c.pushed)})`);
17741
+ if (c.conflicts)
17742
+ parts.push(`${c.conflicts} conflict${c.conflicts === 1 ? "" : "s"}`);
17743
+ parts.push(`${c.unchanged} up to date`);
17744
+ return parts.join(", ");
17745
+ }
17746
+ var REASON_LABEL = {
17747
+ mcp: "MCP",
17748
+ settings: "settings"
17749
+ };
17750
+ function formatRestartChanges(changes) {
17751
+ if (changes.length === 0)
17752
+ return "none";
17753
+ return changes.map((c) => `${c.name} (${c.reasons.map((r) => REASON_LABEL[r]).join(", ")})`).join("; ");
17754
+ }
17755
+
17670
17756
  // src/sync/executor.ts
17671
17757
  async function executeSyncPlan(plan, resolvedConflicts, ctx) {
17672
17758
  const newHashes = {};
@@ -17682,7 +17768,7 @@ async function executeSyncPlan(plan, resolvedConflicts, ctx) {
17682
17768
  remoteId: action.remoteId,
17683
17769
  appId: action.appId
17684
17770
  };
17685
- console.log(` Pulled: ${action.name}`);
17771
+ vlog(` Pulled: ${action.name}`);
17686
17772
  }
17687
17773
  for (const action of plan.pushes) {
17688
17774
  if (!action.localContent || !action.remoteId)
@@ -17699,7 +17785,7 @@ async function executeSyncPlan(plan, resolvedConflicts, ctx) {
17699
17785
  source: "external",
17700
17786
  remoteId: action.remoteId
17701
17787
  };
17702
- console.log(` Pushed: ${action.name}`);
17788
+ vlog(` Pushed: ${action.name}`);
17703
17789
  }
17704
17790
  for (const action of plan.conflicts) {
17705
17791
  const resolution = resolvedConflicts.get(action.name);
@@ -17713,7 +17799,7 @@ async function executeSyncPlan(plan, resolvedConflicts, ctx) {
17713
17799
  });
17714
17800
  const hash = contentHash(action.localContent);
17715
17801
  newHashes[action.name] = { localHash: hash, remoteHash: hash, source: "external", remoteId: action.remoteId };
17716
- console.log(` Pushed (conflict resolved): ${action.name}`);
17802
+ vlog(` Pushed (conflict resolved): ${action.name}`);
17717
17803
  } else if (resolution === "remote" && action.remoteContent) {
17718
17804
  const skillFile = makeSkillFile(action.name, action.remoteContent);
17719
17805
  await writeSkillToAgents(skillFile, action.source, ctx);
@@ -17723,12 +17809,12 @@ async function executeSyncPlan(plan, resolvedConflicts, ctx) {
17723
17809
  source: "external",
17724
17810
  remoteId: action.remoteId
17725
17811
  };
17726
- console.log(` Pulled (conflict resolved): ${action.name}`);
17812
+ vlog(` Pulled (conflict resolved): ${action.name}`);
17727
17813
  }
17728
17814
  }
17729
17815
  for (const _action of plan.skips) {}
17730
17816
  for (const action of plan.deletions) {
17731
- console.log(` Deleted locally: ${action.name}`);
17817
+ vlog(` Deleted locally: ${action.name}`);
17732
17818
  }
17733
17819
  return newHashes;
17734
17820
  }
@@ -17758,6 +17844,53 @@ function extractDescription(content) {
17758
17844
  return match ? match[1].trim() : "";
17759
17845
  }
17760
17846
 
17847
+ // src/sync/restart-stamp.ts
17848
+ function computeRestartReasons(input) {
17849
+ const settingsSet = new Set(input.settingsChangedSlugs);
17850
+ const out = {};
17851
+ for (const agent of input.agents) {
17852
+ if (agent.connectOnly)
17853
+ continue;
17854
+ const reasons = [];
17855
+ if ((input.mcpChanged || agent.isNew) && agent.receivesMcp)
17856
+ reasons.push("mcp");
17857
+ if (settingsSet.has(agent.slug))
17858
+ reasons.push("settings");
17859
+ if (reasons.length > 0)
17860
+ out[agent.slug] = reasons;
17861
+ }
17862
+ return out;
17863
+ }
17864
+ function summarizeRealSkillChanges(newLocalHashes, priorLocalHashes, materializedDeletions) {
17865
+ const added = [];
17866
+ const updated = [];
17867
+ for (const [name, hash] of Object.entries(newLocalHashes)) {
17868
+ if (!(name in priorLocalHashes))
17869
+ added.push(name);
17870
+ else if (priorLocalHashes[name] !== hash)
17871
+ updated.push(name);
17872
+ }
17873
+ return {
17874
+ added: added.sort(),
17875
+ updated: updated.sort(),
17876
+ removed: [...materializedDeletions].sort()
17877
+ };
17878
+ }
17879
+ function hasRealSkillChange(c) {
17880
+ return c.added.length > 0 || c.updated.length > 0 || c.removed.length > 0;
17881
+ }
17882
+ function sameStringSet(a, b) {
17883
+ const setA = new Set(a ?? []);
17884
+ const setB = new Set(b ?? []);
17885
+ if (setA.size !== setB.size)
17886
+ return false;
17887
+ for (const item of setA) {
17888
+ if (!setB.has(item))
17889
+ return false;
17890
+ }
17891
+ return true;
17892
+ }
17893
+
17761
17894
  // src/commands/sync.ts
17762
17895
  async function printAdoptionHint(credentials, workspaceId) {
17763
17896
  if (!workspaceId)
@@ -17842,6 +17975,7 @@ async function refreshConfiguredAgents(state) {
17842
17975
  }
17843
17976
  async function syncFromState(state, statePath2, credentials, opts) {
17844
17977
  const client = new ApiClient(credentials);
17978
+ setVerbose(!!opts.verbose);
17845
17979
  if (!state.workspaceName || state.workspaceName === state.workspaceId || !state.workspaceSlug) {
17846
17980
  try {
17847
17981
  const workspaces = await client.listWorkspaces();
@@ -17853,11 +17987,12 @@ async function syncFromState(state, statePath2, credentials, opts) {
17853
17987
  } catch {}
17854
17988
  }
17855
17989
  console.log(`Syncing workspace: ${state.workspaceName || state.workspaceId}`);
17990
+ let addedThisSync = [];
17856
17991
  if (shouldRedetect(state, opts)) {
17857
17992
  console.log(" Detecting installed agents...");
17858
- const added = await refreshConfiguredAgents(state);
17859
- if (added.length > 0) {
17860
- console.log(` Detected new agent${added.length > 1 ? "s" : ""}: ${added.join(", ")}`);
17993
+ addedThisSync = await refreshConfiguredAgents(state);
17994
+ if (addedThisSync.length > 0) {
17995
+ console.log(` Detected new agent${addedThisSync.length > 1 ? "s" : ""}: ${addedThisSync.join(", ")}`);
17861
17996
  }
17862
17997
  }
17863
17998
  console.log(" Fetching workspace data...");
@@ -17899,6 +18034,8 @@ async function syncFromState(state, statePath2, credentials, opts) {
17899
18034
  headers: { Authorization: `Bearer ${credentials.apiKey}` },
17900
18035
  description: "Runwork workspace tools: query entities, trigger workflows, manage integrations, run agents, and access shared data across your team's apps."
17901
18036
  });
18037
+ const mcpEntriesHash = configHash([...mcpEntries].sort((a, b) => a.name.localeCompare(b.name)));
18038
+ const mcpChanged = state.lastMcpEntriesHash !== undefined && state.lastMcpEntriesHash !== mcpEntriesHash;
17902
18039
  const remoteSkills = [];
17903
18040
  for (const s of externalSkills) {
17904
18041
  remoteSkills.push({ name: s.name, content: s.content, source: "external", remoteId: s.id });
@@ -17928,12 +18065,9 @@ async function syncFromState(state, statePath2, credentials, opts) {
17928
18065
  plan.skips.push(...plan.conflicts.map((c) => ({ ...c, action: "skip" })));
17929
18066
  plan.conflicts = [];
17930
18067
  }
17931
- const hasChanges = plan.pulls.length > 0 || plan.pushes.length > 0 || plan.conflicts.length > 0 || plan.deletions.length > 0;
17932
- if (hasChanges) {
18068
+ console.log(` Skills: ${formatSkillSummaryLine(summarizeSkillPlan(plan))}`);
18069
+ if (isVerbose())
17933
18070
  printSyncSummary(plan);
17934
- } else {
17935
- console.log(" All skills up to date.");
17936
- }
17937
18071
  const adapters = [];
17938
18072
  for (const slug of state.configuredAgents) {
17939
18073
  const adapter2 = getAdapterBySlug(slug);
@@ -17973,6 +18107,7 @@ async function syncFromState(state, statePath2, credentials, opts) {
17973
18107
  }
17974
18108
  }
17975
18109
  }
18110
+ const settingsChangedSlugs = [];
17976
18111
  const scopes = state.scope === "both" ? ["project", "user"] : [state.scope];
17977
18112
  if (adapters.length > 0) {
17978
18113
  console.log(` Syncing to: ${adapters.map((a) => a.name).join(", ")}`);
@@ -18021,6 +18156,8 @@ async function syncFromState(state, statePath2, credentials, opts) {
18021
18156
  buildSaveConversationSkill()
18022
18157
  ];
18023
18158
  const builtInNames = builtInSkills.map((s) => s.name);
18159
+ const builtInSkillsHash = configHash(builtInSkills.map((s) => s.content));
18160
+ const builtInSkillsChanged = state.builtInSkillsHash !== undefined && state.builtInSkillsHash !== builtInSkillsHash;
18024
18161
  const summary = {
18025
18162
  adaptersProcessed: 0,
18026
18163
  adaptersFailed: 0,
@@ -18030,16 +18167,18 @@ async function syncFromState(state, statePath2, credentials, opts) {
18030
18167
  instructionHintWrites: 0,
18031
18168
  hookInstallCalls: 0
18032
18169
  };
18170
+ const mcpFailedAdapters = new Set;
18171
+ const skillFailedAdapters = new Set;
18033
18172
  for (const adapter2 of adapters) {
18034
18173
  if (isConnectOnlyAgent(getRegistryAgent(adapter2.slug))) {
18035
- console.log(` [${adapter2.name}] Connect-only agent: no local files to sync`);
18174
+ vlog(` [${adapter2.name}] Connect-only agent: no local files to sync`);
18036
18175
  summary.adaptersProcessed++;
18037
18176
  continue;
18038
18177
  }
18039
18178
  let adapterFailedAnyScope = false;
18040
18179
  for (const scope of scopes) {
18041
- try {
18042
- if (adapter2.supportsSkills()) {
18180
+ if (adapter2.supportsSkills()) {
18181
+ try {
18043
18182
  const skipAppSkills = adapter2.mcpProvidesSkills && mcpEntries.length > 0;
18044
18183
  const scopeSkills = remoteSkills.filter((s) => {
18045
18184
  if (builtInNameSet.has(s.name))
@@ -18059,25 +18198,41 @@ async function syncFromState(state, statePath2, credentials, opts) {
18059
18198
  description: s.source === "app" ? buildAppSkillDescription(s.name, registries) || `${s.name} - Runwork workspace application` : `${s.name} - Runwork workspace skill`
18060
18199
  }));
18061
18200
  const allSkillFiles = [...builtInSkills, ...workspaceSkillFiles];
18062
- await adapter2.writeSkills(allSkillFiles, scope);
18063
- summary.skillWrites++;
18064
- summary.skillFilesWritten += allSkillFiles.length;
18065
- console.log(` [${adapter2.name}] Wrote ${allSkillFiles.length} skills (${scope}): ` + `${builtInSkills.length} built-in [${builtInNames.join(", ")}], ` + `${workspaceSkillFiles.length} workspace`);
18066
- } else {
18067
- console.log(` [${adapter2.name}] Skipped skills (${scope}): adapter does not support skills`);
18201
+ const written = await adapter2.writeSkills(allSkillFiles, scope);
18202
+ if (written > 0) {
18203
+ summary.skillWrites++;
18204
+ summary.skillFilesWritten += written;
18205
+ vlog(` [${adapter2.name}] Wrote ${written} skills (${scope}): ` + `${builtInSkills.length} built-in [${builtInNames.join(", ")}], ` + `${workspaceSkillFiles.length} workspace`);
18206
+ } else {
18207
+ vlog(` [${adapter2.name}] Skipped skills (${scope}): adapter writes none at this scope`);
18208
+ }
18209
+ } catch (err) {
18210
+ adapterFailedAnyScope = true;
18211
+ skillFailedAdapters.add(adapter2.slug);
18212
+ console.warn(` [${adapter2.name}] Failed skills (${scope}): ${err instanceof Error ? err.message : err}`);
18068
18213
  }
18069
- if (adapter2.supportsMcpScope(scope) && mcpEntries.length > 0) {
18214
+ } else {
18215
+ vlog(` [${adapter2.name}] Skipped skills (${scope}): adapter does not support skills`);
18216
+ }
18217
+ if (adapter2.supportsMcpScope(scope) && mcpEntries.length > 0) {
18218
+ try {
18070
18219
  await adapter2.writeMcpServers(mcpEntries, scope);
18071
18220
  summary.mcpServerWrites++;
18072
- console.log(` [${adapter2.name}] Updated ${mcpEntries.length} MCP server${mcpEntries.length > 1 ? "s" : ""} (${scope})`);
18073
- } else if (mcpEntries.length === 0) {
18074
- console.log(` [${adapter2.name}] Skipped MCP servers (${scope}): no workspace MCP servers configured`);
18075
- } else if (!adapter2.supportsMcpScope(scope)) {
18076
- console.log(` [${adapter2.name}] Skipped MCP servers (${scope}): adapter does not support MCP at this scope`);
18221
+ vlog(` [${adapter2.name}] Updated ${mcpEntries.length} MCP server${mcpEntries.length > 1 ? "s" : ""} (${scope})`);
18222
+ } catch (err) {
18223
+ adapterFailedAnyScope = true;
18224
+ mcpFailedAdapters.add(adapter2.slug);
18225
+ console.warn(` [${adapter2.name}] Failed MCP servers (${scope}): ${err instanceof Error ? err.message : err}`);
18077
18226
  }
18227
+ } else if (mcpEntries.length === 0) {
18228
+ vlog(` [${adapter2.name}] Skipped MCP servers (${scope}): no workspace MCP servers configured`);
18229
+ } else if (!adapter2.supportsMcpScope(scope)) {
18230
+ vlog(` [${adapter2.name}] Skipped MCP servers (${scope}): adapter does not support MCP at this scope`);
18231
+ }
18232
+ try {
18078
18233
  await adapter2.writeInstructionHint(instructionHint, scope);
18079
18234
  summary.instructionHintWrites++;
18080
- console.log(` [${adapter2.name}] Updated instruction hints (${scope})`);
18235
+ vlog(` [${adapter2.name}] Updated instruction hints (${scope})`);
18081
18236
  if (adapter2.writeBuiltInHooks) {
18082
18237
  await adapter2.writeBuiltInHooks(scope);
18083
18238
  summary.hookInstallCalls++;
@@ -18116,7 +18271,7 @@ async function syncFromState(state, statePath2, credentials, opts) {
18116
18271
  try {
18117
18272
  await adapter2.writeTeamInstructions(fullInstructions, scope);
18118
18273
  teamInstructionsApplied = true;
18119
- console.log(` [${adapter2.name}] Updated team instructions (${scope})`);
18274
+ vlog(` [${adapter2.name}] Updated team instructions (${scope})`);
18120
18275
  } catch {}
18121
18276
  }
18122
18277
  }
@@ -18137,7 +18292,7 @@ async function syncFromState(state, statePath2, credentials, opts) {
18137
18292
  await adapter2.writeAgentConfig(configWithoutInstructions, scope);
18138
18293
  agentConfigsApplied++;
18139
18294
  const configKeys = Object.keys(configWithoutInstructions).join(", ");
18140
- console.log(` [${adapter2.name}] Updated agent config: ${configKeys} (${scope})`);
18295
+ vlog(` [${adapter2.name}] Updated agent config: ${configKeys} (${scope})`);
18141
18296
  } catch {}
18142
18297
  }
18143
18298
  }
@@ -18192,6 +18347,12 @@ async function syncFromState(state, statePath2, credentials, opts) {
18192
18347
  try {
18193
18348
  const baseline = agentState?.lastInjected;
18194
18349
  await adapter2.writeAgentConfig(mergedConfig, "user", baseline);
18350
+ if (baked && !resolved.bootstrapped) {
18351
+ const prev = agentState?.lastInjected;
18352
+ if (!sameStringSet(prev?.allow, resolved.applicableAllow) || !sameStringSet(prev?.deny, resolved.applicableDeny)) {
18353
+ settingsChangedSlugs.push(slug);
18354
+ }
18355
+ }
18195
18356
  if (baked) {
18196
18357
  const nextState = {
18197
18358
  lastInjected: {
@@ -18205,11 +18366,11 @@ async function syncFromState(state, statePath2, credentials, opts) {
18205
18366
  };
18206
18367
  state.agentDefaults[slug] = nextState;
18207
18368
  if (resolved.bootstrapped) {
18208
- console.log(` [${adapter2.name}] Bootstrapped default-rule tracking (defaults apply on next sync)`);
18369
+ vlog(` [${adapter2.name}] Bootstrapped default-rule tracking (defaults apply on next sync)`);
18209
18370
  } else if (resolved.applicableAllow.length || resolved.applicableDeny.length || resolved.removalsThisSync.allow || resolved.removalsThisSync.deny) {
18210
18371
  const totalRemovals = resolved.removalsThisSync.allow + resolved.removalsThisSync.deny;
18211
18372
  const removalNote = totalRemovals > 0 ? ` (${totalRemovals} new opt-out${totalRemovals > 1 ? "s" : ""} honored)` : "";
18212
- console.log(` [${adapter2.name}] Applied defaults: ${resolved.applicableAllow.length} allow, ${resolved.applicableDeny.length} deny${removalNote}`);
18373
+ vlog(` [${adapter2.name}] Applied defaults: ${resolved.applicableAllow.length} allow, ${resolved.applicableDeny.length} deny${removalNote}`);
18213
18374
  }
18214
18375
  }
18215
18376
  if (team)
@@ -18223,11 +18384,12 @@ async function syncFromState(state, statePath2, credentials, opts) {
18223
18384
  const runworkDir = join35(homedir17(), ".runwork");
18224
18385
  const result = adapter2.registerDesktopWorkspace(runworkDir, "Runwork");
18225
18386
  if (result === "written") {
18226
- console.log(` [${adapter2.name}] Registered workspace in Codex desktop app`);
18387
+ vlog(` [${adapter2.name}] Registered workspace in Codex desktop app`);
18227
18388
  }
18228
18389
  break;
18229
18390
  }
18230
18391
  }
18392
+ const prevMcpNames = state.mcpServers ?? [];
18231
18393
  state.lastSyncAt = new Date().toISOString();
18232
18394
  state.mcpServers = mcpEntries.map((e) => e.name);
18233
18395
  state.skills = remoteSkills.map((s) => s.name);
@@ -18235,6 +18397,57 @@ async function syncFromState(state, statePath2, credentials, opts) {
18235
18397
  "runwork",
18236
18398
  ...remoteSkills.map((s) => s.name.toLowerCase().replace(/[^a-z0-9]+/g, "-"))
18237
18399
  ];
18400
+ const priorSkillHashes = state.skillHashes || {};
18401
+ const newLocalHashes = {};
18402
+ for (const [name, h] of Object.entries(newHashes))
18403
+ newLocalHashes[name] = h.localHash;
18404
+ const priorLocalHashes = {};
18405
+ for (const [name, h] of Object.entries(priorSkillHashes))
18406
+ priorLocalHashes[name] = h.localHash;
18407
+ const materializedDeletions = plan.deletions.filter((d) => d.localContent !== undefined).map((d) => d.name);
18408
+ const skillChanges = summarizeRealSkillChanges(newLocalHashes, priorLocalHashes, materializedDeletions);
18409
+ const newMcpNames = mcpEntries.map((e) => e.name);
18410
+ const prevMcpSet = new Set(prevMcpNames);
18411
+ const newMcpSet = new Set(newMcpNames);
18412
+ const mcpAdded = mcpChanged ? newMcpNames.filter((n) => !prevMcpSet.has(n)) : [];
18413
+ const mcpRemoved = mcpChanged ? prevMcpNames.filter((n) => !newMcpSet.has(n)) : [];
18414
+ const addedSet = new Set(addedThisSync);
18415
+ const restartReasons = computeRestartReasons({
18416
+ agents: adapters.map((a) => ({
18417
+ slug: a.slug,
18418
+ receivesMcp: mcpEntries.length > 0 && scopes.some((sc) => a.supportsMcpScope(sc)) && !mcpFailedAdapters.has(a.slug),
18419
+ connectOnly: isConnectOnlyAgent(getRegistryAgent(a.slug)),
18420
+ isNew: !!opts.initialSetup || addedSet.has(a.slug)
18421
+ })),
18422
+ mcpChanged,
18423
+ settingsChangedSlugs
18424
+ });
18425
+ if (Object.keys(restartReasons).length > 0) {
18426
+ if (!state.agentConfigChangedAt)
18427
+ state.agentConfigChangedAt = {};
18428
+ for (const [slug, reasons] of Object.entries(restartReasons)) {
18429
+ state.agentConfigChangedAt[slug] = { changedAt: state.lastSyncAt, reasons };
18430
+ }
18431
+ }
18432
+ if (hasRealSkillChange(skillChanges) || builtInSkillsChanged || mcpChanged || settingsChangedSlugs.length > 0) {
18433
+ state.lastSyncChange = {
18434
+ at: state.lastSyncAt,
18435
+ skills: skillChanges,
18436
+ builtInSkillsChanged,
18437
+ mcp: { changed: mcpChanged, added: mcpAdded, removed: mcpRemoved },
18438
+ settingsSlugs: settingsChangedSlugs
18439
+ };
18440
+ }
18441
+ if (mcpFailedAdapters.size === 0)
18442
+ state.lastMcpEntriesHash = mcpEntriesHash;
18443
+ if (skillFailedAdapters.size === 0)
18444
+ state.builtInSkillsHash = builtInSkillsHash;
18445
+ const nameBySlug = new Map(adapters.map((a) => [a.slug, a.name]));
18446
+ const restartChanges = Object.entries(restartReasons).map(([slug, reasons]) => ({
18447
+ name: nameBySlug.get(slug) ?? slug,
18448
+ reasons
18449
+ }));
18450
+ console.log(` Restart-worthy changes: ${formatRestartChanges(restartChanges)}`);
18238
18451
  const mergedHashes = { ...state.skillHashes || {} };
18239
18452
  for (const [name, hash] of Object.entries(newHashes)) {
18240
18453
  mergedHashes[name] = hash;
@@ -18256,25 +18469,29 @@ async function syncFromState(state, statePath2, credentials, opts) {
18256
18469
  if (opts.verbose)
18257
18470
  printTelemetryVerbose(telemetry);
18258
18471
  await client.reportTelemetry(state.workspaceId, telemetry.events);
18259
- if (telemetry.healthReported)
18472
+ if (telemetry.healthReported) {
18260
18473
  state.lastHealthReportAt = new Date().toISOString();
18261
- state.reportedAgentSlugs = [
18262
- ...new Set([...state.reportedAgentSlugs ?? [], ...telemetry.newlyReportedAgents])
18263
- ];
18264
- writeFileSync29(statePath2, JSON.stringify(state, null, 2));
18474
+ writeFileSync29(statePath2, JSON.stringify(state, null, 2));
18475
+ }
18265
18476
  } catch {}
18266
- const summaryParts = [];
18267
- summaryParts.push(`${summary.adaptersProcessed} adapter${summary.adaptersProcessed === 1 ? "" : "s"}`);
18268
- if (summary.adaptersFailed > 0)
18269
- summaryParts.push(`${summary.adaptersFailed} failed`);
18270
- summaryParts.push(`${summary.skillWrites} skill write${summary.skillWrites === 1 ? "" : "s"} (${summary.skillFilesWritten} files)`);
18271
- if (summary.mcpServerWrites > 0)
18272
- summaryParts.push(`${summary.mcpServerWrites} MCP config write${summary.mcpServerWrites === 1 ? "" : "s"}`);
18273
- if (summary.hookInstallCalls > 0)
18274
- summaryParts.push(`${summary.hookInstallCalls} hook install${summary.hookInstallCalls === 1 ? "" : "s"}`);
18275
- summaryParts.push(`${summary.instructionHintWrites} instruction hint write${summary.instructionHintWrites === 1 ? "" : "s"}`);
18276
- console.log(`
18477
+ const failedNote = summary.adaptersFailed > 0 ? ` (${summary.adaptersFailed} failed)` : "";
18478
+ if (isVerbose()) {
18479
+ const summaryParts = [];
18480
+ summaryParts.push(`${summary.adaptersProcessed} adapter${summary.adaptersProcessed === 1 ? "" : "s"}`);
18481
+ if (summary.adaptersFailed > 0)
18482
+ summaryParts.push(`${summary.adaptersFailed} failed`);
18483
+ summaryParts.push(`${summary.skillWrites} skill write${summary.skillWrites === 1 ? "" : "s"} (${summary.skillFilesWritten} files)`);
18484
+ if (summary.mcpServerWrites > 0)
18485
+ summaryParts.push(`${summary.mcpServerWrites} MCP config write${summary.mcpServerWrites === 1 ? "" : "s"}`);
18486
+ if (summary.hookInstallCalls > 0)
18487
+ summaryParts.push(`${summary.hookInstallCalls} hook install${summary.hookInstallCalls === 1 ? "" : "s"}`);
18488
+ summaryParts.push(`${summary.instructionHintWrites} instruction hint write${summary.instructionHintWrites === 1 ? "" : "s"}`);
18489
+ console.log(`
18277
18490
  Summary: ${summaryParts.join(", ")}.`);
18491
+ } else {
18492
+ console.log(`
18493
+ ${summary.adaptersProcessed} agent${summary.adaptersProcessed === 1 ? "" : "s"} synced${failedNote}.`);
18494
+ }
18278
18495
  try {
18279
18496
  const cadence = loadCadenceState();
18280
18497
  const lastMs = cadence.lastReflectedAt ? new Date(cadence.lastReflectedAt).getTime() : 0;
@@ -18467,7 +18684,8 @@ Syncing workspace data...
18467
18684
  dryRun: false,
18468
18685
  pullOnly: true,
18469
18686
  yes: true,
18470
- prefer: "remote"
18687
+ prefer: "remote",
18688
+ initialSetup: true
18471
18689
  });
18472
18690
  }
18473
18691
  console.log("\nSetup complete. Run `runwork sync` anytime to refresh.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",