pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { buildSetupGuide, collectSetupState } from './onboarding.js';
|
|
4
|
+
import { getUserConfigPath, saveUserConfig } from './user-config.js';
|
|
5
|
+
function createTerminalIO() {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
return {
|
|
8
|
+
ask(prompt) {
|
|
9
|
+
return rl.question(prompt);
|
|
10
|
+
},
|
|
11
|
+
write(text) {
|
|
12
|
+
process.stdout.write(text);
|
|
13
|
+
},
|
|
14
|
+
runCommand(command, args = []) {
|
|
15
|
+
return new Promise(resolve => {
|
|
16
|
+
const child = spawn(command, args, {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
env: process.env,
|
|
19
|
+
});
|
|
20
|
+
child.on('exit', code => resolve(code ?? 1));
|
|
21
|
+
child.on('error', () => resolve(1));
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
close() {
|
|
25
|
+
void rl.close();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function parseChoice(raw) {
|
|
30
|
+
return String(raw || '').trim().toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
async function askYesNo(io, prompt, def) {
|
|
33
|
+
const suffix = def ? ' [Y/n] ' : ' [y/N] ';
|
|
34
|
+
for (;;) {
|
|
35
|
+
const answer = parseChoice(await io.ask(`${prompt}${suffix}`));
|
|
36
|
+
if (!answer)
|
|
37
|
+
return def;
|
|
38
|
+
if (['y', 'yes'].includes(answer))
|
|
39
|
+
return true;
|
|
40
|
+
if (['n', 'no'].includes(answer))
|
|
41
|
+
return false;
|
|
42
|
+
io.write('Please answer y or n.\n');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function askAgentChoice(io, installed) {
|
|
46
|
+
io.write('Choose your local coding agent:\n');
|
|
47
|
+
io.write(` 1. Codex${installed.some(a => a.agent === 'codex' && a.installed) ? ' (installed)' : ''}\n`);
|
|
48
|
+
io.write(` 2. Claude Code${installed.some(a => a.agent === 'claude' && a.installed) ? ' (installed)' : ''}\n`);
|
|
49
|
+
io.write(' q. Quit setup\n');
|
|
50
|
+
for (;;) {
|
|
51
|
+
const answer = parseChoice(await io.ask('Selection [1/2/q]: '));
|
|
52
|
+
if (answer === '1' || answer === 'codex')
|
|
53
|
+
return 'codex';
|
|
54
|
+
if (answer === '2' || answer === 'claude')
|
|
55
|
+
return 'claude';
|
|
56
|
+
if (answer === 'q' || answer === 'quit')
|
|
57
|
+
return null;
|
|
58
|
+
io.write('Please choose 1, 2, or q.\n');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function preferredInstalledAgent(agents) {
|
|
62
|
+
if (agents.some(agent => agent.agent === 'codex' && agent.installed))
|
|
63
|
+
return 'codex';
|
|
64
|
+
if (agents.some(agent => agent.agent === 'claude' && agent.installed))
|
|
65
|
+
return 'claude';
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function parseAgent(value) {
|
|
69
|
+
return value === 'claude' || value === 'codex' ? value : null;
|
|
70
|
+
}
|
|
71
|
+
function title(label) {
|
|
72
|
+
return `\n${label}\n${'-'.repeat(label.length)}\n`;
|
|
73
|
+
}
|
|
74
|
+
export async function validateTelegramToken(token) {
|
|
75
|
+
const value = String(token || '').trim();
|
|
76
|
+
if (!value)
|
|
77
|
+
return { ok: false, bot: null, error: 'The token was empty.' };
|
|
78
|
+
try {
|
|
79
|
+
const resp = await fetch(`https://api.telegram.org/bot${value}/getMe`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
signal: AbortSignal.timeout(8_000),
|
|
82
|
+
});
|
|
83
|
+
const raw = await resp.text();
|
|
84
|
+
let parsed = null;
|
|
85
|
+
try {
|
|
86
|
+
parsed = raw ? JSON.parse(raw) : null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { ok: false, bot: null, error: `Telegram returned invalid JSON (${resp.status}).` };
|
|
90
|
+
}
|
|
91
|
+
if (!resp.ok || parsed?.ok !== true || !parsed?.result) {
|
|
92
|
+
const detail = typeof parsed?.description === 'string' ? parsed.description : `HTTP ${resp.status}`;
|
|
93
|
+
return { ok: false, bot: null, error: `Telegram rejected this token: ${detail}` };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
bot: {
|
|
98
|
+
id: parsed.result.id,
|
|
99
|
+
username: parsed.result.username || null,
|
|
100
|
+
displayName: parsed.result.first_name || null,
|
|
101
|
+
},
|
|
102
|
+
error: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err ?? 'unknown error');
|
|
107
|
+
return { ok: false, bot: null, error: `Failed to reach Telegram: ${msg}` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function runSetupWizard(options) {
|
|
111
|
+
const io = options.io || createTerminalIO();
|
|
112
|
+
const validate = options.validateTelegramToken || validateTelegramToken;
|
|
113
|
+
const persistConfig = options.persistConfig || saveUserConfig;
|
|
114
|
+
let state = options.initialState;
|
|
115
|
+
let selectedAgent = parseAgent(options.argsAgent);
|
|
116
|
+
let token = String(options.currentToken || '').trim() || null;
|
|
117
|
+
let tokenCheck = null;
|
|
118
|
+
let configPath = null;
|
|
119
|
+
const refreshState = () => {
|
|
120
|
+
state = collectSetupState({
|
|
121
|
+
agents: options.listAgents(),
|
|
122
|
+
channel: options.channel,
|
|
123
|
+
tokenProvided: !!token,
|
|
124
|
+
});
|
|
125
|
+
return state;
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
io.write(title(`pikiclaw v${options.version} setup`));
|
|
129
|
+
if (options.channel !== 'telegram') {
|
|
130
|
+
io.write(buildSetupGuide(state, options.version));
|
|
131
|
+
io.write('\nInteractive setup is currently only available for Telegram.\n');
|
|
132
|
+
return { completed: false, token, agent: null, configPath: null, tokenCheck: null };
|
|
133
|
+
}
|
|
134
|
+
io.write('This wizard will help you install or verify a local agent, validate your Telegram bot token, and optionally save the setup for next time.\n');
|
|
135
|
+
refreshState();
|
|
136
|
+
io.write(title('Step 1: Local agent'));
|
|
137
|
+
if (!selectedAgent) {
|
|
138
|
+
const installedAgent = preferredInstalledAgent(state.agents);
|
|
139
|
+
if (state.agents.filter(agent => agent.installed).length > 1) {
|
|
140
|
+
selectedAgent = await askAgentChoice(io, state.agents);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
selectedAgent = installedAgent || await askAgentChoice(io, state.agents);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!selectedAgent) {
|
|
147
|
+
io.write('Setup cancelled.\n');
|
|
148
|
+
return { completed: false, token, agent: null, configPath: null, tokenCheck: null };
|
|
149
|
+
}
|
|
150
|
+
let selectedState = refreshState().agents.find(agent => agent.agent === selectedAgent) || null;
|
|
151
|
+
if (!selectedState?.installed) {
|
|
152
|
+
io.write(`${selectedState?.label || selectedAgent} is not installed.\n`);
|
|
153
|
+
const installNow = await askYesNo(io, `Install ${selectedAgent === 'claude' ? 'Claude Code' : 'Codex'} now?`, true);
|
|
154
|
+
if (!installNow) {
|
|
155
|
+
io.write('Setup cancelled. Install the agent first, then run `npx pikiclaw@latest` again.\n');
|
|
156
|
+
return { completed: false, token, agent: selectedAgent, configPath: null, tokenCheck: null };
|
|
157
|
+
}
|
|
158
|
+
io.write(`Running: ${selectedState?.installCommand || `npm install -g ${selectedAgent}`}\n`);
|
|
159
|
+
const exitCode = await io.runCommand('npm', ['install', '-g', selectedAgent === 'claude' ? '@anthropic-ai/claude-code' : '@openai/codex']);
|
|
160
|
+
selectedState = refreshState().agents.find(agent => agent.agent === selectedAgent) || null;
|
|
161
|
+
if (exitCode !== 0 || !selectedState?.installed) {
|
|
162
|
+
io.write(`Install did not complete successfully. You can try manually: ${selectedState?.installCommand || ''}\n`);
|
|
163
|
+
return { completed: false, token, agent: selectedAgent, configPath: null, tokenCheck: null };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
io.write(`Using ${selectedState.label}.\n`);
|
|
167
|
+
io.write(title('Step 2: Telegram bot token'));
|
|
168
|
+
if (token) {
|
|
169
|
+
tokenCheck = await validate(token);
|
|
170
|
+
if (tokenCheck.ok) {
|
|
171
|
+
io.write(`Existing token looks valid for @${tokenCheck.bot?.username || 'unknown_bot'}.\n`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
io.write(`The existing token could not be verified: ${tokenCheck.error}\n`);
|
|
175
|
+
token = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!token) {
|
|
179
|
+
io.write('Get a bot token from Telegram first:\n');
|
|
180
|
+
io.write(' 1. Open Telegram and search for @BotFather\n');
|
|
181
|
+
io.write(' 2. Send /newbot\n');
|
|
182
|
+
io.write(' 3. Choose a display name and a username\n');
|
|
183
|
+
io.write(' 4. Paste the token here\n');
|
|
184
|
+
}
|
|
185
|
+
while (!token) {
|
|
186
|
+
const answer = String(await io.ask('Paste your Telegram bot token (or type q to quit): ')).trim();
|
|
187
|
+
if (!answer)
|
|
188
|
+
continue;
|
|
189
|
+
if (parseChoice(answer) === 'q') {
|
|
190
|
+
io.write('Setup cancelled.\n');
|
|
191
|
+
return { completed: false, token: null, agent: selectedAgent, configPath: null, tokenCheck: null };
|
|
192
|
+
}
|
|
193
|
+
const check = await validate(answer);
|
|
194
|
+
if (!check.ok) {
|
|
195
|
+
io.write(`${check.error}\n`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
token = answer;
|
|
199
|
+
tokenCheck = check;
|
|
200
|
+
io.write(`Telegram bot verified: @${check.bot?.username || 'unknown_bot'}${check.bot?.displayName ? ` (${check.bot.displayName})` : ''}\n`);
|
|
201
|
+
}
|
|
202
|
+
io.write(title('Step 3: Save setup'));
|
|
203
|
+
const configFile = getUserConfigPath();
|
|
204
|
+
const saveIt = await askYesNo(io, `Save this setup to ${configFile}?`, true);
|
|
205
|
+
if (saveIt) {
|
|
206
|
+
configPath = persistConfig({
|
|
207
|
+
channel: 'telegram',
|
|
208
|
+
defaultAgent: selectedAgent,
|
|
209
|
+
telegramBotToken: token,
|
|
210
|
+
});
|
|
211
|
+
io.write(`Saved config to ${configPath}\n`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
io.write('Skipping config save. This run will still start now, but you may need to provide the token again next time.\n');
|
|
215
|
+
}
|
|
216
|
+
io.write(title('Ready'));
|
|
217
|
+
io.write(`Agent: ${selectedAgent}\n`);
|
|
218
|
+
io.write(`Telegram bot: @${tokenCheck?.bot?.username || 'unknown_bot'}\n`);
|
|
219
|
+
io.write('Starting pikiclaw now...\n');
|
|
220
|
+
return {
|
|
221
|
+
completed: true,
|
|
222
|
+
token,
|
|
223
|
+
agent: selectedAgent,
|
|
224
|
+
configPath,
|
|
225
|
+
tokenCheck,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
io.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const MANAGED_ENV_KEYS = [
|
|
5
|
+
'PIKICLAW_CHANNEL',
|
|
6
|
+
'PIKICLAW_WORKDIR',
|
|
7
|
+
'DEFAULT_AGENT',
|
|
8
|
+
'TELEGRAM_BOT_TOKEN',
|
|
9
|
+
'TELEGRAM_ALLOWED_CHAT_IDS',
|
|
10
|
+
'FEISHU_APP_ID',
|
|
11
|
+
'FEISHU_APP_SECRET',
|
|
12
|
+
];
|
|
13
|
+
let activeUserConfig = {};
|
|
14
|
+
const userConfigListeners = new Set();
|
|
15
|
+
let userConfigSyncTimer = null;
|
|
16
|
+
let userConfigSyncRefCount = 0;
|
|
17
|
+
let userConfigSyncRaw = '';
|
|
18
|
+
let userConfigSyncOverrides = {};
|
|
19
|
+
function expandHomeDir(value) {
|
|
20
|
+
return value.replace(/^~/, process.env.HOME || '');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Single canonical config path: ~/.pikiclaw/setting.json
|
|
24
|
+
* Both CLI and dashboard read/write this file exclusively.
|
|
25
|
+
*/
|
|
26
|
+
export function getUserConfigPath() {
|
|
27
|
+
const custom = (process.env.PIKICLAW_CONFIG || '').trim();
|
|
28
|
+
if (custom)
|
|
29
|
+
return path.resolve(custom);
|
|
30
|
+
return path.join(os.homedir(), '.pikiclaw', 'setting.json');
|
|
31
|
+
}
|
|
32
|
+
function loadJsonFile(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
return typeof parsed === 'object' && parsed ? normalizeUserConfig(parsed) : {};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function normalizeUserConfig(config) {
|
|
43
|
+
const next = { ...config };
|
|
44
|
+
const workdir = typeof next.workdir === 'string' && next.workdir.trim() ? next.workdir.trim() : '';
|
|
45
|
+
if (workdir)
|
|
46
|
+
next.workdir = resolveUserWorkdir({ workdir });
|
|
47
|
+
else
|
|
48
|
+
delete next.workdir;
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
export function loadUserConfig() {
|
|
52
|
+
return loadJsonFile(getUserConfigPath());
|
|
53
|
+
}
|
|
54
|
+
export function hasUserConfigFile() {
|
|
55
|
+
try {
|
|
56
|
+
return fs.existsSync(getUserConfigPath());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function getActiveUserConfig() {
|
|
63
|
+
return activeUserConfig;
|
|
64
|
+
}
|
|
65
|
+
export function saveUserConfig(config) {
|
|
66
|
+
const filePath = getUserConfigPath();
|
|
67
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
68
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ version: 1, ...normalizeUserConfig(config) }, null, 2)}\n`, { mode: 0o600 });
|
|
69
|
+
return filePath;
|
|
70
|
+
}
|
|
71
|
+
export function updateUserConfig(patch) {
|
|
72
|
+
return saveUserConfig({ ...loadUserConfig(), ...patch });
|
|
73
|
+
}
|
|
74
|
+
export function resolveUserWorkdir(opts = {}) {
|
|
75
|
+
const raw = String(opts.workdir
|
|
76
|
+
|| opts.config?.workdir
|
|
77
|
+
|| process.env.PIKICLAW_WORKDIR
|
|
78
|
+
|| opts.cwd
|
|
79
|
+
|| process.cwd()).trim();
|
|
80
|
+
return path.resolve(expandHomeDir(raw));
|
|
81
|
+
}
|
|
82
|
+
function buildManagedEnv(config) {
|
|
83
|
+
const configuredWorkdir = config.workdir || '';
|
|
84
|
+
return {
|
|
85
|
+
PIKICLAW_CHANNEL: String(config.channel || '').trim(),
|
|
86
|
+
PIKICLAW_WORKDIR: configuredWorkdir ? resolveUserWorkdir({ workdir: configuredWorkdir }) : '',
|
|
87
|
+
DEFAULT_AGENT: String(config.defaultAgent || '').trim(),
|
|
88
|
+
TELEGRAM_BOT_TOKEN: String(config.telegramBotToken || '').trim(),
|
|
89
|
+
TELEGRAM_ALLOWED_CHAT_IDS: String(config.telegramAllowedChatIds || '').trim(),
|
|
90
|
+
FEISHU_APP_ID: String(config.feishuAppId || '').trim(),
|
|
91
|
+
FEISHU_APP_SECRET: String(config.feishuAppSecret || '').trim(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function notifyUserConfigListeners(config, changedKeys) {
|
|
95
|
+
for (const listener of userConfigListeners) {
|
|
96
|
+
try {
|
|
97
|
+
listener(config, changedKeys);
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function readUserConfigRaw() {
|
|
103
|
+
try {
|
|
104
|
+
return fs.readFileSync(getUserConfigPath(), 'utf-8');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function onUserConfigChange(listener) {
|
|
111
|
+
userConfigListeners.add(listener);
|
|
112
|
+
return () => userConfigListeners.delete(listener);
|
|
113
|
+
}
|
|
114
|
+
function configValuesEqual(a, b) {
|
|
115
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
116
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
117
|
+
return a === b;
|
|
118
|
+
}
|
|
119
|
+
function diffConfigKeys(prev, next) {
|
|
120
|
+
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
121
|
+
const changed = [];
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
if (!configValuesEqual(prev[key], next[key]))
|
|
124
|
+
changed.push(key);
|
|
125
|
+
}
|
|
126
|
+
return changed;
|
|
127
|
+
}
|
|
128
|
+
export function applyUserConfig(config, _channel, options = {}) {
|
|
129
|
+
const overwrite = options.overwrite ?? true;
|
|
130
|
+
const clearMissing = options.clearMissing ?? true;
|
|
131
|
+
const notify = options.notify ?? true;
|
|
132
|
+
const managed = buildManagedEnv(config);
|
|
133
|
+
const changedKeys = [];
|
|
134
|
+
const prevConfig = activeUserConfig;
|
|
135
|
+
for (const key of MANAGED_ENV_KEYS) {
|
|
136
|
+
const next = managed[key];
|
|
137
|
+
const prev = process.env[key] ?? '';
|
|
138
|
+
if (!next) {
|
|
139
|
+
if (clearMissing && key in process.env) {
|
|
140
|
+
delete process.env[key];
|
|
141
|
+
changedKeys.push(key);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!overwrite && prev)
|
|
146
|
+
continue;
|
|
147
|
+
if (prev !== next) {
|
|
148
|
+
process.env[key] = next;
|
|
149
|
+
changedKeys.push(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
activeUserConfig = { ...config };
|
|
153
|
+
const configChangedKeys = diffConfigKeys(prevConfig, activeUserConfig);
|
|
154
|
+
const notifyKeys = [...new Set([...changedKeys, ...configChangedKeys])];
|
|
155
|
+
if (notify && notifyKeys.length)
|
|
156
|
+
notifyUserConfigListeners(activeUserConfig, notifyKeys);
|
|
157
|
+
return changedKeys;
|
|
158
|
+
}
|
|
159
|
+
export function setUserWorkdir(workdir, options = {}) {
|
|
160
|
+
const resolvedWorkdir = resolveUserWorkdir({ workdir });
|
|
161
|
+
const config = normalizeUserConfig({ ...loadUserConfig(), workdir: resolvedWorkdir });
|
|
162
|
+
const configPath = saveUserConfig(config);
|
|
163
|
+
applyUserConfig(config, undefined, { overwrite: true, clearMissing: true, notify: options.notify ?? true });
|
|
164
|
+
return { configPath, workdir: resolvedWorkdir, config };
|
|
165
|
+
}
|
|
166
|
+
export function startUserConfigSync(options = {}) {
|
|
167
|
+
const intervalMs = Math.max(250, Math.round(options.intervalMs ?? 1_000));
|
|
168
|
+
if (options.overrides)
|
|
169
|
+
userConfigSyncOverrides = { ...options.overrides };
|
|
170
|
+
const syncNow = () => {
|
|
171
|
+
const raw = readUserConfigRaw();
|
|
172
|
+
if (raw === userConfigSyncRaw && userConfigSyncTimer)
|
|
173
|
+
return;
|
|
174
|
+
userConfigSyncRaw = raw;
|
|
175
|
+
const merged = { ...loadUserConfig(), ...userConfigSyncOverrides };
|
|
176
|
+
const changedKeys = applyUserConfig(merged, undefined, { overwrite: true, clearMissing: true, notify: true });
|
|
177
|
+
if (changedKeys.length)
|
|
178
|
+
options.log?.(`config reloaded from setting.json (${changedKeys.join(', ')})`);
|
|
179
|
+
};
|
|
180
|
+
syncNow();
|
|
181
|
+
userConfigSyncRefCount++;
|
|
182
|
+
if (!userConfigSyncTimer) {
|
|
183
|
+
userConfigSyncTimer = setInterval(syncNow, intervalMs);
|
|
184
|
+
userConfigSyncTimer.unref?.();
|
|
185
|
+
}
|
|
186
|
+
return () => {
|
|
187
|
+
userConfigSyncRefCount = Math.max(0, userConfigSyncRefCount - 1);
|
|
188
|
+
if (userConfigSyncRefCount > 0 || !userConfigSyncTimer)
|
|
189
|
+
return;
|
|
190
|
+
clearInterval(userConfigSyncTimer);
|
|
191
|
+
userConfigSyncTimer = null;
|
|
192
|
+
userConfigSyncRaw = '';
|
|
193
|
+
userConfigSyncOverrides = {};
|
|
194
|
+
};
|
|
195
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pikiclaw",
|
|
3
|
+
"version": "0.2.35",
|
|
4
|
+
"description": "The best IM-driven remote coding experience. Bridge AI coding agents to any IM.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pikiclaw": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"telegram",
|
|
16
|
+
"ai",
|
|
17
|
+
"coding",
|
|
18
|
+
"claude",
|
|
19
|
+
"codex",
|
|
20
|
+
"bridge",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"author": "xiaotonng",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/xiaotonng/pikiclaw.git"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"dev": "npm run build:dashboard && tsx src/cli.ts",
|
|
31
|
+
"command": "set -ae && . ./.env && set +a && tsx src/run.ts",
|
|
32
|
+
"build:dashboard": "tsx scripts/build-dashboard.ts",
|
|
33
|
+
"build": "npm run build:dashboard && tsc",
|
|
34
|
+
"prepublishOnly": "npm run build",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:e2e": "set -ae && . ./.env && set +a && vitest run --config vitest.e2e.config.ts test/e2e/",
|
|
37
|
+
"test:watch": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
44
|
+
"@types/node": "^25.3.5",
|
|
45
|
+
"@types/react": "^19.2.14",
|
|
46
|
+
"@types/react-dom": "^19.2.3",
|
|
47
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
48
|
+
"react": "^19.2.4",
|
|
49
|
+
"react-dom": "^19.2.4",
|
|
50
|
+
"tailwindcss": "^4.2.1",
|
|
51
|
+
"tsx": "^4.21.0",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"vite-plugin-singlefile": "^2.3.0",
|
|
54
|
+
"vitest": "^4.0.18"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
58
|
+
"undici": "^7.22.0"
|
|
59
|
+
}
|
|
60
|
+
}
|