@kkelly-offical/kkcode 0.2.1 → 0.2.3-preview.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.
- package/README.md +27 -5
- package/package.json +1 -1
- package/src/commands/update.mjs +32 -0
- package/src/config/defaults.mjs +9 -0
- package/src/config/schema.mjs +13 -0
- package/src/index.mjs +4 -1
- package/src/mcp/constants.mjs +3 -1
- package/src/repl.mjs +4 -1
- package/src/storage/paths.mjs +4 -0
- package/src/update/checker.mjs +184 -0
- package/src/version.mjs +3 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# kkcode
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
3
|
+
[](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
4
4
|
[](https://github.com/kkelly-offical/kkcode/releases)
|
|
5
5
|

|
|
6
6
|

|
|
@@ -351,20 +351,42 @@ Run `kkcode --help` or `kkcode <command> --help` for the full surface.
|
|
|
351
351
|
|
|
352
352
|
---
|
|
353
353
|
|
|
354
|
+
<a id="updates"></a>
|
|
355
|
+
## Updates / 更新
|
|
356
|
+
|
|
357
|
+
KKCode checks npm dist-tags in the background on startup and caches the result under `~/.kkcode/update-state.json`. By default it only prints a notice; it does not modify your global install unless you explicitly run the updater.
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
kkcode update --check
|
|
361
|
+
kkcode update --install --channel latest
|
|
362
|
+
kkcode update --install --channel preview
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Config:
|
|
366
|
+
|
|
367
|
+
```yaml
|
|
368
|
+
update:
|
|
369
|
+
enabled: true
|
|
370
|
+
notify_on_startup: true
|
|
371
|
+
auto_install: false
|
|
372
|
+
channel: "latest"
|
|
373
|
+
check_interval_hours: 12
|
|
374
|
+
```
|
|
375
|
+
|
|
354
376
|
<a id="release-status"></a>
|
|
355
377
|
## Release Status / 发布状态
|
|
356
378
|
|
|
357
|
-
**Current
|
|
379
|
+
**Current preview / 当前预览版本**: `v0.2.3-preview.1`
|
|
358
380
|
**Latest releases / 最新发布**: [GitHub Releases](https://github.com/kkelly-offical/kkcode/releases)
|
|
359
381
|
**Package / 包地址**: [npm](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
360
382
|
|
|
361
383
|
**English**
|
|
362
|
-
- `0.2.1`
|
|
363
|
-
-
|
|
384
|
+
- `0.2.3-preview.1` is the Preview V1 updater release: kkcode can check npm dist-tags at startup and exposes `kkcode update` for manual upgrades.
|
|
385
|
+
- `0.2.1` rebuilt kkcode around Assistant as the default general-purpose lane, with dedicated Agent and LongAgent modes for coding work.
|
|
364
386
|
|
|
365
387
|
**中文**
|
|
388
|
+
- `0.2.3-preview.1` 是 Preview V1 更新器版本:kkcode 可在启动时检查 npm dist-tag,并提供 `kkcode update` 手动升级入口。
|
|
366
389
|
- `0.2.1` 将 kkcode 重构为以 Assistant 为默认入口的通用个人助手,同时保留专门面向代码工作的 Agent 和 LongAgent 模式。
|
|
367
|
-
- 公共 Ask 通道已移除;问答、解释和轻量个人助手任务现在统一路由到 Assistant。
|
|
368
390
|
|
|
369
391
|
---
|
|
370
392
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kkelly-offical/kkcode",
|
|
3
|
-
"version": "0.2.1",
|
|
3
|
+
"version": "0.2.3-preview.1",
|
|
4
4
|
"description": "CLI-first personal assistant with dedicated coding and LongAgent modes for governed terminal workflows, MCP integrations, and extensible automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.5.2",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from "commander"
|
|
2
|
+
import { loadConfig } from "../config/load-config.mjs"
|
|
3
|
+
import { PACKAGE_VERSION } from "../version.mjs"
|
|
4
|
+
import { checkForUpdate, installUpdate, updateMessage } from "../update/checker.mjs"
|
|
5
|
+
|
|
6
|
+
export function createUpdateCommand() {
|
|
7
|
+
return new Command("update")
|
|
8
|
+
.description("check for and install kkcode updates")
|
|
9
|
+
.option("--check", "only check for updates", false)
|
|
10
|
+
.option("--install", "install the selected update", false)
|
|
11
|
+
.option("--channel <channel>", "npm dist-tag to follow", null)
|
|
12
|
+
.option("--json", "print structured result", false)
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const state = await loadConfig(process.cwd())
|
|
15
|
+
const config = { ...state.config, update: { ...(state.config.update || {}) } }
|
|
16
|
+
if (options.channel) config.update.channel = options.channel
|
|
17
|
+
const result = await checkForUpdate(config, { force: true, currentVersion: PACKAGE_VERSION })
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify(result, null, 2))
|
|
20
|
+
} else if (result.hasUpdate) {
|
|
21
|
+
console.log(updateMessage(result))
|
|
22
|
+
} else {
|
|
23
|
+
console.log(`kkcode is up to date (${result.currentVersion}) on ${result.channel}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (options.check || !options.install) return
|
|
27
|
+
if (!result.hasUpdate) return
|
|
28
|
+
const installed = await installUpdate(config, { channel: result.channel })
|
|
29
|
+
if (!installed.ok) throw new Error(`update install failed: ${installed.error}`)
|
|
30
|
+
console.log(`installed kkcode ${result.latestVersion}; restart your shell or kkcode session if needed`)
|
|
31
|
+
})
|
|
32
|
+
}
|
package/src/config/defaults.mjs
CHANGED
|
@@ -252,6 +252,15 @@ export const DEFAULT_CONFIG = {
|
|
|
252
252
|
strategy: "warn"
|
|
253
253
|
}
|
|
254
254
|
},
|
|
255
|
+
update: {
|
|
256
|
+
enabled: true,
|
|
257
|
+
notify_on_startup: true,
|
|
258
|
+
auto_install: false,
|
|
259
|
+
channel: "latest",
|
|
260
|
+
check_interval_hours: 12,
|
|
261
|
+
registry: "https://registry.npmjs.org",
|
|
262
|
+
timeout_ms: 2500
|
|
263
|
+
},
|
|
255
264
|
ui: {
|
|
256
265
|
theme_file: null,
|
|
257
266
|
mode_colors: {
|
package/src/config/schema.mjs
CHANGED
|
@@ -512,6 +512,19 @@ export function validateConfig(config) {
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
if (config.update !== undefined) {
|
|
516
|
+
if (!isObj(config.update)) err(errors, "update", "must be object")
|
|
517
|
+
else {
|
|
518
|
+
if (config.update.enabled !== undefined && typeof config.update.enabled !== "boolean") err(errors, "update.enabled", "must be boolean")
|
|
519
|
+
if (config.update.notify_on_startup !== undefined && typeof config.update.notify_on_startup !== "boolean") err(errors, "update.notify_on_startup", "must be boolean")
|
|
520
|
+
if (config.update.auto_install !== undefined && typeof config.update.auto_install !== "boolean") err(errors, "update.auto_install", "must be boolean")
|
|
521
|
+
if (config.update.channel !== undefined && typeof config.update.channel !== "string") err(errors, "update.channel", "must be string")
|
|
522
|
+
if (config.update.registry !== undefined && typeof config.update.registry !== "string") err(errors, "update.registry", "must be string")
|
|
523
|
+
if (config.update.check_interval_hours !== undefined) checkInt(errors, "update.check_interval_hours", config.update.check_interval_hours, 0)
|
|
524
|
+
if (config.update.timeout_ms !== undefined) checkInt(errors, "update.timeout_ms", config.update.timeout_ms, 100)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
515
528
|
if (config.ui !== undefined) {
|
|
516
529
|
if (!isObj(config.ui)) err(errors, "ui", "must be object")
|
|
517
530
|
else {
|
package/src/index.mjs
CHANGED
|
@@ -20,6 +20,8 @@ import { createInitCommand } from "./commands/init.mjs"
|
|
|
20
20
|
import { createAuditCommand } from "./commands/audit.mjs"
|
|
21
21
|
import { createSkillCommand } from "./commands/skill.mjs"
|
|
22
22
|
import { startRepl } from "./repl.mjs"
|
|
23
|
+
import { PACKAGE_VERSION } from "./version.mjs"
|
|
24
|
+
import { createUpdateCommand } from "./commands/update.mjs"
|
|
23
25
|
|
|
24
26
|
async function main() {
|
|
25
27
|
const hasTrust = process.argv.includes("--trust")
|
|
@@ -55,7 +57,7 @@ async function main() {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const program = new Command()
|
|
58
|
-
program.name("kkcode").description("kkcode CLI").version(
|
|
60
|
+
program.name("kkcode").description("kkcode CLI").version(PACKAGE_VERSION)
|
|
59
61
|
program.addCommand(createChatCommand())
|
|
60
62
|
program.addCommand(createThemeCommand())
|
|
61
63
|
program.addCommand(createUsageCommand())
|
|
@@ -75,6 +77,7 @@ async function main() {
|
|
|
75
77
|
program.addCommand(createAuditCommand())
|
|
76
78
|
program.addCommand(createInitCommand())
|
|
77
79
|
program.addCommand(createSkillCommand())
|
|
80
|
+
program.addCommand(createUpdateCommand())
|
|
78
81
|
await program.parseAsync(process.argv)
|
|
79
82
|
}
|
|
80
83
|
|
package/src/mcp/constants.mjs
CHANGED
package/src/repl.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "./version.mjs"
|
|
2
|
+
import { maybeNotifyUpdateOnStartup } from "./update/checker.mjs"
|
|
1
3
|
import { stdin as input, stdout as output } from "node:process"
|
|
2
4
|
import { createInterface } from "node:readline/promises"
|
|
3
5
|
import { emitKeypressEvents } from "node:readline"
|
|
@@ -3310,10 +3312,11 @@ export async function startRepl({ trust = false } = {}) {
|
|
|
3310
3312
|
const { checkWorkspaceTrust } = await import("./permission/workspace-trust.mjs")
|
|
3311
3313
|
const trustState = await checkWorkspaceTrust({ cwd: process.cwd(), cliTrust: trust, isTTY: process.stdin.isTTY })
|
|
3312
3314
|
|
|
3313
|
-
const splash = startSplash({ version:
|
|
3315
|
+
const splash = startSplash({ version: `v${PACKAGE_VERSION}` })
|
|
3314
3316
|
|
|
3315
3317
|
const ctx = await buildContext({ trust, trustState })
|
|
3316
3318
|
printContextWarnings(ctx)
|
|
3319
|
+
void maybeNotifyUpdateOnStartup(ctx.configState.config, { currentVersion: PACKAGE_VERSION })
|
|
3317
3320
|
|
|
3318
3321
|
splash.update("loading tools & MCP servers...")
|
|
3319
3322
|
await ToolRegistry.initialize({ config: ctx.configState.config, cwd: process.cwd() })
|
package/src/storage/paths.mjs
CHANGED
|
@@ -111,6 +111,10 @@ export function auditStorePath() {
|
|
|
111
111
|
return path.join(userRootDir(), "audit-log.json")
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
export function updateStatePath() {
|
|
115
|
+
return path.join(userRootDir(), "update-state.json")
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
export async function ensureUserRoot() {
|
|
115
119
|
await mkdir(userRootDir(), { recursive: true })
|
|
116
120
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { dirname } from "node:path"
|
|
4
|
+
import { mkdir } from "node:fs/promises"
|
|
5
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "../version.mjs"
|
|
6
|
+
import { updateStatePath } from "../storage/paths.mjs"
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org"
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 2500
|
|
10
|
+
|
|
11
|
+
function normalizeRegistry(registry = DEFAULT_REGISTRY) {
|
|
12
|
+
return String(registry || DEFAULT_REGISTRY).replace(/\/+$/, "")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function encodePackageName(name) {
|
|
16
|
+
return String(name).replace("/", "%2F")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseVersion(version) {
|
|
20
|
+
const match = String(version || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/)
|
|
21
|
+
if (!match) return null
|
|
22
|
+
return {
|
|
23
|
+
major: Number(match[1]),
|
|
24
|
+
minor: Number(match[2]),
|
|
25
|
+
patch: Number(match[3]),
|
|
26
|
+
prerelease: match[4] ? match[4].split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : part)) : []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compareIdentifier(a, b) {
|
|
31
|
+
if (a === b) return 0
|
|
32
|
+
const aNum = typeof a === "number"
|
|
33
|
+
const bNum = typeof b === "number"
|
|
34
|
+
if (aNum && bNum) return a > b ? 1 : -1
|
|
35
|
+
if (aNum) return -1
|
|
36
|
+
if (bNum) return 1
|
|
37
|
+
return String(a) > String(b) ? 1 : -1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function compareVersions(a, b) {
|
|
41
|
+
const av = parseVersion(a)
|
|
42
|
+
const bv = parseVersion(b)
|
|
43
|
+
if (!av || !bv) return String(a || "").localeCompare(String(b || ""))
|
|
44
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
45
|
+
if (av[key] !== bv[key]) return av[key] > bv[key] ? 1 : -1
|
|
46
|
+
}
|
|
47
|
+
const aPre = av.prerelease
|
|
48
|
+
const bPre = bv.prerelease
|
|
49
|
+
if (!aPre.length && !bPre.length) return 0
|
|
50
|
+
if (!aPre.length) return 1
|
|
51
|
+
if (!bPre.length) return -1
|
|
52
|
+
const len = Math.max(aPre.length, bPre.length)
|
|
53
|
+
for (let i = 0; i < len; i++) {
|
|
54
|
+
if (aPre[i] === undefined) return -1
|
|
55
|
+
if (bPre[i] === undefined) return 1
|
|
56
|
+
const cmp = compareIdentifier(aPre[i], bPre[i])
|
|
57
|
+
if (cmp !== 0) return cmp
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function updateConfig(config = {}) {
|
|
63
|
+
return {
|
|
64
|
+
enabled: config.update?.enabled !== false,
|
|
65
|
+
notifyOnStartup: config.update?.notify_on_startup !== false,
|
|
66
|
+
autoInstall: Boolean(config.update?.auto_install),
|
|
67
|
+
channel: config.update?.channel || "latest",
|
|
68
|
+
checkIntervalHours: Number(config.update?.check_interval_hours ?? 12),
|
|
69
|
+
registry: config.update?.registry || DEFAULT_REGISTRY,
|
|
70
|
+
timeoutMs: Number(config.update?.timeout_ms ?? DEFAULT_TIMEOUT_MS)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readUpdateState(file = updateStatePath()) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(await readFile(file, "utf8"))
|
|
77
|
+
} catch {
|
|
78
|
+
return {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writeUpdateState(state, file = updateStatePath()) {
|
|
83
|
+
await mkdir(dirname(file), { recursive: true })
|
|
84
|
+
await writeFile(file, `${JSON.stringify(state, null, 2)}\n`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function fetchPackageMetadata({ packageName = PACKAGE_NAME, registry = DEFAULT_REGISTRY, timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl = globalThis.fetch } = {}) {
|
|
88
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this Node runtime")
|
|
89
|
+
const controller = new AbortController()
|
|
90
|
+
const timer = setTimeout(() => controller.abort(), Math.max(100, Number(timeoutMs || DEFAULT_TIMEOUT_MS)))
|
|
91
|
+
try {
|
|
92
|
+
const url = `${normalizeRegistry(registry)}/${encodePackageName(packageName)}`
|
|
93
|
+
const res = await fetchImpl(url, {
|
|
94
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
95
|
+
signal: controller.signal
|
|
96
|
+
})
|
|
97
|
+
if (!res.ok) throw new Error(`npm registry returned HTTP ${res.status}`)
|
|
98
|
+
return await res.json()
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function checkForUpdate(config = {}, options = {}) {
|
|
105
|
+
const cfg = updateConfig(config)
|
|
106
|
+
if (!cfg.enabled && !options.force) return { ok: false, skipped: true, reason: "disabled" }
|
|
107
|
+
|
|
108
|
+
const now = Number(options.now ?? Date.now())
|
|
109
|
+
const stateFile = options.stateFile || updateStatePath()
|
|
110
|
+
const state = options.state ?? await readUpdateState(stateFile)
|
|
111
|
+
const intervalMs = Math.max(0, cfg.checkIntervalHours) * 60 * 60 * 1000
|
|
112
|
+
if (!options.force && intervalMs > 0 && state.checkedAt && now - Date.parse(state.checkedAt) < intervalMs) {
|
|
113
|
+
return { ok: true, skipped: true, reason: "interval", state }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const metadata = await fetchPackageMetadata({
|
|
117
|
+
packageName: options.packageName || PACKAGE_NAME,
|
|
118
|
+
registry: cfg.registry,
|
|
119
|
+
timeoutMs: cfg.timeoutMs,
|
|
120
|
+
fetchImpl: options.fetchImpl
|
|
121
|
+
})
|
|
122
|
+
const distTags = metadata["dist-tags"] || {}
|
|
123
|
+
const latestVersion = distTags[cfg.channel] || distTags.latest || metadata.version
|
|
124
|
+
const currentVersion = options.currentVersion || PACKAGE_VERSION
|
|
125
|
+
const hasUpdate = Boolean(latestVersion && compareVersions(latestVersion, currentVersion) > 0)
|
|
126
|
+
const result = {
|
|
127
|
+
ok: true,
|
|
128
|
+
packageName: options.packageName || PACKAGE_NAME,
|
|
129
|
+
channel: cfg.channel,
|
|
130
|
+
currentVersion,
|
|
131
|
+
latestVersion,
|
|
132
|
+
hasUpdate,
|
|
133
|
+
installSpec: `${options.packageName || PACKAGE_NAME}@${cfg.channel}`,
|
|
134
|
+
checkedAt: new Date(now).toISOString()
|
|
135
|
+
}
|
|
136
|
+
await writeUpdateState(result, stateFile)
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function updateMessage(result) {
|
|
141
|
+
if (!result?.hasUpdate) return null
|
|
142
|
+
return `Update available: kkcode ${result.currentVersion} -> ${result.latestVersion} (${result.channel}). Run: kkcode update --channel ${result.channel}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function maybeNotifyUpdateOnStartup(config = {}, options = {}) {
|
|
146
|
+
const cfg = updateConfig(config)
|
|
147
|
+
if (!cfg.enabled || !cfg.notifyOnStartup || process.env.KKCODE_DISABLE_UPDATE_CHECK === "1") return null
|
|
148
|
+
try {
|
|
149
|
+
const result = await checkForUpdate(config, options)
|
|
150
|
+
const message = updateMessage(result)
|
|
151
|
+
if (message) {
|
|
152
|
+
const print = options.print || console.error
|
|
153
|
+
print(message)
|
|
154
|
+
if (cfg.autoInstall) {
|
|
155
|
+
const install = await installUpdate(config, { channel: result.channel, stdio: "ignore" })
|
|
156
|
+
if (install.ok) print(`kkcode update installed ${result.latestVersion}; restart kkcode to use it.`)
|
|
157
|
+
else print(`kkcode auto-update failed: ${install.error}`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (options.verbose) (options.print || console.error)(`kkcode update check failed: ${error.message}`)
|
|
163
|
+
return { ok: false, error: error.message }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function runCommand(command, args, { cwd = process.cwd(), env = process.env, stdio = "inherit" } = {}) {
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
const child = spawn(command, args, { cwd, env, stdio, shell: process.platform === "win32" })
|
|
170
|
+
child.on("exit", (code) => resolve({ ok: code === 0, code }))
|
|
171
|
+
child.on("error", (error) => resolve({ ok: false, code: 1, error: error.message }))
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function installUpdate(config = {}, options = {}) {
|
|
176
|
+
const cfg = updateConfig(config)
|
|
177
|
+
const channel = options.channel || cfg.channel || "latest"
|
|
178
|
+
const packageName = options.packageName || PACKAGE_NAME
|
|
179
|
+
const npm = options.npmCommand || process.env.npm_execpath || "npm"
|
|
180
|
+
const args = ["install", "-g", `${packageName}@${channel}`]
|
|
181
|
+
const result = await (options.runCommand || runCommand)(npm, args, options)
|
|
182
|
+
if (!result.ok) return { ok: false, code: result.code, error: result.error || `npm exited with ${result.code}` }
|
|
183
|
+
return { ok: true, command: npm, args }
|
|
184
|
+
}
|
package/src/version.mjs
ADDED