@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # kkcode
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@kkelly-offical/kkcode?label=v0.2.1)](https://www.npmjs.com/package/@kkelly-offical/kkcode)
3
+ [![npm version](https://img.shields.io/npm/v/@kkelly-offical/kkcode?label=v0.2.3-preview.1)](https://www.npmjs.com/package/@kkelly-offical/kkcode)
4
4
  [![GitHub Release](https://img.shields.io/github/v/release/kkelly-offical/kkcode)](https://github.com/kkelly-offical/kkcode/releases)
5
5
  ![Node](https://img.shields.io/badge/Node.js-%3E%3D22-green)
6
6
  ![License](https://img.shields.io/badge/License-GPL--3.0-blue)
@@ -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 stable / 当前稳定版本**: `v0.2.1`
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` rebuilds kkcode around Assistant as the default general-purpose lane, with dedicated Agent and LongAgent modes for coding work.
363
- - The public Ask lane has been removed; question, explanation, and lightweight personal-assistant tasks now route to Assistant.
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
+ }
@@ -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: {
@@ -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("0.2.1")
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
 
@@ -1,2 +1,4 @@
1
+ import { PACKAGE_VERSION } from "../version.mjs"
2
+
1
3
  export const MCP_PROTOCOL_VERSION = "2024-11-05"
2
- export const MCP_CLIENT_INFO = { name: "kkcode", version: "0.2.1" }
4
+ export const MCP_CLIENT_INFO = { name: "kkcode", version: PACKAGE_VERSION }
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: "v0.2.1" })
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() })
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export const PACKAGE_NAME = "@kkelly-offical/kkcode"
2
+ export const PACKAGE_VERSION = "0.2.3-preview.1"
3
+ export const RELEASE_LABEL = "0.2.3 Preview V1"