@dayofweek/dcli 1.6.0 → 1.7.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.
package/README.md CHANGED
@@ -72,6 +72,34 @@ Updates overwrite only files whose installed hash still matches the managed mani
72
72
 
73
73
  The legacy entity/proposal Agent Skill remains available through `dcli skill install` without a bundle name.
74
74
 
75
+ ### Shared skills
76
+
77
+ Beyond the named bundles above, users can publish their own skills into a shared knowledge area. Anyone who can read the area — or, with `--visibility company`, anyone in the publishing area's organization — can then discover, install, and update the skill. Service administrators can additionally publish with `--visibility global`, making a skill a shared starter for every user of the service; the desktop app syncs global skills into managed wikis automatically.
78
+
79
+ ```bash
80
+ # Publish a local skill directory (name/version come from SKILL.md frontmatter)
81
+ dcli skill publish --area <areaId> --dir .agents/skills/meeting-notes --visibility company
82
+
83
+ # Discover: --shared adds skills shared with you to the bundle listing
84
+ dcli skill list --shared --json
85
+
86
+ # Install by canonical URI, or by name + area
87
+ dcli skill install 'dayofweek://brain/<areaId>/skill/<skillId>'
88
+ dcli skill install meeting-notes --area <areaId>
89
+
90
+ # Update goes back to wherever the install came from
91
+ dcli skill update meeting-notes --area <areaId>
92
+ dcli skill update --dir .agents/skills/meeting-notes
93
+
94
+ # Check for newer versions without writing anything
95
+ dcli skill status meeting-notes --dir .agents/skills/meeting-notes --check
96
+
97
+ # Retire a shared skill you own
98
+ dcli skill archive 'dayofweek://brain/<areaId>/skill/<skillId>'
99
+ ```
100
+
101
+ Shared skills go through the same integrity pipeline as named bundles: per-file SHA-256 plus a manifest hash computed server-side and re-verified locally before anything touches disk, the same path-safety rules, and the same conflict handling — locally edited files are never overwritten. The install origin is recorded in `.dayofweek-skill.json`, so `skill update` and `skill status --check` know whether to ask the bundle endpoint or the shared-skill endpoint. Publishing requires write access to the area; `--visibility company` and `skill archive` require ownership.
102
+
75
103
  ## URI contract
76
104
 
77
105
  `dcli` accepts only exact canonical resource forms:
package/dist/bin/dcli.js CHANGED
@@ -4,13 +4,13 @@ import { ApiError, DayOfWeekClient, toArrayBuffer } from "../client.js";
4
4
  import { getToken, getApiUrl, saveConfig, loadConfig, saveCredential, deleteCredential } from "../config.js";
5
5
  import { browserLogin } from "../auth/login.js";
6
6
  import { parseBrainResource } from "../uri.js";
7
- import { accessSync, constants, mkdirSync, readFileSync, existsSync, statSync, writeFileSync } from "node:fs";
7
+ import { accessSync, constants, mkdirSync, readdirSync, readFileSync, existsSync, statSync, writeFileSync } from "node:fs";
8
8
  import { join, basename, resolve, relative, sep } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import pkg from "../../package.json" with { type: "json" };
12
12
  import { createHash } from "node:crypto";
13
- import { validateSkillBundle, writeSkillBundle } from "../skills.js";
13
+ import { readInstalledSkill, validateSkillBundle, writeSkillBundle } from "../skills.js";
14
14
  const program = new Command()
15
15
  .name("dcli")
16
16
  .description("CLI for the Day of Week AgTech platform")
@@ -768,53 +768,207 @@ function parseTarget(value) {
768
768
  }
769
769
  return v;
770
770
  }
771
+ function sharedToBundle(shared) {
772
+ return {
773
+ bundle: { name: shared.name, version: shared.version, hash: shared.hash, files: shared.files },
774
+ origin: { source: "shared", skillId: shared.id, areaId: shared.areaId, uri: shared.uri },
775
+ };
776
+ }
777
+ async function fetchSkillByOrigin(client, nameOrUri, areaId) {
778
+ if (nameOrUri?.includes("://")) {
779
+ const resource = parseBrainResource(nameOrUri);
780
+ if (resource.resourceType !== "skill" || !resource.resourceId)
781
+ throw new Error("A shared skill URI is required");
782
+ const shared = await client.getSharedSkill(resource.resourceId);
783
+ if (shared.areaId !== resource.areaId)
784
+ throw new Error("Server returned a mismatched area");
785
+ return sharedToBundle(shared);
786
+ }
787
+ if (areaId) {
788
+ if (!nameOrUri)
789
+ throw new Error("A skill name is required together with --area");
790
+ const match = (await client.listSharedSkills()).find((candidate) => candidate.areaId === areaId && candidate.name === nameOrUri);
791
+ if (!match)
792
+ throw new Error("Shared skill not found in that area");
793
+ return sharedToBundle(await client.getSharedSkill(match.id));
794
+ }
795
+ return { bundle: await client.getSkillBundle(nameOrUri), origin: { source: "platform" } };
796
+ }
797
+ async function installOrUpdateSkill(action, name, opts) {
798
+ const client = getClient();
799
+ let resolved;
800
+ if (action === "update" && !name && !opts.area && opts.dir) {
801
+ // Bare `skill update --dir …`: go back to wherever this install came from.
802
+ const installed = readInstalledSkill(opts.dir);
803
+ if (installed) {
804
+ resolved = installed.origin?.source === "shared"
805
+ ? sharedToBundle(await client.getSharedSkill(installed.origin.skillId))
806
+ : { bundle: await client.getSkillBundle(installed.name), origin: { source: "platform" } };
807
+ }
808
+ }
809
+ resolved = resolved ?? await fetchSkillByOrigin(client, name, opts.area);
810
+ const bundle = validateSkillBundle(resolved.bundle);
811
+ const dirs = resolveTargetDirs(parseTarget(opts.target), bundle.name, opts.dir);
812
+ const installations = [];
813
+ for (const dir of dirs) {
814
+ const result = writeSkillBundle(bundle, dir, resolved.origin);
815
+ installations.push({ directory: dir, ...result });
816
+ }
817
+ output({
818
+ action,
819
+ bundle: bundle.name,
820
+ version: bundle.version,
821
+ hash: bundle.hash,
822
+ origin: resolved.origin.source,
823
+ ...(resolved.origin.source === "shared" ? { uri: resolved.origin.uri } : {}),
824
+ installations,
825
+ });
826
+ }
827
+ /** Read a skill directory into bundle files: relative paths, utf-8 content. */
828
+ function collectSkillFiles(dir) {
829
+ const root = resolve(dir);
830
+ if (!existsSync(root) || !statSync(root).isDirectory())
831
+ throw new Error("Skill directory not found");
832
+ const files = [];
833
+ const walk = (current, prefix) => {
834
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
835
+ // Hidden entries (including the .dayofweek-skill.json install manifest)
836
+ // and update-conflict artifacts never belong in a published bundle.
837
+ if (entry.name.startsWith(".") || /\.new(\.\d+)?$/.test(entry.name))
838
+ continue;
839
+ if (entry.isSymbolicLink())
840
+ throw new Error("Symlinks are not allowed in a skill directory");
841
+ const full = join(current, entry.name);
842
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
843
+ if (entry.isDirectory())
844
+ walk(full, relativePath);
845
+ else if (entry.isFile())
846
+ files.push({ path: relativePath, content: readFileSync(full, "utf8") });
847
+ }
848
+ };
849
+ walk(root, "");
850
+ return files.sort((left, right) => left.path.localeCompare(right.path));
851
+ }
771
852
  skill
772
853
  .command("list")
773
- .description("List authenticated named skill bundles")
774
- .action(async () => output(await getClient().listSkillBundles()));
854
+ .description("List installable named skill bundles; --shared adds skills shared with you")
855
+ .option("--shared", "Include skills other users shared with you")
856
+ .action(async (opts) => {
857
+ const client = getClient();
858
+ // The bare listing stays exactly the named bundles: managed installers
859
+ // iterate it and fetch every entry by plain name, so shared skills (which
860
+ // resolve by URI) only appear when explicitly asked for.
861
+ const listings = (await client.listSkillBundles()).map((bundle) => ({
862
+ ...bundle,
863
+ source: "platform",
864
+ }));
865
+ if (opts.shared) {
866
+ try {
867
+ for (const shared of await client.listSharedSkills()) {
868
+ listings.push({
869
+ name: shared.name,
870
+ version: shared.version,
871
+ hash: shared.hash,
872
+ source: "shared",
873
+ areaId: shared.areaId,
874
+ areaName: shared.areaName,
875
+ visibility: shared.visibility,
876
+ description: shared.description,
877
+ uri: shared.uri,
878
+ updatedAt: shared.updatedAt,
879
+ });
880
+ }
881
+ }
882
+ catch (error) {
883
+ // Older servers or tokens without knowledge scopes have no shared-skill
884
+ // surface — the named bundles are still worth listing.
885
+ console.error(`Shared skills unavailable: ${error instanceof Error ? error.message : String(error)}`);
886
+ }
887
+ }
888
+ output(listings.sort((left, right) => left.name.localeCompare(right.name)));
889
+ });
775
890
  skill
776
- .command("bundle <name>")
777
- .description("Fetch and verify a named skill bundle for a managed installer")
778
- .action(async (name) => output(validateSkillBundle(await getClient().getSkillBundle(name))));
891
+ .command("bundle <nameOrUri>")
892
+ .description("Fetch and verify a skill bundle (by name, or shared-skill URI) for a managed installer")
893
+ .action(async (nameOrUri) => {
894
+ if (nameOrUri.includes("://")) {
895
+ const resource = parseBrainResource(nameOrUri);
896
+ if (resource.resourceType !== "skill" || !resource.resourceId)
897
+ throw new Error("A shared skill URI is required");
898
+ const shared = await getClient().getSharedSkill(resource.resourceId);
899
+ if (shared.areaId !== resource.areaId)
900
+ throw new Error("Server returned a mismatched area");
901
+ // Emit the plain bundle shape managed installers expect.
902
+ output(validateSkillBundle({ name: shared.name, version: shared.version, hash: shared.hash, files: shared.files }));
903
+ return;
904
+ }
905
+ output(validateSkillBundle(await getClient().getSkillBundle(nameOrUri)));
906
+ });
779
907
  skill
780
908
  .command("install [name]")
781
- .description("Install the agent skill (requires valid auth)")
909
+ .description("Install a skill by bundle name, shared-skill URI, or name + --area")
782
910
  .option("--dir <path>", "Custom install directory (overrides --target)")
783
911
  .option("--target <target>", "Install target: agents, claude, or all (default: all)")
784
- .action(async (name, opts) => {
785
- const client = getClient();
786
- const bundle = await client.getSkillBundle(name);
787
- const target = parseTarget(opts.target);
788
- const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
789
- const installations = [];
790
- for (const dir of dirs) {
791
- const result = writeSkillBundle(bundle, dir);
792
- installations.push({ directory: dir, ...result });
793
- }
794
- output({ action: "install", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
795
- });
912
+ .option("--area <areaId>", "Install a shared skill from this area")
913
+ .action(async (name, opts) => installOrUpdateSkill("install", name, opts));
796
914
  skill
797
915
  .command("update [name]")
798
- .description("Update the skill to the latest version")
916
+ .description("Update a skill to the latest version from its origin")
799
917
  .option("--dir <path>", "Custom install directory (overrides --target)")
800
918
  .option("--target <target>", "Install target: agents, claude, or all (default: all)")
801
- .action(async (name, opts) => {
802
- const client = getClient();
803
- const bundle = await client.getSkillBundle(name);
804
- const target = parseTarget(opts.target);
805
- const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
806
- const installations = [];
807
- for (const dir of dirs) {
808
- const result = writeSkillBundle(bundle, dir);
809
- installations.push({ directory: dir, ...result });
919
+ .option("--area <areaId>", "Update a shared skill from this area")
920
+ .action(async (name, opts) => installOrUpdateSkill("update", name, opts));
921
+ skill
922
+ .command("publish")
923
+ .description("Share a skill directory to a knowledge area so teammates can install it")
924
+ .requiredOption("--area <areaId>", "Owning area (see: dcli brain list)")
925
+ .requiredOption("--dir <path>", "Skill directory containing SKILL.md")
926
+ .option("--name <name>", "Skill name (default: SKILL.md frontmatter, else the directory name)")
927
+ .option("--skill-version <version>", "Version string (default: SKILL.md frontmatter version)")
928
+ .option("--visibility <visibility>", "Who can discover it: area (members only), company, or global (admins; every user)")
929
+ .option("--description <text>", "Short description shown in listings")
930
+ .action(async (opts) => {
931
+ const visibility = (opts.visibility ?? "area").toLowerCase();
932
+ if (visibility !== "area" && visibility !== "company" && visibility !== "global") {
933
+ throw new Error("Invalid --visibility: use area, company, or global");
810
934
  }
811
- output({ action: "update", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
935
+ const files = collectSkillFiles(opts.dir);
936
+ const skillMd = files.find((file) => file.path === "SKILL.md");
937
+ if (!skillMd)
938
+ throw new Error("The skill directory must contain SKILL.md");
939
+ const name = opts.name
940
+ ?? skillMd.content.match(/^name:\s*"?([a-z0-9][a-z0-9-]*)"?\s*$/m)?.[1]
941
+ ?? basename(resolve(opts.dir));
942
+ const version = opts.skillVersion ?? skillMd.content.match(/version:\s*"([^"]+)"/)?.[1] ?? "1.0.0";
943
+ validateSkillBundle({ name, version, files });
944
+ const result = await getClient().publishSharedSkill({
945
+ areaId: opts.area,
946
+ name,
947
+ version,
948
+ description: opts.description,
949
+ visibility,
950
+ files,
951
+ });
952
+ output({ action: "publish", ...result });
953
+ });
954
+ skill
955
+ .command("archive <uri>")
956
+ .description("Archive a shared skill you own (removes it from listing and install)")
957
+ .action(async (uri) => {
958
+ const resource = parseBrainResource(uri);
959
+ if (resource.resourceType !== "skill" || !resource.resourceId)
960
+ throw new Error("A shared skill URI is required");
961
+ const result = await getClient().archiveSharedSkill(resource.resourceId);
962
+ if (result.areaId !== resource.areaId)
963
+ throw new Error("Server returned a mismatched area");
964
+ output({ action: "archive", ...result });
812
965
  });
813
966
  skill
814
967
  .command("status [name]")
815
- .description("Check if the skill is installed")
968
+ .description("Check if the skill is installed; --check compares against its origin")
816
969
  .option("--dir <path>", "Custom install directory (overrides --target)")
817
970
  .option("--target <target>", "Check target: agents, claude, or all (default: all)")
971
+ .option("--check", "Also ask the server whether a newer version exists")
818
972
  .action(async (name, opts) => {
819
973
  const bundleName = name ?? "dayofweek-platform";
820
974
  const target = parseTarget(opts.target);
@@ -823,6 +977,7 @@ skill
823
977
  : target === "all"
824
978
  ? [join(homedir(), ".agents", "skills", bundleName), join(homedir(), ".claude", "skills", bundleName)]
825
979
  : resolveTargetDirs(target, bundleName);
980
+ let platformBundles;
826
981
  const installations = [];
827
982
  for (const dir of dirs) {
828
983
  const skillPath = join(dir, "SKILL.md");
@@ -830,9 +985,40 @@ skill
830
985
  installations.push({ installed: false, directory: dir, name: bundleName });
831
986
  continue;
832
987
  }
988
+ const metadata = readInstalledSkill(dir);
833
989
  const content = readFileSync(skillPath, "utf-8");
834
990
  const versionMatch = content.match(/version:\s*"([^"]+)"/);
835
- installations.push({ installed: true, directory: dir, name: bundleName, version: versionMatch?.[1] ?? "unknown", sha256: sha256(content) });
991
+ const entry = {
992
+ installed: true,
993
+ directory: dir,
994
+ name: metadata?.name ?? bundleName,
995
+ version: metadata?.version ?? versionMatch?.[1] ?? "unknown",
996
+ sha256: sha256(content),
997
+ origin: metadata?.origin?.source ?? "platform",
998
+ };
999
+ if (opts.check) {
1000
+ try {
1001
+ if (metadata?.origin?.source === "shared") {
1002
+ const latest = await getClient().getSharedSkill(metadata.origin.skillId);
1003
+ entry.latestVersion = latest.version;
1004
+ entry.upToDate = metadata.hash === latest.hash;
1005
+ }
1006
+ else {
1007
+ platformBundles ??= await getClient().listSkillBundles();
1008
+ const latest = platformBundles.find((candidate) => candidate.name === entry.name);
1009
+ if (latest) {
1010
+ entry.latestVersion = latest.version;
1011
+ // Pre-origin installs have no recorded manifest hash; fall back
1012
+ // to comparing the declared versions.
1013
+ entry.upToDate = metadata ? metadata.hash === latest.hash : latest.version === entry.version;
1014
+ }
1015
+ }
1016
+ }
1017
+ catch (error) {
1018
+ entry.checkError = error instanceof Error ? error.message : String(error);
1019
+ }
1020
+ }
1021
+ installations.push(entry);
836
1022
  }
837
1023
  const installed = installations.some((entry) => entry.installed);
838
1024
  output({ installed, bundle: bundleName, installations });
@@ -4633,6 +4633,23 @@ var DayOfWeekClient = class {
4633
4633
  async listSkillBundles() {
4634
4634
  return this.get("/skill?list=1");
4635
4635
  }
4636
+ // ── Shared skills ─────────────────────────────────────────────────────────
4637
+ //
4638
+ // Skills other users published into shared knowledge areas. Discovery is
4639
+ // server-owned, like the named bundles above: the CLI ships no skill names,
4640
+ // and the server decides which skills this device's user may see.
4641
+ async listSharedSkills() {
4642
+ return this.get("/brain/skills");
4643
+ }
4644
+ async getSharedSkill(skillId) {
4645
+ return this.get(`/brain/skills/${encodeURIComponent(skillId)}`);
4646
+ }
4647
+ async publishSharedSkill(input) {
4648
+ return this.post("/brain/skills", input);
4649
+ }
4650
+ async archiveSharedSkill(skillId) {
4651
+ return this.delete(`/brain/skills/${encodeURIComponent(skillId)}`);
4652
+ }
4636
4653
  // ── Schema ────────────────────────────────────────────────────────────────
4637
4654
  async getSchema() {
4638
4655
  return this.get("/schema");
@@ -5037,7 +5054,7 @@ function parseSegments(pathname) {
5037
5054
  if (segments.length !== 3) throw new Error("Unsupported brain URI form");
5038
5055
  const areaId = assertId(segments[0], "area ID");
5039
5056
  const resourceType = segments[1];
5040
- if (resourceType !== "note" && resourceType !== "source") {
5057
+ if (resourceType !== "note" && resourceType !== "source" && resourceType !== "skill") {
5041
5058
  throw new Error("Unsupported brain resource type");
5042
5059
  }
5043
5060
  return {
@@ -5081,7 +5098,7 @@ var import_promises4 = require("node:readline/promises");
5081
5098
  // package.json
5082
5099
  var package_default = {
5083
5100
  name: "@dayofweek/dcli",
5084
- version: "1.6.0",
5101
+ version: "1.7.0",
5085
5102
  description: "CLI for the Day of Week AgTech platform \u2014 read data and submit proposals for review",
5086
5103
  license: "MIT",
5087
5104
  type: "module",
@@ -5141,6 +5158,17 @@ var import_node_crypto4 = require("node:crypto");
5141
5158
  var import_node_crypto3 = require("node:crypto");
5142
5159
  var import_node_fs7 = require("node:fs");
5143
5160
  var import_node_path5 = require("node:path");
5161
+ function readInstalledSkill(targetDir) {
5162
+ const metadataPath = (0, import_node_path5.join)(targetDir, ".dayofweek-skill.json");
5163
+ if (!(0, import_node_fs7.existsSync)(metadataPath)) return null;
5164
+ try {
5165
+ const parsed = JSON.parse((0, import_node_fs7.readFileSync)(metadataPath, "utf8"));
5166
+ if (typeof parsed.name !== "string" || typeof parsed.version !== "string") return null;
5167
+ return parsed;
5168
+ } catch {
5169
+ return null;
5170
+ }
5171
+ }
5144
5172
  function sha256(value) {
5145
5173
  return (0, import_node_crypto3.createHash)("sha256").update(value).digest("hex");
5146
5174
  }
@@ -5164,7 +5192,7 @@ function validateSkillBundle(bundle) {
5164
5192
  if (bundle.hash && bundle.hash !== actualBundleHash) throw new Error("Skill bundle manifest checksum mismatch");
5165
5193
  return { ...bundle, hash: actualBundleHash, files };
5166
5194
  }
5167
- function writeSkillBundle(input, targetDir) {
5195
+ function writeSkillBundle(input, targetDir, origin) {
5168
5196
  const bundle = validateSkillBundle(input);
5169
5197
  let filesWritten = 0;
5170
5198
  let unchanged = 0;
@@ -5200,7 +5228,15 @@ function writeSkillBundle(input, targetDir) {
5200
5228
  filesWritten++;
5201
5229
  }
5202
5230
  (0, import_node_fs7.mkdirSync)(targetDir, { recursive: true });
5203
- (0, import_node_fs7.writeFileSync)(metadataPath, JSON.stringify({ name: bundle.name, version: bundle.version, hash: bundle.hash, files: hashes }, null, 2), "utf8");
5231
+ (0, import_node_fs7.writeFileSync)(
5232
+ metadataPath,
5233
+ JSON.stringify(
5234
+ { name: bundle.name, version: bundle.version, hash: bundle.hash, files: hashes, ...origin ? { origin } : {} },
5235
+ null,
5236
+ 2
5237
+ ),
5238
+ "utf8"
5239
+ );
5204
5240
  return { written: filesWritten, unchanged, conflicts };
5205
5241
  }
5206
5242
 
@@ -5699,36 +5735,148 @@ function parseTarget(value) {
5699
5735
  }
5700
5736
  return v;
5701
5737
  }
5702
- skill.command("list").description("List authenticated named skill bundles").action(async () => output(await getClient().listSkillBundles()));
5703
- skill.command("bundle <name>").description("Fetch and verify a named skill bundle for a managed installer").action(async (name) => output(validateSkillBundle(await getClient().getSkillBundle(name))));
5704
- skill.command("install [name]").description("Install the agent skill (requires valid auth)").option("--dir <path>", "Custom install directory (overrides --target)").option("--target <target>", "Install target: agents, claude, or all (default: all)").action(async (name, opts) => {
5738
+ function sharedToBundle(shared) {
5739
+ return {
5740
+ bundle: { name: shared.name, version: shared.version, hash: shared.hash, files: shared.files },
5741
+ origin: { source: "shared", skillId: shared.id, areaId: shared.areaId, uri: shared.uri }
5742
+ };
5743
+ }
5744
+ async function fetchSkillByOrigin(client, nameOrUri, areaId) {
5745
+ if (nameOrUri?.includes("://")) {
5746
+ const resource = parseBrainResource(nameOrUri);
5747
+ if (resource.resourceType !== "skill" || !resource.resourceId) throw new Error("A shared skill URI is required");
5748
+ const shared = await client.getSharedSkill(resource.resourceId);
5749
+ if (shared.areaId !== resource.areaId) throw new Error("Server returned a mismatched area");
5750
+ return sharedToBundle(shared);
5751
+ }
5752
+ if (areaId) {
5753
+ if (!nameOrUri) throw new Error("A skill name is required together with --area");
5754
+ const match = (await client.listSharedSkills()).find(
5755
+ (candidate) => candidate.areaId === areaId && candidate.name === nameOrUri
5756
+ );
5757
+ if (!match) throw new Error("Shared skill not found in that area");
5758
+ return sharedToBundle(await client.getSharedSkill(match.id));
5759
+ }
5760
+ return { bundle: await client.getSkillBundle(nameOrUri), origin: { source: "platform" } };
5761
+ }
5762
+ async function installOrUpdateSkill(action, name, opts) {
5705
5763
  const client = getClient();
5706
- const bundle = await client.getSkillBundle(name);
5707
- const target = parseTarget(opts.target);
5708
- const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
5764
+ let resolved;
5765
+ if (action === "update" && !name && !opts.area && opts.dir) {
5766
+ const installed = readInstalledSkill(opts.dir);
5767
+ if (installed) {
5768
+ resolved = installed.origin?.source === "shared" ? sharedToBundle(await client.getSharedSkill(installed.origin.skillId)) : { bundle: await client.getSkillBundle(installed.name), origin: { source: "platform" } };
5769
+ }
5770
+ }
5771
+ resolved = resolved ?? await fetchSkillByOrigin(client, name, opts.area);
5772
+ const bundle = validateSkillBundle(resolved.bundle);
5773
+ const dirs = resolveTargetDirs(parseTarget(opts.target), bundle.name, opts.dir);
5709
5774
  const installations = [];
5710
5775
  for (const dir of dirs) {
5711
- const result = writeSkillBundle(bundle, dir);
5776
+ const result = writeSkillBundle(bundle, dir, resolved.origin);
5712
5777
  installations.push({ directory: dir, ...result });
5713
5778
  }
5714
- output({ action: "install", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
5715
- });
5716
- skill.command("update [name]").description("Update the skill to the latest version").option("--dir <path>", "Custom install directory (overrides --target)").option("--target <target>", "Install target: agents, claude, or all (default: all)").action(async (name, opts) => {
5779
+ output({
5780
+ action,
5781
+ bundle: bundle.name,
5782
+ version: bundle.version,
5783
+ hash: bundle.hash,
5784
+ origin: resolved.origin.source,
5785
+ ...resolved.origin.source === "shared" ? { uri: resolved.origin.uri } : {},
5786
+ installations
5787
+ });
5788
+ }
5789
+ function collectSkillFiles(dir) {
5790
+ const root = (0, import_node_path6.resolve)(dir);
5791
+ if (!(0, import_node_fs8.existsSync)(root) || !(0, import_node_fs8.statSync)(root).isDirectory()) throw new Error("Skill directory not found");
5792
+ const files = [];
5793
+ const walk = (current, prefix) => {
5794
+ for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
5795
+ if (entry.name.startsWith(".") || /\.new(\.\d+)?$/.test(entry.name)) continue;
5796
+ if (entry.isSymbolicLink()) throw new Error("Symlinks are not allowed in a skill directory");
5797
+ const full = (0, import_node_path6.join)(current, entry.name);
5798
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
5799
+ if (entry.isDirectory()) walk(full, relativePath);
5800
+ else if (entry.isFile()) files.push({ path: relativePath, content: (0, import_node_fs8.readFileSync)(full, "utf8") });
5801
+ }
5802
+ };
5803
+ walk(root, "");
5804
+ return files.sort((left, right) => left.path.localeCompare(right.path));
5805
+ }
5806
+ skill.command("list").description("List installable named skill bundles; --shared adds skills shared with you").option("--shared", "Include skills other users shared with you").action(async (opts) => {
5717
5807
  const client = getClient();
5718
- const bundle = await client.getSkillBundle(name);
5719
- const target = parseTarget(opts.target);
5720
- const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
5721
- const installations = [];
5722
- for (const dir of dirs) {
5723
- const result = writeSkillBundle(bundle, dir);
5724
- installations.push({ directory: dir, ...result });
5808
+ const listings = (await client.listSkillBundles()).map((bundle) => ({
5809
+ ...bundle,
5810
+ source: "platform"
5811
+ }));
5812
+ if (opts.shared) {
5813
+ try {
5814
+ for (const shared of await client.listSharedSkills()) {
5815
+ listings.push({
5816
+ name: shared.name,
5817
+ version: shared.version,
5818
+ hash: shared.hash,
5819
+ source: "shared",
5820
+ areaId: shared.areaId,
5821
+ areaName: shared.areaName,
5822
+ visibility: shared.visibility,
5823
+ description: shared.description,
5824
+ uri: shared.uri,
5825
+ updatedAt: shared.updatedAt
5826
+ });
5827
+ }
5828
+ } catch (error) {
5829
+ console.error(`Shared skills unavailable: ${error instanceof Error ? error.message : String(error)}`);
5830
+ }
5831
+ }
5832
+ output(listings.sort((left, right) => left.name.localeCompare(right.name)));
5833
+ });
5834
+ skill.command("bundle <nameOrUri>").description("Fetch and verify a skill bundle (by name, or shared-skill URI) for a managed installer").action(async (nameOrUri) => {
5835
+ if (nameOrUri.includes("://")) {
5836
+ const resource = parseBrainResource(nameOrUri);
5837
+ if (resource.resourceType !== "skill" || !resource.resourceId) throw new Error("A shared skill URI is required");
5838
+ const shared = await getClient().getSharedSkill(resource.resourceId);
5839
+ if (shared.areaId !== resource.areaId) throw new Error("Server returned a mismatched area");
5840
+ output(validateSkillBundle({ name: shared.name, version: shared.version, hash: shared.hash, files: shared.files }));
5841
+ return;
5725
5842
  }
5726
- output({ action: "update", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
5843
+ output(validateSkillBundle(await getClient().getSkillBundle(nameOrUri)));
5727
5844
  });
5728
- skill.command("status [name]").description("Check if the skill is installed").option("--dir <path>", "Custom install directory (overrides --target)").option("--target <target>", "Check target: agents, claude, or all (default: all)").action(async (name, opts) => {
5845
+ skill.command("install [name]").description("Install a skill by bundle name, shared-skill URI, or name + --area").option("--dir <path>", "Custom install directory (overrides --target)").option("--target <target>", "Install target: agents, claude, or all (default: all)").option("--area <areaId>", "Install a shared skill from this area").action(async (name, opts) => installOrUpdateSkill("install", name, opts));
5846
+ skill.command("update [name]").description("Update a skill to the latest version from its origin").option("--dir <path>", "Custom install directory (overrides --target)").option("--target <target>", "Install target: agents, claude, or all (default: all)").option("--area <areaId>", "Update a shared skill from this area").action(async (name, opts) => installOrUpdateSkill("update", name, opts));
5847
+ skill.command("publish").description("Share a skill directory to a knowledge area so teammates can install it").requiredOption("--area <areaId>", "Owning area (see: dcli brain list)").requiredOption("--dir <path>", "Skill directory containing SKILL.md").option("--name <name>", "Skill name (default: SKILL.md frontmatter, else the directory name)").option("--skill-version <version>", "Version string (default: SKILL.md frontmatter version)").option("--visibility <visibility>", "Who can discover it: area (members only), company, or global (admins; every user)").option("--description <text>", "Short description shown in listings").action(async (opts) => {
5848
+ const visibility = (opts.visibility ?? "area").toLowerCase();
5849
+ if (visibility !== "area" && visibility !== "company" && visibility !== "global") {
5850
+ throw new Error("Invalid --visibility: use area, company, or global");
5851
+ }
5852
+ const files = collectSkillFiles(opts.dir);
5853
+ const skillMd = files.find((file) => file.path === "SKILL.md");
5854
+ if (!skillMd) throw new Error("The skill directory must contain SKILL.md");
5855
+ const name = opts.name ?? skillMd.content.match(/^name:\s*"?([a-z0-9][a-z0-9-]*)"?\s*$/m)?.[1] ?? (0, import_node_path6.basename)((0, import_node_path6.resolve)(opts.dir));
5856
+ const version = opts.skillVersion ?? skillMd.content.match(/version:\s*"([^"]+)"/)?.[1] ?? "1.0.0";
5857
+ validateSkillBundle({ name, version, files });
5858
+ const result = await getClient().publishSharedSkill({
5859
+ areaId: opts.area,
5860
+ name,
5861
+ version,
5862
+ description: opts.description,
5863
+ visibility,
5864
+ files
5865
+ });
5866
+ output({ action: "publish", ...result });
5867
+ });
5868
+ skill.command("archive <uri>").description("Archive a shared skill you own (removes it from listing and install)").action(async (uri) => {
5869
+ const resource = parseBrainResource(uri);
5870
+ if (resource.resourceType !== "skill" || !resource.resourceId) throw new Error("A shared skill URI is required");
5871
+ const result = await getClient().archiveSharedSkill(resource.resourceId);
5872
+ if (result.areaId !== resource.areaId) throw new Error("Server returned a mismatched area");
5873
+ output({ action: "archive", ...result });
5874
+ });
5875
+ skill.command("status [name]").description("Check if the skill is installed; --check compares against its origin").option("--dir <path>", "Custom install directory (overrides --target)").option("--target <target>", "Check target: agents, claude, or all (default: all)").option("--check", "Also ask the server whether a newer version exists").action(async (name, opts) => {
5729
5876
  const bundleName2 = name ?? "dayofweek-platform";
5730
5877
  const target = parseTarget(opts.target);
5731
5878
  const dirs = opts.dir ? [opts.dir] : target === "all" ? [(0, import_node_path6.join)((0, import_node_os5.homedir)(), ".agents", "skills", bundleName2), (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".claude", "skills", bundleName2)] : resolveTargetDirs(target, bundleName2);
5879
+ let platformBundles;
5732
5880
  const installations = [];
5733
5881
  for (const dir of dirs) {
5734
5882
  const skillPath = (0, import_node_path6.join)(dir, "SKILL.md");
@@ -5736,9 +5884,36 @@ skill.command("status [name]").description("Check if the skill is installed").op
5736
5884
  installations.push({ installed: false, directory: dir, name: bundleName2 });
5737
5885
  continue;
5738
5886
  }
5887
+ const metadata = readInstalledSkill(dir);
5739
5888
  const content = (0, import_node_fs8.readFileSync)(skillPath, "utf-8");
5740
5889
  const versionMatch = content.match(/version:\s*"([^"]+)"/);
5741
- installations.push({ installed: true, directory: dir, name: bundleName2, version: versionMatch?.[1] ?? "unknown", sha256: sha2562(content) });
5890
+ const entry = {
5891
+ installed: true,
5892
+ directory: dir,
5893
+ name: metadata?.name ?? bundleName2,
5894
+ version: metadata?.version ?? versionMatch?.[1] ?? "unknown",
5895
+ sha256: sha2562(content),
5896
+ origin: metadata?.origin?.source ?? "platform"
5897
+ };
5898
+ if (opts.check) {
5899
+ try {
5900
+ if (metadata?.origin?.source === "shared") {
5901
+ const latest = await getClient().getSharedSkill(metadata.origin.skillId);
5902
+ entry.latestVersion = latest.version;
5903
+ entry.upToDate = metadata.hash === latest.hash;
5904
+ } else {
5905
+ platformBundles ??= await getClient().listSkillBundles();
5906
+ const latest = platformBundles.find((candidate) => candidate.name === entry.name);
5907
+ if (latest) {
5908
+ entry.latestVersion = latest.version;
5909
+ entry.upToDate = metadata ? metadata.hash === latest.hash : latest.version === entry.version;
5910
+ }
5911
+ }
5912
+ } catch (error) {
5913
+ entry.checkError = error instanceof Error ? error.message : String(error);
5914
+ }
5915
+ }
5916
+ installations.push(entry);
5742
5917
  }
5743
5918
  const installed = installations.some((entry) => entry.installed);
5744
5919
  output({ installed, bundle: bundleName2, installations });
package/dist/client.d.ts CHANGED
@@ -333,6 +333,20 @@ export declare class DayOfWeekClient {
333
333
  version: string;
334
334
  hash: string;
335
335
  }>>;
336
+ listSharedSkills(): Promise<SharedSkillSummary[]>;
337
+ getSharedSkill(skillId: string): Promise<SharedSkillBundle>;
338
+ publishSharedSkill(input: {
339
+ areaId: string;
340
+ name: string;
341
+ version: string;
342
+ description?: string;
343
+ visibility: "area" | "company" | "global";
344
+ files: Array<{
345
+ path: string;
346
+ content: string;
347
+ }>;
348
+ }): Promise<SharedSkillSummary>;
349
+ archiveSharedSkill(skillId: string): Promise<SharedSkillSummary>;
336
350
  getSchema(): Promise<any>;
337
351
  private get;
338
352
  private post;
@@ -399,3 +413,26 @@ export type BrainResolvedResource = {
399
413
  resourceType: "source";
400
414
  source: BrainSource;
401
415
  };
416
+ export type SharedSkillSummary = {
417
+ id: string;
418
+ areaId: string;
419
+ areaName: string;
420
+ name: string;
421
+ version: string;
422
+ description?: string;
423
+ hash: string;
424
+ visibility: "area" | "company" | "global";
425
+ revision: number;
426
+ fileCount: number;
427
+ byteSize: number;
428
+ uri: string;
429
+ httpsUrl: string;
430
+ updatedAt: number;
431
+ };
432
+ export type SharedSkillBundle = Omit<SharedSkillSummary, "fileCount" | "byteSize"> & {
433
+ files: Array<{
434
+ path: string;
435
+ content: string;
436
+ sha256: string;
437
+ }>;
438
+ };
package/dist/client.js CHANGED
@@ -512,6 +512,23 @@ export class DayOfWeekClient {
512
512
  async listSkillBundles() {
513
513
  return this.get("/skill?list=1");
514
514
  }
515
+ // ── Shared skills ─────────────────────────────────────────────────────────
516
+ //
517
+ // Skills other users published into shared knowledge areas. Discovery is
518
+ // server-owned, like the named bundles above: the CLI ships no skill names,
519
+ // and the server decides which skills this device's user may see.
520
+ async listSharedSkills() {
521
+ return this.get("/brain/skills");
522
+ }
523
+ async getSharedSkill(skillId) {
524
+ return this.get(`/brain/skills/${encodeURIComponent(skillId)}`);
525
+ }
526
+ async publishSharedSkill(input) {
527
+ return this.post("/brain/skills", input);
528
+ }
529
+ async archiveSharedSkill(skillId) {
530
+ return this.delete(`/brain/skills/${encodeURIComponent(skillId)}`);
531
+ }
515
532
  // ── Schema ────────────────────────────────────────────────────────────────
516
533
  async getSchema() {
517
534
  return this.get("/schema");
package/dist/skills.d.ts CHANGED
@@ -8,8 +8,31 @@ export type SkillBundle = {
8
8
  sha256?: string;
9
9
  }>;
10
10
  };
11
+ /**
12
+ * Where an installed skill came from — recorded in `.dayofweek-skill.json` so
13
+ * `skill update` and `skill status --check` can go back to the same origin.
14
+ * "platform" = a named bundle from the built-in endpoint; "shared" = a skill
15
+ * another user published into a shared knowledge area.
16
+ */
17
+ export type SkillOrigin = {
18
+ source: "platform";
19
+ } | {
20
+ source: "shared";
21
+ skillId: string;
22
+ areaId: string;
23
+ uri: string;
24
+ };
25
+ export type InstalledSkillMetadata = {
26
+ name: string;
27
+ version: string;
28
+ hash: string;
29
+ files: Record<string, string>;
30
+ origin?: SkillOrigin;
31
+ };
32
+ /** Parse `<dir>/.dayofweek-skill.json`; null when absent or malformed. */
33
+ export declare function readInstalledSkill(targetDir: string): InstalledSkillMetadata | null;
11
34
  export declare function validateSkillBundle(bundle: SkillBundle): SkillBundle;
12
- export declare function writeSkillBundle(input: SkillBundle, targetDir: string): {
35
+ export declare function writeSkillBundle(input: SkillBundle, targetDir: string, origin?: SkillOrigin): {
13
36
  written: number;
14
37
  unchanged: number;
15
38
  conflicts: string[];
package/dist/skills.js CHANGED
@@ -1,6 +1,21 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
+ /** Parse `<dir>/.dayofweek-skill.json`; null when absent or malformed. */
5
+ export function readInstalledSkill(targetDir) {
6
+ const metadataPath = join(targetDir, ".dayofweek-skill.json");
7
+ if (!existsSync(metadataPath))
8
+ return null;
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(metadataPath, "utf8"));
11
+ if (typeof parsed.name !== "string" || typeof parsed.version !== "string")
12
+ return null;
13
+ return parsed;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
4
19
  function sha256(value) {
5
20
  return createHash("sha256").update(value).digest("hex");
6
21
  }
@@ -33,7 +48,7 @@ export function validateSkillBundle(bundle) {
33
48
  throw new Error("Skill bundle manifest checksum mismatch");
34
49
  return { ...bundle, hash: actualBundleHash, files };
35
50
  }
36
- export function writeSkillBundle(input, targetDir) {
51
+ export function writeSkillBundle(input, targetDir, origin) {
37
52
  const bundle = validateSkillBundle(input);
38
53
  let filesWritten = 0;
39
54
  let unchanged = 0;
@@ -72,6 +87,6 @@ export function writeSkillBundle(input, targetDir) {
72
87
  filesWritten++;
73
88
  }
74
89
  mkdirSync(targetDir, { recursive: true });
75
- writeFileSync(metadataPath, JSON.stringify({ name: bundle.name, version: bundle.version, hash: bundle.hash, files: hashes }, null, 2), "utf8");
90
+ writeFileSync(metadataPath, JSON.stringify({ name: bundle.name, version: bundle.version, hash: bundle.hash, files: hashes, ...(origin ? { origin } : {}) }, null, 2), "utf8");
76
91
  return { written: filesWritten, unchanged, conflicts };
77
92
  }
package/dist/uri.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type BrainResource = {
2
2
  version: 1;
3
3
  areaId: string;
4
- resourceType: "area" | "note" | "source";
4
+ resourceType: "area" | "note" | "source" | "skill";
5
5
  resourceId?: string;
6
6
  };
7
7
  export declare function parseBrainResource(input: string): BrainResource;
package/dist/uri.js CHANGED
@@ -21,7 +21,7 @@ function parseSegments(pathname) {
21
21
  throw new Error("Unsupported brain URI form");
22
22
  const areaId = assertId(segments[0], "area ID");
23
23
  const resourceType = segments[1];
24
- if (resourceType !== "note" && resourceType !== "source") {
24
+ if (resourceType !== "note" && resourceType !== "source" && resourceType !== "skill") {
25
25
  throw new Error("Unsupported brain resource type");
26
26
  }
27
27
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI for the Day of Week AgTech platform — read data and submit proposals for review",
5
5
  "license": "MIT",
6
6
  "type": "module",