@notis_ai/cli 0.2.0-beta.154.1 → 0.2.0-beta.155.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.
@@ -979,7 +979,7 @@ var require_suggestSimilar = __commonJS({
979
979
  // node_modules/commander/lib/command.js
980
980
  var require_command = __commonJS({
981
981
  "node_modules/commander/lib/command.js"(exports) {
982
- var EventEmitter = __require("node:events").EventEmitter;
982
+ var EventEmitter2 = __require("node:events").EventEmitter;
983
983
  var childProcess = __require("node:child_process");
984
984
  var path3 = __require("node:path");
985
985
  var fs3 = __require("node:fs");
@@ -989,7 +989,7 @@ var require_command = __commonJS({
989
989
  var { Help: Help2 } = require_help();
990
990
  var { Option: Option2, DualOptions } = require_option();
991
991
  var { suggestSimilar } = require_suggestSimilar();
992
- var Command2 = class _Command extends EventEmitter {
992
+ var Command2 = class _Command extends EventEmitter2 {
993
993
  /**
994
994
  * Initialize a new `Command`.
995
995
  *
@@ -3796,15 +3796,22 @@ async function downloadSkillBundle(bundleUrl) {
3796
3796
  }
3797
3797
  return Buffer.from(await response.arrayBuffer());
3798
3798
  }
3799
- async function updateAgentTargets(serverUrl, jwt, skillId, targets) {
3799
+ async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt) {
3800
3800
  return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
3801
3801
  method: "PATCH",
3802
3802
  body: {
3803
3803
  skill_id: skillId,
3804
- agent_targets: targets
3804
+ agent_targets: targets,
3805
+ ...expectedUpdatedAt ? { expected_updated_at: expectedUpdatedAt } : {}
3805
3806
  }
3806
3807
  });
3807
3808
  }
3809
+ function agentFailureLabel(agent) {
3810
+ return AGENT_FAILURE_LABELS[agent] ?? agent;
3811
+ }
3812
+ function agentFolderFailureName(agent) {
3813
+ return `${agentFailureLabel(agent)} skills folder`;
3814
+ }
3808
3815
  async function removeForeignAccountSymlinks(skillsDir, options = {}) {
3809
3816
  const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
3810
3817
  const currentRoot = path2.resolve(skillsDir);
@@ -3865,8 +3872,9 @@ async function isManagedSymlink(linkPath, managedRoots) {
3865
3872
  const target = await fs2.readlink(linkPath);
3866
3873
  const resolvedTarget = path2.resolve(path2.dirname(linkPath), target);
3867
3874
  return managedRoots.some((root) => resolvedTarget === root || resolvedTarget.startsWith(`${root}${path2.sep}`));
3868
- } catch {
3869
- return false;
3875
+ } catch (error) {
3876
+ if (error?.code === "ENOENT") return false;
3877
+ throw error;
3870
3878
  }
3871
3879
  }
3872
3880
  async function ensureCorrectSymlink(linkPath, targetPath) {
@@ -3882,7 +3890,8 @@ async function ensureCorrectSymlink(linkPath, targetPath) {
3882
3890
  } else {
3883
3891
  return "blocked";
3884
3892
  }
3885
- } catch {
3893
+ } catch (error) {
3894
+ if (error?.code !== "ENOENT") throw error;
3886
3895
  }
3887
3896
  const relativePath = path2.relative(path2.dirname(linkPath), targetPath);
3888
3897
  await fs2.symlink(relativePath, linkPath);
@@ -3894,19 +3903,26 @@ function defaultAgentSkillDirs(skillsDir) {
3894
3903
  ...EXTERNAL_AGENT_SKILL_DIRS
3895
3904
  };
3896
3905
  }
3897
- async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots) {
3906
+ async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, failures, agent) {
3898
3907
  let removed = 0;
3899
3908
  try {
3900
3909
  await fs2.mkdir(agentDir, { recursive: true });
3901
3910
  const existingEntries = await fs2.readdir(agentDir, { withFileTypes: true });
3902
3911
  for (const entry of existingEntries) {
3903
3912
  const entryPath = path2.join(agentDir, entry.name);
3904
- if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
3905
- await fs2.unlink(entryPath);
3906
- removed += 1;
3913
+ try {
3914
+ if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
3915
+ await fs2.unlink(entryPath);
3916
+ removed += 1;
3917
+ }
3918
+ } catch (error) {
3919
+ if (error?.code !== "ENOENT") {
3920
+ failures.push({ name: entry.name, error: `${agentFailureLabel(agent)}: could not remove skill link (${error.message})` });
3921
+ }
3907
3922
  }
3908
3923
  }
3909
- } catch {
3924
+ } catch (error) {
3925
+ failures.push({ name: agentFolderFailureName(agent), error: `Could not read agent skills directory (${error.message})` });
3910
3926
  }
3911
3927
  return removed;
3912
3928
  }
@@ -3951,7 +3967,7 @@ async function detectDeletedAgentSymlinks(cloudSkills, previousState, skillsDir
3951
3967
  continue;
3952
3968
  }
3953
3969
  const previous = previousState.skills[skill.name];
3954
- if (!previous) {
3970
+ if (!previous || previous.cloudId !== skill.id || previous.verifiedAgentLinks?.[agent] !== true || !skill.updated_at || previous.cloudUpdatedAt !== skill.updated_at) {
3955
3971
  continue;
3956
3972
  }
3957
3973
  const cloudTargets = normalizeAgentTargets(skill.agent_targets);
@@ -3982,7 +3998,9 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
3982
3998
  const result = {
3983
3999
  linked: 0,
3984
4000
  removed: 0,
3985
- skipped: 0
4001
+ skipped: 0,
4002
+ verifiedAgentLinks: {},
4003
+ failures: []
3986
4004
  };
3987
4005
  const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
3988
4006
  const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
@@ -4017,21 +4035,34 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
4017
4035
  result.skipped += desiredByAgent[agent].size;
4018
4036
  continue;
4019
4037
  }
4020
- await fs2.mkdir(agentDir, { recursive: true });
4038
+ try {
4039
+ await fs2.mkdir(agentDir, { recursive: true });
4040
+ } catch (error) {
4041
+ result.failures.push({ name: agentFolderFailureName(agent), error: `Could not create agent skills directory (${error.message})` });
4042
+ continue;
4043
+ }
4021
4044
  const desiredSkills = desiredByAgent[agent];
4022
4045
  if (options.removeUndesired !== false) {
4023
- result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots);
4046
+ result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, result.failures, agent);
4024
4047
  }
4025
4048
  for (const skillName of desiredSkills) {
4026
4049
  const targetPath = path2.join(skillsDir, skillName);
4027
4050
  const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
4028
4051
  try {
4029
- await fs2.access(targetPath);
4052
+ if (!(await fs2.stat(path2.join(targetPath, "SKILL.md"))).isFile()) throw new Error("Missing SKILL.md");
4030
4053
  } catch {
4031
4054
  result.skipped += 1;
4055
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: SKILL.md is missing or unreadable` });
4056
+ continue;
4057
+ }
4058
+ let syncOutcome;
4059
+ try {
4060
+ syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
4061
+ } catch (error) {
4062
+ result.skipped += 1;
4063
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: could not create skill link (${error.message})` });
4032
4064
  continue;
4033
4065
  }
4034
- const syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
4035
4066
  if (syncOutcome === "linked") {
4036
4067
  result.linked += 1;
4037
4068
  } else if (syncOutcome === "blocked") {
@@ -4039,9 +4070,13 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
4039
4070
  `[skill-sync] Could not link "${skillName}" for ${agent}: non-symlink entry blocks ${linkPath}`
4040
4071
  );
4041
4072
  result.skipped += 1;
4073
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: an existing file or folder blocks the skill link` });
4042
4074
  } else {
4043
4075
  result.skipped += 1;
4044
4076
  }
4077
+ if (syncOutcome !== "blocked") {
4078
+ result.verifiedAgentLinks[skillName] = { ...result.verifiedAgentLinks[skillName], [agent]: true };
4079
+ }
4045
4080
  }
4046
4081
  }
4047
4082
  if (options.removeUndesired !== false && !Object.values(agentSkillDirs).some(
@@ -4050,7 +4085,9 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
4050
4085
  result.removed += await removeUndesiredManagedSymlinks(
4051
4086
  legacyGlobalSkillsDir,
4052
4087
  /* @__PURE__ */ new Set(),
4053
- managedRoots
4088
+ managedRoots,
4089
+ result.failures,
4090
+ "legacy"
4054
4091
  );
4055
4092
  }
4056
4093
  return result;
@@ -4149,7 +4186,7 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
4149
4186
  const localChangedSinceLastSync = !previous || previous.folderHash !== localSkill.folderHash;
4150
4187
  return !localChangedSinceLastSync && Boolean(cloudHash) && cloudHash !== localSkill.folderHash;
4151
4188
  }
4152
- function buildSyncState(pullResponse, localSkills, lastSyncedAt) {
4189
+ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}) {
4153
4190
  const localSkillMap = toSkillMap(localSkills);
4154
4191
  const skills = Object.fromEntries(
4155
4192
  pullResponse.skills.map((skill) => {
@@ -4160,6 +4197,8 @@ function buildSyncState(pullResponse, localSkills, lastSyncedAt) {
4160
4197
  cloudId: skill.id,
4161
4198
  folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
4162
4199
  agentTargets: normalizeAgentTargets(skill.agent_targets),
4200
+ verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
4201
+ cloudUpdatedAt: skill.updated_at,
4163
4202
  syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
4164
4203
  }
4165
4204
  ];
@@ -4215,7 +4254,7 @@ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
4215
4254
  skills: migratedSkills
4216
4255
  };
4217
4256
  }
4218
- async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps) {
4257
+ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = []) {
4219
4258
  const localSkillMap = toSkillMap(localSkills);
4220
4259
  const warnSkillSync = (message, error) => {
4221
4260
  console.warn(`[Notis] ${message}`, error);
@@ -4231,6 +4270,8 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
4231
4270
  onWarning: warnSkillSync
4232
4271
  })) {
4233
4272
  downloaded += 1;
4273
+ } else {
4274
+ failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
4234
4275
  }
4235
4276
  }
4236
4277
  return downloaded;
@@ -4258,25 +4299,42 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
4258
4299
  assertSkillsPullAuthorized(pullResponse);
4259
4300
  const previousState = await deps.readSyncState(syncPaths);
4260
4301
  const localSkills = await deps.scanLocalSkills(syncPaths);
4302
+ const failedDownloads = [];
4261
4303
  const downloaded = await writePulledSkillsToScopedMirror(
4262
4304
  pullResponse,
4263
4305
  localSkills,
4264
4306
  previousState,
4265
4307
  syncPaths,
4266
- deps
4308
+ deps,
4309
+ failedDownloads
4267
4310
  );
4268
4311
  const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
4269
4312
  const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
4270
4313
  const relinkSkillNames = new Set(options.relinkSkillNames || []);
4314
+ const failures = [...failedDownloads];
4315
+ const verifiedLinks = {};
4316
+ for (const skill of pullResponse.skills) {
4317
+ const previous = previousState.skills[skill.name];
4318
+ if (skill.updated_at && previous?.cloudId === skill.id && previous.cloudUpdatedAt === skill.updated_at) {
4319
+ verifiedLinks[skill.name] = { ...previous.verifiedAgentLinks };
4320
+ }
4321
+ }
4271
4322
  if (relinkSkillNames.size > 0) {
4272
- await deps.syncSymlinks(
4323
+ const relinked = await deps.syncSymlinks(
4273
4324
  pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
4274
4325
  syncPaths.skillsDir,
4275
4326
  { removeUndesired: false }
4276
4327
  );
4328
+ failures.push(...(relinked.failures ?? []).filter(
4329
+ (failure) => !failedDownloads.some((download) => download.name === failure.name)
4330
+ ));
4331
+ for (const name of relinkSkillNames) {
4332
+ verifiedLinks[name] = relinked.verifiedAgentLinks?.[name] ?? {};
4333
+ }
4277
4334
  }
4335
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
4278
4336
  await deps.writeSyncState(
4279
- buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
4337
+ buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt, verifiedLinks),
4280
4338
  syncPaths
4281
4339
  );
4282
4340
  return {
@@ -4284,10 +4342,11 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
4284
4342
  downloaded,
4285
4343
  deleted: 0,
4286
4344
  removed: 0,
4287
- lastSyncedAt
4345
+ lastSyncedAt,
4346
+ ...failures.length ? { failedLinks: failures } : {}
4288
4347
  };
4289
4348
  }
4290
- async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps) {
4349
+ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures) {
4291
4350
  if (isEmptySyncState(scopedState)) {
4292
4351
  return 0;
4293
4352
  }
@@ -4299,22 +4358,6 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
4299
4358
  if (deletions.length === 0) {
4300
4359
  return 0;
4301
4360
  }
4302
- const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
4303
- let fresh = null;
4304
- try {
4305
- fresh = await deps.pullSkills(serverUrl, jwt);
4306
- } catch (error) {
4307
- console.warn(
4308
- "[skill-sync] Could not re-pull latest agent targets before deactivation; using the top-of-sync snapshot.",
4309
- error
4310
- );
4311
- }
4312
- if (fresh) {
4313
- assertSkillsPullAuthorized(fresh);
4314
- for (const skill of fresh.skills) {
4315
- latestSkillsById.set(skill.id, skill);
4316
- }
4317
- }
4318
4361
  const agentsBySkill = /* @__PURE__ */ new Map();
4319
4362
  for (const deletion of deletions) {
4320
4363
  const entry = agentsBySkill.get(deletion.skillId) ?? {
@@ -4324,31 +4367,35 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
4324
4367
  entry.agents.add(deletion.agent);
4325
4368
  agentsBySkill.set(deletion.skillId, entry);
4326
4369
  }
4327
- const inMemoryById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
4370
+ const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
4371
+ assertSkillsPullAuthorized(fresh);
4372
+ Object.assign(pullResponse, fresh);
4373
+ let needsRefresh = false;
4328
4374
  let deactivated = 0;
4329
4375
  for (const [skillId, { skillName, agents }] of agentsBySkill) {
4330
- const latest = latestSkillsById.get(skillId);
4331
- if (!latest) {
4332
- continue;
4333
- }
4334
- const nextTargets = { ...normalizeAgentTargets(latest.agent_targets) };
4335
- for (const agent of agents) {
4336
- nextTargets[agent] = false;
4337
- }
4376
+ const skill = pullResponse.skills.find((item) => item.id === skillId);
4377
+ const previous = previousState.skills[skillName];
4378
+ if (!skill?.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
4379
+ const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
4338
4380
  try {
4339
- await deps.updateAgentTargets(serverUrl, jwt, skillId, nextTargets);
4340
- const inMemory = inMemoryById.get(skillId);
4341
- if (inMemory) {
4342
- inMemory.agent_targets = nextTargets;
4381
+ const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch, skill.updated_at);
4382
+ if (saved.success !== true || !saved.updated_at?.trim() || saved.updated_at === skill.updated_at || !["notis", "claude_code", "cursor", "codex"].every((agent) => typeof saved.agent_targets?.[agent] === "boolean") || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
4383
+ throw new Error("Assignment update did not return a verified saved revision");
4343
4384
  }
4385
+ skill.agent_targets = saved.agent_targets;
4386
+ skill.updated_at = saved.updated_at;
4344
4387
  deactivated += agents.size;
4345
4388
  } catch (error) {
4346
- console.warn(
4347
- `[skill-sync] Failed to deactivate "${skillName}" for ${[...agents].join(", ")} after local symlink deletion:`,
4348
- error
4349
- );
4389
+ needsRefresh = true;
4390
+ failures.push({ name: skillName, error: "Could not save the local agent removal; refreshed saved assignments" });
4391
+ console.warn(`[skill-sync] Assignment changed or could not be saved for "${skillName}"; refreshing before reconciliation.`, error);
4350
4392
  }
4351
4393
  }
4394
+ if (needsRefresh) {
4395
+ const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
4396
+ assertSkillsPullAuthorized(refreshed);
4397
+ Object.assign(pullResponse, refreshed);
4398
+ }
4352
4399
  return deactivated;
4353
4400
  }
4354
4401
  async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
@@ -4391,6 +4438,18 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
4391
4438
  pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
4392
4439
  );
4393
4440
  const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES2]);
4441
+ const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
4442
+ const assignmentFailures = [];
4443
+ const deactivated = syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
4444
+ serverUrl,
4445
+ jwt,
4446
+ pullResponse,
4447
+ scopedState,
4448
+ scopedState,
4449
+ syncPaths.skillsDir,
4450
+ deps,
4451
+ assignmentFailures
4452
+ ) : 0;
4394
4453
  const authUserId = decodeJwtSubject(jwt);
4395
4454
  let previousAuthState = null;
4396
4455
  if (authUserId && authUserId !== syncUserId) {
@@ -4405,22 +4464,12 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
4405
4464
  protectedSkillNames
4406
4465
  });
4407
4466
  const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES2.has(skill.name));
4408
- const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
4409
4467
  const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
4410
4468
  localSkills,
4411
4469
  scopedState,
4412
4470
  isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
4413
4471
  ));
4414
- const deactivated = await deactivateDeletedAgentSkills(
4415
- serverUrl,
4416
- jwt,
4417
- pullResponse,
4418
- previousState,
4419
- scopedState,
4420
- syncPaths.skillsDir,
4421
- deps
4422
- );
4423
- await deps.syncSymlinks(
4472
+ const gatheredSymlinkResult = await deps.syncSymlinks(
4424
4473
  buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
4425
4474
  syncPaths.skillsDir
4426
4475
  );
@@ -4451,21 +4500,25 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
4451
4500
  deleted += 1;
4452
4501
  }
4453
4502
  }
4503
+ const failedDownloads = [];
4454
4504
  const downloaded = await writePulledSkillsToScopedMirror(
4455
4505
  pullResponse,
4456
4506
  localSkills,
4457
4507
  previousState,
4458
4508
  syncPaths,
4459
- deps
4509
+ deps,
4510
+ failedDownloads
4460
4511
  );
4461
4512
  const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES2.has(skill.name));
4462
4513
  const symlinkResult = await deps.syncSymlinks(
4463
4514
  buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
4464
4515
  syncPaths.skillsDir
4465
4516
  );
4517
+ const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
4518
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
4466
4519
  const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
4467
4520
  await deps.writeSyncState(
4468
- buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
4521
+ buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt, verifiedLinks),
4469
4522
  syncPaths
4470
4523
  );
4471
4524
  return {
@@ -4475,14 +4528,17 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
4475
4528
  downloaded,
4476
4529
  deleted,
4477
4530
  deactivated,
4478
- linked: symlinkResult.linked,
4479
- removed: foreignLinksRemoved + symlinkResult.removed,
4531
+ linked: gatheredSymlinkResult.linked + symlinkResult.linked,
4532
+ removed: foreignLinksRemoved + gatheredSymlinkResult.removed + symlinkResult.removed,
4480
4533
  skipped: symlinkResult.skipped,
4534
+ failedLinks: [...assignmentFailures, ...failedDownloads, ...(symlinkResult.failures ?? []).filter(
4535
+ (failure) => !failedDownloads.some((download) => download.name === failure.name)
4536
+ )],
4481
4537
  lastSyncedAt,
4482
4538
  failedPushes
4483
4539
  };
4484
4540
  }
4485
- var DEFAULT_AGENT_TARGETS, HOME_DIR, execFileAsync, AGENTS_DIR, LEGACY_AGENTS_SKILLS_DIR, NOTIS_SKILL_SYNC_ROOT, LEGACY_NOTIS_SYNC_STATE_PATH, AGENTS_SKILLS_DIR, SKILL_LOCK_PATH, DEFAULT_SYNC_STATE, EXCLUDED_TOP_LEVEL_ROOT_NAMES, DEFAULT_SYNC_PATHS, HOME_DIR2, EXTERNAL_AGENT_SKILL_DIRS, EXTERNAL_AGENTS, BASE_SKILL_NAMES2, DEFAULT_RUN_SKILL_SYNC_DEPS;
4541
+ var DEFAULT_AGENT_TARGETS, HOME_DIR, execFileAsync, AGENTS_DIR, LEGACY_AGENTS_SKILLS_DIR, NOTIS_SKILL_SYNC_ROOT, LEGACY_NOTIS_SYNC_STATE_PATH, AGENTS_SKILLS_DIR, SKILL_LOCK_PATH, DEFAULT_SYNC_STATE, EXCLUDED_TOP_LEVEL_ROOT_NAMES, DEFAULT_SYNC_PATHS, HOME_DIR2, EXTERNAL_AGENT_SKILL_DIRS, EXTERNAL_AGENTS, AGENT_FAILURE_LABELS, BASE_SKILL_NAMES2, DEFAULT_RUN_SKILL_SYNC_DEPS;
4486
4542
  var init_skill_sync = __esm({
4487
4543
  "dist/skill-sync/index.js"() {
4488
4544
  DEFAULT_AGENT_TARGETS = {
@@ -4541,6 +4597,12 @@ var init_skill_sync = __esm({
4541
4597
  codex: path2.join(HOME_DIR2, ".codex", "skills")
4542
4598
  };
4543
4599
  EXTERNAL_AGENTS = Object.keys(EXTERNAL_AGENT_SKILL_DIRS);
4600
+ AGENT_FAILURE_LABELS = {
4601
+ claude_code: "Claude Code",
4602
+ cursor: "Cursor",
4603
+ codex: "Codex",
4604
+ legacy: "legacy ~/.agents/skills"
4605
+ };
4544
4606
  BASE_SKILL_NAMES2 = /* @__PURE__ */ new Set(["notis-apps", "notis-query", "notis-cli"]);
4545
4607
  DEFAULT_RUN_SKILL_SYNC_DEPS = {
4546
4608
  fetchSyncSettings,
@@ -4566,7 +4628,7 @@ var init_skill_sync = __esm({
4566
4628
  // src/cli.js
4567
4629
  import { readFileSync as readFileSync21 } from "node:fs";
4568
4630
  import { dirname as dirname18, join as join19 } from "node:path";
4569
- import { fileURLToPath as fileURLToPath10 } from "node:url";
4631
+ import { fileURLToPath as fileURLToPath11 } from "node:url";
4570
4632
 
4571
4633
  // node_modules/commander/esm.mjs
4572
4634
  var import_index = __toESM(require_commander(), 1);
@@ -7721,7 +7783,7 @@ async function directDeploy(projectDir, appId) {
7721
7783
 
7722
7784
  // src/runtime/app-dev-server.js
7723
7785
  import { createServer } from "node:http";
7724
- import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
7786
+ import { execFileSync as execFileSync2, spawn as spawn3 } from "node:child_process";
7725
7787
  import {
7726
7788
  appendFileSync,
7727
7789
  existsSync as existsSync7,
@@ -7735,7 +7797,7 @@ import {
7735
7797
  import { freemem, loadavg, totalmem } from "node:os";
7736
7798
  import { dirname as dirname6, join as join7, resolve as resolve5 } from "node:path";
7737
7799
  import { setTimeout as delay } from "node:timers/promises";
7738
- import { fileURLToPath as fileURLToPath3 } from "node:url";
7800
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
7739
7801
 
7740
7802
  // src/runtime/app-dev-sessions.js
7741
7803
  import { randomUUID as randomUUID3 } from "node:crypto";
@@ -8900,6 +8962,40 @@ function captureDesktopHostOwnership({
8900
8962
  };
8901
8963
  }
8902
8964
 
8965
+ // src/runtime/app-dev-build.js
8966
+ import { spawn as spawn2 } from "node:child_process";
8967
+ import { EventEmitter } from "node:events";
8968
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8969
+ async function startAppDevBuild(cwd, command = "npm", args = ["run", "build", "--", "--watch"]) {
8970
+ const supervisor = spawn2(process.execPath, [fileURLToPath3(new URL("./app-dev-build-supervisor.js", import.meta.url))], {
8971
+ cwd,
8972
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
8973
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: "1", NOTIS_DEV: "1" }
8974
+ });
8975
+ const build = new EventEmitter();
8976
+ build.pid = null;
8977
+ build.exitCode = null;
8978
+ build.signalCode = null;
8979
+ build.kill = (signal = "SIGTERM") => supervisor.kill(signal);
8980
+ await new Promise((resolve10, reject) => {
8981
+ supervisor.once("error", reject);
8982
+ supervisor.once("exit", (code, signal) => {
8983
+ build.exitCode = code;
8984
+ build.signalCode = signal;
8985
+ reject(new Error(`Build supervisor exited before startup (${code ?? signal})`));
8986
+ build.emit("exit", code, signal);
8987
+ });
8988
+ supervisor.once("message", ({ pid }) => {
8989
+ build.pid = pid;
8990
+ resolve10();
8991
+ });
8992
+ supervisor.send({ command, args }, (error) => {
8993
+ if (error) reject(error);
8994
+ });
8995
+ });
8996
+ return build;
8997
+ }
8998
+
8903
8999
  // src/runtime/app-dev-server.js
8904
9000
  var CONTENT_TYPES = {
8905
9001
  ".js": "application/javascript; charset=utf-8",
@@ -8907,7 +9003,7 @@ var CONTENT_TYPES = {
8907
9003
  ".map": "application/json; charset=utf-8"
8908
9004
  };
8909
9005
  var MAX_JSON_BODY_BYTES = 64 * 1024;
8910
- var RUNTIME_DIR = dirname6(fileURLToPath3(import.meta.url));
9006
+ var RUNTIME_DIR = dirname6(fileURLToPath4(import.meta.url));
8911
9007
  var CLI_ROOT2 = resolve5(RUNTIME_DIR, "../..");
8912
9008
  var REPO_ROOT = resolve5(RUNTIME_DIR, "../../../..");
8913
9009
  var HARNESS_TEMPLATE_PATH = join7(CLI_ROOT2, "template", ".harness", "index.html.tmpl");
@@ -8948,7 +9044,7 @@ function readProcessGroupRssBytes(groupPids) {
8948
9044
  async function terminateBuildProcessTree(child, {
8949
9045
  platform = process.platform,
8950
9046
  signalProcess = process.kill,
8951
- spawnProcess = spawn2,
9047
+ spawnProcess = spawn3,
8952
9048
  graceMs = BUILD_PROCESS_STOP_GRACE_MS
8953
9049
  } = {}) {
8954
9050
  const pid = child?.pid;
@@ -9755,12 +9851,7 @@ data: ${JSON.stringify({ slug, at: Date.now() })}
9755
9851
  await prepareArtifactBuild(state.projectDir);
9756
9852
  watchManifestInputs(state);
9757
9853
  pollForBundleAndWatch(state);
9758
- const buildProcess = spawn2("npm", ["run", "build", "--", "--watch"], {
9759
- cwd: state.projectDir,
9760
- detached: process.platform !== "win32",
9761
- stdio: "inherit",
9762
- env: { ...process.env, NOTIS_DEV: "1" }
9763
- });
9854
+ const buildProcess = await startAppDevBuild(state.projectDir);
9764
9855
  state.buildProcess = buildProcess;
9765
9856
  for (let attempt = 0; attempt < 5 && !state.watcherOwnership; attempt += 1) {
9766
9857
  state.watcherOwnership = captureDesktopWatcherOwnership({
@@ -10122,7 +10213,7 @@ function discoverRegisteredAppProjects(options = {}) {
10122
10213
  }
10123
10214
 
10124
10215
  // src/runtime/agent-browser.js
10125
- import { spawn as spawn3, spawnSync } from "node:child_process";
10216
+ import { spawn as spawn4, spawnSync } from "node:child_process";
10126
10217
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
10127
10218
  import { dirname as dirname8 } from "node:path";
10128
10219
  var PREPARE_CAPTURE_SCRIPT = "(() => { const s = document.getElementById('harness-status'); if (s) s.style.display = 'none'; const r = document.getElementById('root'); if (r) { r.style.paddingTop = '0'; r.style.minHeight = '0'; } document.body.style.minHeight = '0'; document.documentElement.style.minHeight = '0'; return true; })()";
@@ -10215,7 +10306,7 @@ function commandError(phase, result) {
10215
10306
  }
10216
10307
  function runAgentBrowser(args, { timeoutMs = 3e4 } = {}) {
10217
10308
  return new Promise((resolvePromise) => {
10218
- const child = spawn3("agent-browser", args, {
10309
+ const child = spawn4("agent-browser", args, {
10219
10310
  stdio: ["ignore", "pipe", "pipe"],
10220
10311
  env: process.env
10221
10312
  });
@@ -10748,9 +10839,9 @@ function getCliMode() {
10748
10839
  }
10749
10840
 
10750
10841
  // src/runtime/store-screenshot.js
10751
- import { fileURLToPath as fileURLToPath4 } from "node:url";
10842
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
10752
10843
  var STORE_SCREENSHOT_ASPECT = 16 / 10;
10753
- var SHARED_BACKDROP_PATH = fileURLToPath4(
10844
+ var SHARED_BACKDROP_PATH = fileURLToPath5(
10754
10845
  new URL("./assets/store-screenshot-dark.png", import.meta.url)
10755
10846
  );
10756
10847
  var ACCENT_PALETTES = {
@@ -10924,8 +11015,8 @@ import {
10924
11015
  import { createServer as createServer3 } from "node:http";
10925
11016
  import { homedir as homedir6 } from "node:os";
10926
11017
  import { basename as basename2, dirname as dirname10, join as join11 } from "node:path";
10927
- import { fileURLToPath as fileURLToPath5 } from "node:url";
10928
- import { execFileSync as execFileSync3, spawn as spawn4 } from "node:child_process";
11018
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
11019
+ import { execFileSync as execFileSync3, spawn as spawn5 } from "node:child_process";
10929
11020
  import { createInterface } from "node:readline/promises";
10930
11021
  var DEFAULT_CLI_OAUTH_SCOPES = [
10931
11022
  "notis:read",
@@ -11462,7 +11553,7 @@ function browserOpenCommand(url, platform = process.platform) {
11462
11553
  function openBrowser(url) {
11463
11554
  const { command, args } = browserOpenCommand(url);
11464
11555
  try {
11465
- const child = spawn4(command, args, { detached: true, stdio: "ignore" });
11556
+ const child = spawn5(command, args, { detached: true, stdio: "ignore" });
11466
11557
  child.on("error", () => {
11467
11558
  });
11468
11559
  child.unref();
@@ -11638,7 +11729,7 @@ async function publishForegroundAuthorization(runtime, authorization) {
11638
11729
  releaseListenerGlobalLock(globalLock);
11639
11730
  }
11640
11731
  }
11641
- var LISTENER_SCRIPT = fileURLToPath5(new URL("./login-listener.js", import.meta.url));
11732
+ var LISTENER_SCRIPT = fileURLToPath6(new URL("./login-listener.js", import.meta.url));
11642
11733
  var LISTENER_SCRIPT_NAME = "login-listener.js";
11643
11734
  var LISTENER_HANDSHAKE_TIMEOUT_MS = 1e4;
11644
11735
  var LISTENER_START_LOCK_STALE_MS = LISTENER_HANDSHAKE_TIMEOUT_MS * 2;
@@ -11938,7 +12029,7 @@ async function startDetachedLoopbackListener(runtime, {
11938
12029
  }
11939
12030
  let child;
11940
12031
  try {
11941
- child = spawn4(process.execPath, [LISTENER_SCRIPT, payloadFile, identityToken], {
12032
+ child = spawn5(process.execPath, [LISTENER_SCRIPT, payloadFile, identityToken], {
11942
12033
  detached: true,
11943
12034
  windowsHide: true,
11944
12035
  // An IPC channel only so the child can report the port it bound. Every
@@ -16036,6 +16127,11 @@ function classifySqlMutation(query) {
16036
16127
  }
16037
16128
  function classifyToolMutation(toolName, args = {}) {
16038
16129
  const normalized = localNotisToolSlug(toolName).toUpperCase();
16130
+ if (normalized === "LOCAL_NOTIS_SANDBOX_PREVIEW") {
16131
+ if (args.action === "status") return false;
16132
+ if (["register", "open", "heartbeat", "stop"].includes(args.action)) return true;
16133
+ return null;
16134
+ }
16039
16135
  if (normalized.includes("EXECUTE_SQL") && typeof args.query === "string") {
16040
16136
  const query = args.query.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ").trim();
16041
16137
  const sqlClassification = classifySqlMutation(query);
@@ -16504,7 +16600,7 @@ var toolsCommandSpecs = [
16504
16600
 
16505
16601
  // src/command-specs/meta.js
16506
16602
  import { dirname as dirname12 } from "node:path";
16507
- import { fileURLToPath as fileURLToPath6 } from "node:url";
16603
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
16508
16604
 
16509
16605
  // src/runtime/help.js
16510
16606
  function canonicalCommandName(spec) {
@@ -16567,7 +16663,7 @@ function doctorToolRoundtripRuntime(runtime) {
16567
16663
  timeoutMs: Math.max(runtime.timeoutMs || 0, DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS)
16568
16664
  };
16569
16665
  }
16570
- function doctorChannelSummary(runtime, moduleDirectory = dirname12(fileURLToPath6(import.meta.url))) {
16666
+ function doctorChannelSummary(runtime, moduleDirectory = dirname12(fileURLToPath7(import.meta.url))) {
16571
16667
  const decision = resolveChannelSwitch({
16572
16668
  runningVersion: runtime.cliVersion,
16573
16669
  profile: {
@@ -16782,7 +16878,7 @@ var metaCommandSpecs = [
16782
16878
  // src/command-specs/onboarding.js
16783
16879
  import { readFileSync as readFileSync17 } from "node:fs";
16784
16880
  import { dirname as dirname15, join as join15 } from "node:path";
16785
- import { fileURLToPath as fileURLToPath8 } from "node:url";
16881
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
16786
16882
 
16787
16883
  // src/command-specs/agents.js
16788
16884
  import { createHash as createHash6 } from "node:crypto";
@@ -16801,8 +16897,8 @@ import {
16801
16897
  import { createHash as createHash4 } from "node:crypto";
16802
16898
  import { homedir as homedir7 } from "node:os";
16803
16899
  import { dirname as dirname13, join as join13 } from "node:path";
16804
- import { fileURLToPath as fileURLToPath7 } from "node:url";
16805
- var HERE = dirname13(fileURLToPath7(import.meta.url));
16900
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
16901
+ var HERE = dirname13(fileURLToPath8(import.meta.url));
16806
16902
  var INSTRUCTIONS_PATH = join13(HERE, "..", "..", "skills", "notis-cli", "AGENT_INSTRUCTIONS.md");
16807
16903
  var HOOK_BUNDLE_PATH = join13(HERE, "..", "..", "dist", "agent-hooks", "notis-agent-hook.mjs");
16808
16904
  var START_MARKER = "<!-- notis-cli:instructions:start -->";
@@ -17641,7 +17737,7 @@ var authCommandSpecs = [
17641
17737
  ];
17642
17738
 
17643
17739
  // src/command-specs/onboarding.js
17644
- var HERE2 = dirname15(fileURLToPath8(import.meta.url));
17740
+ var HERE2 = dirname15(fileURLToPath9(import.meta.url));
17645
17741
  var BUNDLED_BRIEF_PATH = join15(HERE2, "..", "..", "skills", "notis-onboarding", "BRIEF.md");
17646
17742
  async function fetchOnboardingState(runtime) {
17647
17743
  try {
@@ -19536,13 +19632,13 @@ import {
19536
19632
  } from "node:fs";
19537
19633
  import { homedir as homedir9 } from "node:os";
19538
19634
  import { dirname as dirname16, join as join17, relative as relative5, resolve as resolve9 } from "node:path";
19539
- import { fileURLToPath as fileURLToPath9 } from "node:url";
19635
+ import { fileURLToPath as fileURLToPath10 } from "node:url";
19540
19636
  var BASE_SKILL_NAMES = Object.freeze([
19541
19637
  "notis-apps",
19542
19638
  "notis-query",
19543
19639
  "notis-cli"
19544
19640
  ]);
19545
- var HERE3 = dirname16(fileURLToPath9(import.meta.url));
19641
+ var HERE3 = dirname16(fileURLToPath10(import.meta.url));
19546
19642
  function resolveBundledBaseSkillsRoot({ resourcesPath = process.resourcesPath } = {}) {
19547
19643
  const sourceCheckout = resolve9(HERE3, "../../../../server/skills");
19548
19644
  const packaged = resolve9(HERE3, "../../dist/base-skills");
@@ -19850,11 +19946,12 @@ async function syncSkillsHandler(ctx) {
19850
19946
  honorSyncEnabled: Boolean(ctx.options.electronRepeat),
19851
19947
  runAccountSync: runSkillSync2
19852
19948
  });
19949
+ const failures = [...result.failedPushes || [], ...result.failedLinks || []];
19853
19950
  return ctx.output.emitSuccess({
19854
19951
  command: "skills sync",
19855
19952
  data: result,
19856
- humanSummary: result.syncEnabled ? `Synced account skills and kept ${result.baseSkills.length} base skills current.` : `Automatic Desktop sync is off; kept ${result.baseSkills.length} base skills current.`,
19857
- renderHuman: () => result.syncEnabled ? `Skills synced. Base skills current: ${result.baseSkills.join(", ")}.` : `Automatic Desktop sync is off. Base skills remain current: ${result.baseSkills.join(", ")}.`
19953
+ humanSummary: failures.length ? `Skill sync completed with ${failures.length} reported failures; inspect failedPushes and failedLinks.` : result.syncEnabled ? `Synced account skills and kept ${result.baseSkills.length} base skills current.` : `Automatic Desktop sync is off; kept ${result.baseSkills.length} base skills current.`,
19954
+ renderHuman: () => failures.length ? `Skill sync needs attention: ${failures.map((failure) => `${failure.name}: ${failure.error}`).join("; ")}` : result.syncEnabled ? `Skills synced. Base skills current: ${result.baseSkills.join(", ")}.` : `Automatic Desktop sync is off. Base skills remain current: ${result.baseSkills.join(", ")}.`
19858
19955
  });
19859
19956
  }
19860
19957
  var skillsCommandSpecs = [
@@ -19990,7 +20087,7 @@ async function reportCliCommand({
19990
20087
  // src/cli.js
19991
20088
  function readCliVersion() {
19992
20089
  try {
19993
- const manifestPath = join19(dirname18(fileURLToPath10(import.meta.url)), "..", "package.json");
20090
+ const manifestPath = join19(dirname18(fileURLToPath11(import.meta.url)), "..", "package.json");
19994
20091
  const version = JSON.parse(readFileSync21(manifestPath, "utf-8")).version;
19995
20092
  if (typeof version === "string" && version.trim()) {
19996
20093
  return version.trim();