akm-opencode 0.5.1 → 0.5.2

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 (3) hide show
  1. package/README.md +1 -1
  2. package/index.ts +136 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -112,7 +112,7 @@ At least one of `ref` or `query` is required.
112
112
 
113
113
  ## Prerequisites
114
114
 
115
- When the plugin loads, it runs `bun install -g akm-cli@latest` so it always picks up the latest published npm package. It then prefers the Bun-installed binary and falls back to an existing `akm` on PATH when needed. It does not run the standalone shell installers automatically.
115
+ When the plugin loads, it checks the installed `akm` version first and only runs `bun install -g akm-cli@latest` when `akm` is missing or older than the latest stable npm release. Newer pre-releases and local builds are left in place. It then prefers the Bun-installed binary and falls back to an existing `akm` on PATH when needed. It does not run the standalone shell installers automatically.
116
116
 
117
117
  ```sh
118
118
  # macOS / Linux
package/index.ts CHANGED
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"
7
7
  let resolvedAkmCommand = "akm"
8
8
  const autoInstallPackageRef = "akm-cli@latest"
9
9
  const moduleDir = path.dirname(fileURLToPath(import.meta.url))
10
+ const SEMVER_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/
10
11
 
11
12
  const AKM_AUTO_FEEDBACK = (process.env.AKM_AUTO_FEEDBACK ?? "1") !== "0"
12
13
  const AKM_AUTO_MEMORY = (process.env.AKM_AUTO_MEMORY ?? "1") !== "0"
@@ -32,6 +33,7 @@ const sessionContextEpoch = new Map<string, number>()
32
33
  const sessionContextInjectedEpoch = new Map<string, number>()
33
34
  const sessionCuratedVersion = new Map<string, number>()
34
35
  const sessionCuratedInjectedVersion = new Map<string, number>()
36
+ type ParsedSemver = { core: [number, number, number]; prerelease: Array<string | number> | null }
35
37
  type SessionBufferEntry = {
36
38
  timestamp: string
37
39
  kind: "memory-intent" | "tool-ref"
@@ -742,6 +744,72 @@ function getCommandStatus(command: string): "ok" | "missing" | "error" {
742
744
  }
743
745
  }
744
746
 
747
+ function extractFirstSemverMatch(value: string): string | null {
748
+ return value.match(SEMVER_PATTERN)?.[0] ?? null
749
+ }
750
+
751
+ function getCommandVersion(command: string): string | null {
752
+ try {
753
+ const version = execFileSync(command, ["--version"], {
754
+ encoding: "utf8",
755
+ timeout: 10_000,
756
+ })
757
+ return extractFirstSemverMatch(version)
758
+ } catch {
759
+ return null
760
+ }
761
+ }
762
+
763
+ function parseSemver(version: string): ParsedSemver | null {
764
+ const normalized = extractFirstSemverMatch(version)
765
+ if (!normalized) return null
766
+
767
+ const [withoutBuildMetadata] = normalized.split("+", 1)
768
+ const [release, prereleaseText] = withoutBuildMetadata.split("-", 2)
769
+ const parts = release.split(".").map((part) => Number(part))
770
+ if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
771
+ return null
772
+ }
773
+ const core = [parts[0], parts[1], parts[2]] as [number, number, number]
774
+
775
+ return {
776
+ core,
777
+ prerelease: prereleaseText
778
+ ? prereleaseText.split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : part))
779
+ : null,
780
+ }
781
+ }
782
+
783
+ function compareSemver(left: string, right: string): number {
784
+ const leftParsed = parseSemver(left)
785
+ const rightParsed = parseSemver(right)
786
+ if (!leftParsed || !rightParsed) return left.localeCompare(right)
787
+
788
+ for (let index = 0; index < leftParsed.core.length; index += 1) {
789
+ const delta = leftParsed.core[index] - rightParsed.core[index]
790
+ if (delta !== 0) return delta
791
+ }
792
+
793
+ if (!leftParsed.prerelease && !rightParsed.prerelease) return 0
794
+ if (!leftParsed.prerelease) return 1
795
+ if (!rightParsed.prerelease) return -1
796
+
797
+ const length = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length)
798
+ for (let index = 0; index < length; index += 1) {
799
+ const leftPart = leftParsed.prerelease[index]
800
+ const rightPart = rightParsed.prerelease[index]
801
+ if (leftPart === undefined) return -1
802
+ if (rightPart === undefined) return 1
803
+ if (leftPart === rightPart) continue
804
+ if (typeof leftPart === "number" && typeof rightPart === "number") return leftPart - rightPart
805
+ if (typeof leftPart === "number") return -1
806
+ if (typeof rightPart === "number") return 1
807
+ return leftPart.localeCompare(rightPart)
808
+ }
809
+
810
+ return 0
811
+ }
812
+
745
813
  function getBunGlobalAkmCommand(): string | null {
746
814
  try {
747
815
  const globalBin = execFileSync("bun", ["pm", "bin", "-g"], {
@@ -757,6 +825,39 @@ function getBunGlobalAkmCommand(): string | null {
757
825
  }
758
826
  }
759
827
 
828
+ function getInstalledAkmDetails(): { command: string; version: string } | null {
829
+ const candidates = [resolvedAkmCommand, getBunGlobalAkmCommand(), "akm"]
830
+ const seen = new Set<string>()
831
+ for (const candidate of candidates) {
832
+ if (!candidate || seen.has(candidate)) continue
833
+ seen.add(candidate)
834
+ const version = getCommandVersion(candidate)
835
+ if (version) return { command: candidate, version }
836
+ }
837
+ return null
838
+ }
839
+
840
+ async function getLatestNpmPackageVersion(packageName: string): Promise<string | null> {
841
+ if (typeof fetch !== "function") return null
842
+
843
+ const controller = new AbortController()
844
+ const timeout = setTimeout(() => controller.abort(), 10_000)
845
+ try {
846
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
847
+ headers: { accept: "application/json" },
848
+ signal: controller.signal,
849
+ })
850
+ if (!response.ok) return null
851
+ const body = await response.json()
852
+ if (!body || typeof body !== "object") return null
853
+ return typeof body.version === "string" ? extractFirstSemverMatch(body.version) : null
854
+ } catch {
855
+ return null
856
+ } finally {
857
+ clearTimeout(timeout)
858
+ }
859
+ }
860
+
760
861
  async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void> {
761
862
  try {
762
863
  execFileSync("bun", ["--version"], {
@@ -772,6 +873,39 @@ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void>
772
873
  return
773
874
  }
774
875
 
876
+ const installedAkm = getInstalledAkmDetails()
877
+ const latestStable = await getLatestNpmPackageVersion("akm-cli")
878
+
879
+ if (installedAkm) {
880
+ if (!latestStable) {
881
+ resolvedAkmCommand = installedAkm.command
882
+ await writePluginLog(client, "info", "AKM auto-install skipped", {
883
+ subsystem: "akm",
884
+ installer: "bun",
885
+ package: autoInstallPackageRef,
886
+ command: resolvedAkmCommand,
887
+ installedVersion: installedAkm.version,
888
+ latestStable,
889
+ reason: "latest_version_unavailable",
890
+ })
891
+ return
892
+ }
893
+
894
+ if (compareSemver(installedAkm.version, latestStable) >= 0) {
895
+ resolvedAkmCommand = installedAkm.command
896
+ await writePluginLog(client, "info", "AKM auto-install skipped", {
897
+ subsystem: "akm",
898
+ installer: "bun",
899
+ package: autoInstallPackageRef,
900
+ command: resolvedAkmCommand,
901
+ installedVersion: installedAkm.version,
902
+ latestStable,
903
+ reason: "installed_version_not_older",
904
+ })
905
+ return
906
+ }
907
+ }
908
+
775
909
  try {
776
910
  execFileSync("bun", ["install", "-g", autoInstallPackageRef], {
777
911
  encoding: "utf8",
@@ -791,6 +925,8 @@ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void>
791
925
  installer: "bun",
792
926
  package: autoInstallPackageRef,
793
927
  command: resolvedAkmCommand,
928
+ installedVersion: installedAkm?.version ?? null,
929
+ latestStable,
794
930
  })
795
931
  } catch (error: unknown) {
796
932
  await writePluginLog(client, "warn", "AKM auto-install failed", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, including v0.5.0 vaults, wikis, and workflows, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
6
6
  "keywords": [