pikiclaw 0.2.56 → 0.2.58

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.
@@ -0,0 +1,194 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { getAgentLabel, getAgentPackage } from './agent-npm.js';
6
+ const AGENT_UPDATE_LOCK_STALE_MS = 60 * 60_000;
7
+ const AGENT_UPDATE_COMMAND_TIMEOUT_MS = 15 * 60_000;
8
+ function updaterLockPath() {
9
+ return path.join(os.homedir(), '.pikiclaw', 'agent-auto-update.lock');
10
+ }
11
+ function normalizeBooleanEnv(value) {
12
+ const text = String(value || '').trim();
13
+ if (!text)
14
+ return null;
15
+ if (/^(1|true|yes|on)$/i.test(text))
16
+ return true;
17
+ if (/^(0|false|no|off)$/i.test(text))
18
+ return false;
19
+ return null;
20
+ }
21
+ export function agentAutoUpdateEnabled(config) {
22
+ const env = normalizeBooleanEnv(process.env.PIKICLAW_AGENT_AUTO_UPDATE);
23
+ if (env != null)
24
+ return env;
25
+ if (typeof config.agentAutoUpdate === 'boolean')
26
+ return config.agentAutoUpdate;
27
+ return true;
28
+ }
29
+ export function extractAgentSemver(value) {
30
+ const text = String(value || '').trim();
31
+ if (!text)
32
+ return null;
33
+ const match = text.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
34
+ return match?.[0] || null;
35
+ }
36
+ function isPathInside(parentDir, childPath) {
37
+ const parent = path.resolve(parentDir);
38
+ const child = path.resolve(childPath);
39
+ return child === parent || child.startsWith(`${parent}${path.sep}`);
40
+ }
41
+ export function resolveAgentUpdateStrategy(agent, npmPrefix) {
42
+ const id = String(agent.agent || '').trim();
43
+ const pkg = getAgentPackage(id);
44
+ if (!pkg)
45
+ return { kind: 'skip', reason: 'unsupported agent' };
46
+ const binPath = String(agent.path || '').trim();
47
+ const npmBinDir = npmPrefix ? path.join(path.resolve(npmPrefix), 'bin') : null;
48
+ const npmManaged = !!(binPath && npmBinDir && isPathInside(npmBinDir, binPath));
49
+ if (npmManaged)
50
+ return { kind: 'npm', pkg };
51
+ return { kind: 'skip', reason: 'non-npm install path' };
52
+ }
53
+ function labelForAgent(agent) {
54
+ return getAgentLabel(agent);
55
+ }
56
+ async function runCommand(cmd, args, opts = {}) {
57
+ return new Promise(resolve => {
58
+ let stdout = '';
59
+ let stderr = '';
60
+ let finished = false;
61
+ const child = spawn(cmd, args, {
62
+ stdio: ['ignore', 'pipe', 'pipe'],
63
+ env: { ...process.env, npm_config_yes: 'true' },
64
+ });
65
+ const timeoutMs = Math.max(500, opts.timeoutMs ?? AGENT_UPDATE_COMMAND_TIMEOUT_MS);
66
+ const timer = setTimeout(() => {
67
+ if (finished)
68
+ return;
69
+ finished = true;
70
+ child.kill('SIGTERM');
71
+ resolve({ ok: false, code: null, stdout, stderr, error: `Timed out after ${Math.round(timeoutMs / 1000)}s` });
72
+ }, timeoutMs);
73
+ child.stdout?.on('data', chunk => { stdout += String(chunk); });
74
+ child.stderr?.on('data', chunk => { stderr += String(chunk); });
75
+ child.on('error', err => {
76
+ if (finished)
77
+ return;
78
+ finished = true;
79
+ clearTimeout(timer);
80
+ resolve({ ok: false, code: null, stdout, stderr, error: err.message });
81
+ });
82
+ child.on('close', code => {
83
+ if (finished)
84
+ return;
85
+ finished = true;
86
+ clearTimeout(timer);
87
+ resolve({
88
+ ok: code === 0,
89
+ code,
90
+ stdout,
91
+ stderr,
92
+ error: code === 0 ? null : (stderr.trim() || stdout.trim() || `Exited with code ${code}`),
93
+ });
94
+ });
95
+ });
96
+ }
97
+ async function getNpmGlobalPrefix() {
98
+ const result = await runCommand('npm', ['prefix', '-g'], { timeoutMs: 10_000 });
99
+ return result.ok ? result.stdout.trim().split('\n')[0] || null : null;
100
+ }
101
+ async function getLatestPackageVersion(pkg) {
102
+ const result = await runCommand('npm', ['view', pkg, 'version', '--json'], { timeoutMs: 20_000 });
103
+ if (!result.ok)
104
+ return null;
105
+ const raw = result.stdout.trim();
106
+ if (!raw)
107
+ return null;
108
+ try {
109
+ const parsed = JSON.parse(raw);
110
+ return typeof parsed === 'string' ? parsed.trim() || null : null;
111
+ }
112
+ catch {
113
+ return raw.replace(/^"+|"+$/g, '').trim() || null;
114
+ }
115
+ }
116
+ function acquireUpdateLock(log) {
117
+ const filePath = updaterLockPath();
118
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
119
+ try {
120
+ const stat = fs.statSync(filePath);
121
+ if (Date.now() - stat.mtimeMs > AGENT_UPDATE_LOCK_STALE_MS)
122
+ fs.rmSync(filePath, { force: true });
123
+ }
124
+ catch { }
125
+ try {
126
+ fs.writeFileSync(filePath, `${process.pid}\n`, { flag: 'wx' });
127
+ let released = false;
128
+ return () => {
129
+ if (released)
130
+ return;
131
+ released = true;
132
+ try {
133
+ fs.rmSync(filePath, { force: true });
134
+ }
135
+ catch { }
136
+ };
137
+ }
138
+ catch {
139
+ log('agent auto-update already running in another process; skipping this startup check');
140
+ return null;
141
+ }
142
+ }
143
+ async function updateViaNpm(pkg) {
144
+ const result = await runCommand('npm', ['install', '-g', `${pkg}@latest`]);
145
+ return { ok: result.ok, detail: result.ok ? result.stdout.trim() || null : result.error };
146
+ }
147
+ export function startAgentAutoUpdate(opts) {
148
+ if (!agentAutoUpdateEnabled(opts.config))
149
+ return;
150
+ const installedAgents = opts.agents.filter(agent => agent.installed && agent.path);
151
+ if (!installedAgents.length)
152
+ return;
153
+ const releaseLock = acquireUpdateLock(opts.log);
154
+ if (!releaseLock)
155
+ return;
156
+ void (async () => {
157
+ try {
158
+ opts.log(`agent auto-update: checking ${installedAgents.length} installed agent${installedAgents.length === 1 ? '' : 's'} in background`);
159
+ const npmPrefix = await getNpmGlobalPrefix();
160
+ for (const agent of installedAgents) {
161
+ const id = String(agent.agent || '').trim();
162
+ const pkg = getAgentPackage(id);
163
+ if (!pkg)
164
+ continue;
165
+ const label = labelForAgent(id);
166
+ const currentVersion = extractAgentSemver(agent.version);
167
+ const latestVersion = await getLatestPackageVersion(pkg);
168
+ if (!latestVersion) {
169
+ opts.log(`agent auto-update: ${label} latest version lookup failed`);
170
+ continue;
171
+ }
172
+ if (currentVersion === latestVersion) {
173
+ opts.log(`agent auto-update: ${label} is already up to date (${latestVersion})`);
174
+ continue;
175
+ }
176
+ const strategy = resolveAgentUpdateStrategy(agent, npmPrefix);
177
+ if (strategy.kind === 'skip') {
178
+ opts.log(`agent auto-update: ${label} is ${currentVersion || 'unknown'} and latest is ${latestVersion}, but update is skipped (${strategy.reason})`);
179
+ continue;
180
+ }
181
+ opts.log(`agent auto-update: updating ${label} ${currentVersion || 'unknown'} -> ${latestVersion}`);
182
+ const result = await updateViaNpm(strategy.pkg);
183
+ if (result.ok)
184
+ opts.log(`agent auto-update: ${label} update completed`);
185
+ else
186
+ opts.log(`agent auto-update: ${label} update failed: ${result.detail || 'unknown error'}`);
187
+ }
188
+ opts.log('agent auto-update: finished');
189
+ }
190
+ finally {
191
+ releaseLock();
192
+ }
193
+ })();
194
+ }
@@ -0,0 +1,20 @@
1
+ const AGENT_PACKAGES = {
2
+ claude: '@anthropic-ai/claude-code',
3
+ codex: '@openai/codex',
4
+ gemini: '@google/gemini-cli',
5
+ };
6
+ const AGENT_LABELS = {
7
+ claude: 'Claude Code',
8
+ codex: 'Codex',
9
+ gemini: 'Gemini CLI',
10
+ };
11
+ export function getAgentPackage(agent) {
12
+ return AGENT_PACKAGES[agent] || null;
13
+ }
14
+ export function getAgentLabel(agent) {
15
+ return AGENT_LABELS[agent] || agent;
16
+ }
17
+ export function getAgentInstallCommand(agent) {
18
+ const pkg = getAgentPackage(agent);
19
+ return pkg ? `npm install -g ${pkg}` : null;
20
+ }
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import path from 'node:path';
12
12
  import { fmtTokens, fmtUptime, fmtBytes } from './bot.js';
13
- import { getProjectSkillPaths } from './code-agent.js';
13
+ import { getProjectSkillPaths, normalizeClaudeModelId } from './code-agent.js';
14
14
  import { getDriver } from './agent-driver.js';
15
15
  import { buildWelcomeIntro, buildSkillCommandName, indexSkillsByCommand, SKILL_CMD_PREFIX } from './bot-menu.js';
16
16
  import { buildBotMenuState } from './bot-orchestration.js';
@@ -156,15 +156,13 @@ export function getSkillsListData(bot, chatId) {
156
156
  return { agent: cs.agent, workdir: bot.workdir, skills };
157
157
  }
158
158
  function claudeModelSelectionKey(modelId) {
159
- const value = String(modelId || '').trim().toLowerCase();
159
+ const value = normalizeClaudeModelId(modelId).toLowerCase();
160
160
  if (!value)
161
161
  return null;
162
- if (value === 'opus' || value === 'opus-1m' || value.startsWith('claude-opus-')) {
163
- return value === 'opus-1m' || value.endsWith('[1m]') ? 'opus-1m' : 'opus';
164
- }
165
- if (value === 'sonnet' || value === 'sonnet-1m' || value.startsWith('claude-sonnet-')) {
166
- return value === 'sonnet-1m' || value.endsWith('[1m]') ? 'sonnet-1m' : 'sonnet';
167
- }
162
+ if (value === 'opus' || value.startsWith('claude-opus-'))
163
+ return 'opus';
164
+ if (value === 'sonnet' || value.startsWith('claude-sonnet-'))
165
+ return 'sonnet';
168
166
  if (value === 'haiku' || value.startsWith('claude-haiku-'))
169
167
  return 'haiku';
170
168
  return null;
package/dist/bot.js CHANGED
@@ -8,7 +8,7 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { execSync, spawn } from 'node:child_process';
10
10
  import { getActiveUserConfig, onUserConfigChange, resolveUserWorkdir, setUserWorkdir } from './user-config.js';
11
- import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, stageSessionFiles, isPendingSessionId, } from './code-agent.js';
11
+ import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, stageSessionFiles, isPendingSessionId, normalizeClaudeModelId, } from './code-agent.js';
12
12
  import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
13
13
  import { terminateProcessTree } from './process-control.js';
14
14
  import { VERSION } from './version.js';
@@ -221,7 +221,7 @@ function buildMcpDeliveryPrompt() {
221
221
  }
222
222
  function configModelValue(config, agent) {
223
223
  switch (agent) {
224
- case 'claude': return String(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6').trim();
224
+ case 'claude': return normalizeClaudeModelId(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6');
225
225
  case 'codex': return String(config.codexModel || process.env.CODEX_MODEL || 'gpt-5.4').trim();
226
226
  case 'gemini': return String(config.geminiModel || process.env.GEMINI_MODEL || 'gemini-3.1-pro-preview').trim();
227
227
  }
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
6
  import path from 'node:path';
7
+ import { startAgentAutoUpdate } from './agent-auto-update.js';
7
8
  import { envBool } from './bot.js';
8
9
  import { TelegramBot } from './bot-telegram.js';
9
10
  import { hasConfiguredChannelToken, resolveConfiguredChannels } from './cli-channels.js';
@@ -434,6 +435,11 @@ Docs: https://github.com/xiaotonng/pikiclaw
434
435
  if (args.allowedIds && channel === 'telegram')
435
436
  runtimeConfig.telegramAllowedChatIds = args.allowedIds;
436
437
  applyUserConfig(runtimeConfig, undefined, { overwrite: true, clearMissing: true });
438
+ startAgentAutoUpdate({
439
+ config: runtimeConfig,
440
+ agents: listAgents({ includeVersion: true, refresh: true }).agents,
441
+ log: processLog,
442
+ });
437
443
  if (args.model) {
438
444
  const ag = args.agent || runtimeConfig.defaultAgent || 'codex';
439
445
  if (ag === 'codex')
@@ -315,6 +315,9 @@ export function modelFamily(model) {
315
315
  return 'sonnet';
316
316
  return null;
317
317
  }
318
+ export function normalizeClaudeModelId(model) {
319
+ return typeof model === 'string' ? model.trim() : '';
320
+ }
318
321
  export function emptyUsage(agent, error) {
319
322
  return { ok: false, agent, source: null, capturedAt: null, status: null, windows: [], error };
320
323
  }