pikiloop 0.4.0
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 +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- package/package.json +82 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard API routes: configuration, channels, extensions, permissions.
|
|
3
|
+
*/
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
9
|
+
import { loadUserConfig, saveUserConfig, applyUserConfig, hasUserConfigFile } from '../../core/config/user-config.js';
|
|
10
|
+
import { expandTilde } from '../../core/platform.js';
|
|
11
|
+
import { readGitStatus } from '../../core/git.js';
|
|
12
|
+
import { isSetupReady } from '../../cli/onboarding.js';
|
|
13
|
+
import { validateDingtalkConfig, validateDiscordConfig, validateFeishuConfig, validateSlackConfig, validateTelegramConfig, validateWecomConfig, validateWeixinConfig, } from '../../core/config/validation.js';
|
|
14
|
+
import { resolveGuiIntegrationConfig } from '../../agent/mcp/bridge.js';
|
|
15
|
+
import { normalizeWeixinBaseUrl, startWeixinQrLogin, waitForWeixinQrLogin, } from '../../channels/weixin/api.js';
|
|
16
|
+
import { getConfiguredRemoteCdpUrl, getManagedBrowserStatus, launchManagedBrowserSetup, } from '../../browser-profile.js';
|
|
17
|
+
import { requestProcessRestart, getActiveTaskCount, } from '../../core/process-control.js';
|
|
18
|
+
import { getPermissionsStatus, getHostTerminalApp, isValidPermissionKey, requestPermission, } from '../platform.js';
|
|
19
|
+
import { VERSION } from '../../core/version.js';
|
|
20
|
+
import { runtime } from '../runtime.js';
|
|
21
|
+
import { writeScopedLog } from '../../core/logging.js';
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export async function buildBrowserStatusResponse(config = loadUserConfig(), browserState = getManagedBrowserStatus()) {
|
|
26
|
+
const gui = resolveGuiIntegrationConfig(config);
|
|
27
|
+
// In remote mode the local Chrome state is irrelevant — pikiloop attaches to
|
|
28
|
+
// the external CDP endpoint and never launches/manages a local browser. Report
|
|
29
|
+
// the endpoint so the dashboard shows the truth instead of "Chrome missing".
|
|
30
|
+
const remoteCdpUrl = gui.browserEnabled ? getConfiguredRemoteCdpUrl() : null;
|
|
31
|
+
return {
|
|
32
|
+
browser: {
|
|
33
|
+
status: gui.browserEnabled ? browserState.status : 'disabled',
|
|
34
|
+
enabled: gui.browserEnabled,
|
|
35
|
+
remoteCdpUrl,
|
|
36
|
+
headlessMode: gui.browserHeadless ? 'headless' : 'headed',
|
|
37
|
+
chromeInstalled: browserState.chromeInstalled,
|
|
38
|
+
profileCreated: browserState.profileCreated,
|
|
39
|
+
running: browserState.running,
|
|
40
|
+
pid: browserState.pid,
|
|
41
|
+
profileDir: browserState.profileDir || gui.browserProfileDir,
|
|
42
|
+
detail: !gui.browserEnabled
|
|
43
|
+
? 'Browser automation is disabled. No browser MCP server will be injected into agent sessions. On macOS, operate your main browser directly with open, osascript, and screencapture when needed.'
|
|
44
|
+
: remoteCdpUrl
|
|
45
|
+
? `Attached to an external Chrome over CDP at ${remoteCdpUrl} (PIKILOOP_BROWSER_CDP_URL). pikiloop does not launch or manage a local browser in this mode — sign in to sites from the Chrome that owns this endpoint (e.g. your sidecar's web VNC).`
|
|
46
|
+
: browserState.detail,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function isOpenTarget(value) {
|
|
51
|
+
return value === 'vscode'
|
|
52
|
+
|| value === 'cursor'
|
|
53
|
+
|| value === 'windsurf'
|
|
54
|
+
|| value === 'finder'
|
|
55
|
+
|| value === 'default';
|
|
56
|
+
}
|
|
57
|
+
function runOpenCommand(command, args) {
|
|
58
|
+
const result = spawnSync(command, args, {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
timeout: 5_000,
|
|
61
|
+
});
|
|
62
|
+
if (result.error)
|
|
63
|
+
throw result.error;
|
|
64
|
+
if ((result.status ?? 0) !== 0) {
|
|
65
|
+
const detail = String(result.stderr || result.stdout || '').trim();
|
|
66
|
+
throw new Error(detail || `Failed to run ${command} ${args.join(' ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function openPathWithTarget(filePath, target, isDirectory) {
|
|
70
|
+
if (process.platform === 'darwin') {
|
|
71
|
+
switch (target) {
|
|
72
|
+
case 'finder':
|
|
73
|
+
runOpenCommand('open', isDirectory ? [filePath] : ['-R', filePath]);
|
|
74
|
+
return;
|
|
75
|
+
case 'default':
|
|
76
|
+
runOpenCommand('open', [filePath]);
|
|
77
|
+
return;
|
|
78
|
+
case 'cursor':
|
|
79
|
+
runOpenCommand('open', ['-a', 'Cursor', filePath]);
|
|
80
|
+
return;
|
|
81
|
+
case 'windsurf':
|
|
82
|
+
runOpenCommand('open', ['-a', 'Windsurf', filePath]);
|
|
83
|
+
return;
|
|
84
|
+
case 'vscode':
|
|
85
|
+
default:
|
|
86
|
+
runOpenCommand('open', ['-a', 'Visual Studio Code', filePath]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
switch (target) {
|
|
92
|
+
case 'cursor':
|
|
93
|
+
runOpenCommand('cursor', [filePath]);
|
|
94
|
+
return;
|
|
95
|
+
case 'windsurf':
|
|
96
|
+
runOpenCommand('windsurf', [filePath]);
|
|
97
|
+
return;
|
|
98
|
+
case 'finder':
|
|
99
|
+
case 'default':
|
|
100
|
+
runOpenCommand('cmd', ['/c', 'start', '', filePath]);
|
|
101
|
+
return;
|
|
102
|
+
case 'vscode':
|
|
103
|
+
default:
|
|
104
|
+
runOpenCommand('code', [filePath]);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
switch (target) {
|
|
109
|
+
case 'cursor':
|
|
110
|
+
runOpenCommand('cursor', [filePath]);
|
|
111
|
+
return;
|
|
112
|
+
case 'windsurf':
|
|
113
|
+
runOpenCommand('windsurf', [filePath]);
|
|
114
|
+
return;
|
|
115
|
+
case 'finder':
|
|
116
|
+
case 'default':
|
|
117
|
+
runOpenCommand('xdg-open', [filePath]);
|
|
118
|
+
return;
|
|
119
|
+
case 'vscode':
|
|
120
|
+
default:
|
|
121
|
+
runOpenCommand('code', [filePath]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Routes
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
const app = new Hono();
|
|
129
|
+
// Full state (config from file only)
|
|
130
|
+
app.get('/api/state', async (c) => {
|
|
131
|
+
const config = loadUserConfig();
|
|
132
|
+
const setupState = await runtime.buildValidatedSetupState(config);
|
|
133
|
+
const permissions = getPermissionsStatus();
|
|
134
|
+
const botRef = runtime.getBotRef();
|
|
135
|
+
return c.json({
|
|
136
|
+
version: VERSION,
|
|
137
|
+
ready: isSetupReady(setupState),
|
|
138
|
+
configExists: hasUserConfigFile(),
|
|
139
|
+
config,
|
|
140
|
+
runtimeWorkdir: runtime.getRuntimeWorkdir(config),
|
|
141
|
+
setupState,
|
|
142
|
+
permissions,
|
|
143
|
+
hostApp: getHostTerminalApp(),
|
|
144
|
+
platform: process.platform,
|
|
145
|
+
pid: process.pid,
|
|
146
|
+
nodeVersion: process.versions.node,
|
|
147
|
+
bot: botRef ? {
|
|
148
|
+
workdir: botRef.workdir,
|
|
149
|
+
defaultAgent: botRef.defaultAgent,
|
|
150
|
+
uptime: Date.now() - botRef.startedAt,
|
|
151
|
+
connected: botRef.connected,
|
|
152
|
+
stats: botRef.stats,
|
|
153
|
+
activeTasks: botRef.activeTasks.size,
|
|
154
|
+
sessions: botRef.sessionStates.size,
|
|
155
|
+
} : null,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// Host info
|
|
159
|
+
app.get('/api/host', (c) => {
|
|
160
|
+
const botRef = runtime.getBotRef();
|
|
161
|
+
if (botRef)
|
|
162
|
+
return c.json(botRef.getHostData());
|
|
163
|
+
const cpus = os.cpus();
|
|
164
|
+
const [one, five, fifteen] = os.loadavg();
|
|
165
|
+
return c.json({
|
|
166
|
+
hostName: os.hostname(), cpuModel: cpus[0]?.model || 'unknown',
|
|
167
|
+
cpuCount: cpus.length, totalMem: os.totalmem(), freeMem: os.freemem(),
|
|
168
|
+
loadAverage: { one, five, fifteen },
|
|
169
|
+
platform: process.platform, arch: os.arch(),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// Permissions
|
|
173
|
+
app.get('/api/permissions', (c) => {
|
|
174
|
+
const data = { ...getPermissionsStatus(), hostApp: getHostTerminalApp() };
|
|
175
|
+
return c.json(data);
|
|
176
|
+
});
|
|
177
|
+
// Save config (to ~/.pikiloop/setting.json). Channel reconciliation is
|
|
178
|
+
// handled by ChannelSupervisor via the onUserConfigChange listener — adding,
|
|
179
|
+
// removing, or swapping credentials of an IM channel takes effect in-process
|
|
180
|
+
// without restarting pikiloop.
|
|
181
|
+
app.post('/api/config', async (c) => {
|
|
182
|
+
const body = await c.req.json();
|
|
183
|
+
const merged = { ...loadUserConfig(), ...body };
|
|
184
|
+
const configPath = saveUserConfig(merged);
|
|
185
|
+
applyUserConfig(loadUserConfig());
|
|
186
|
+
return c.json({ ok: true, configPath });
|
|
187
|
+
});
|
|
188
|
+
// Validate Telegram token
|
|
189
|
+
app.post('/api/validate-telegram-token', async (c) => {
|
|
190
|
+
const body = await c.req.json();
|
|
191
|
+
const result = await validateTelegramConfig(body.token || '', body.allowedChatIds || '');
|
|
192
|
+
return c.json({
|
|
193
|
+
ok: result.state.ready,
|
|
194
|
+
error: result.state.ready ? null : result.state.detail,
|
|
195
|
+
bot: result.bot,
|
|
196
|
+
normalizedAllowedChatIds: result.normalizedAllowedChatIds,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
// Validate Feishu credentials
|
|
200
|
+
app.post('/api/validate-feishu-config', async (c) => {
|
|
201
|
+
const body = await c.req.json();
|
|
202
|
+
const startedAt = Date.now();
|
|
203
|
+
const rawAppId = String(body.appId || '').trim();
|
|
204
|
+
const maskedAppId = !rawAppId
|
|
205
|
+
? '(missing)'
|
|
206
|
+
: rawAppId.length <= 10
|
|
207
|
+
? rawAppId
|
|
208
|
+
: `${rawAppId.slice(0, 6)}...${rawAppId.slice(-4)}`;
|
|
209
|
+
writeScopedLog('dashboard', `[feishu-config] request app=${maskedAppId}`, { level: 'debug' });
|
|
210
|
+
const result = await validateFeishuConfig(body.appId || '', body.appSecret || '');
|
|
211
|
+
writeScopedLog('dashboard', `[feishu-config] result app=${maskedAppId} ok=${result.state.ready} status=${result.state.status} elapsedMs=${Date.now() - startedAt}`, { level: 'debug' });
|
|
212
|
+
return c.json({
|
|
213
|
+
ok: result.state.ready,
|
|
214
|
+
error: result.state.ready ? null : result.state.detail,
|
|
215
|
+
app: result.app,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// Validate Weixin credentials
|
|
219
|
+
app.post('/api/validate-weixin-config', async (c) => {
|
|
220
|
+
const body = await c.req.json();
|
|
221
|
+
const result = await validateWeixinConfig(body.baseUrl || '', body.botToken || '', body.accountId || '');
|
|
222
|
+
return c.json({
|
|
223
|
+
ok: result.state.ready,
|
|
224
|
+
error: result.state.ready ? null : result.state.detail,
|
|
225
|
+
account: result.account,
|
|
226
|
+
normalizedBaseUrl: result.normalizedBaseUrl,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// Validate Slack credentials
|
|
230
|
+
app.post('/api/validate-slack-config', async (c) => {
|
|
231
|
+
const body = await c.req.json();
|
|
232
|
+
const result = await validateSlackConfig(body.botToken || '', body.appToken || '');
|
|
233
|
+
return c.json({
|
|
234
|
+
ok: result.state.ready,
|
|
235
|
+
error: result.state.ready ? null : result.state.detail,
|
|
236
|
+
bot: result.bot,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// Validate Discord credentials
|
|
240
|
+
app.post('/api/validate-discord-config', async (c) => {
|
|
241
|
+
const body = await c.req.json();
|
|
242
|
+
const result = await validateDiscordConfig(body.botToken || '');
|
|
243
|
+
return c.json({
|
|
244
|
+
ok: result.state.ready,
|
|
245
|
+
error: result.state.ready ? null : result.state.detail,
|
|
246
|
+
bot: result.bot,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// Validate DingTalk credentials
|
|
250
|
+
app.post('/api/validate-dingtalk-config', async (c) => {
|
|
251
|
+
const body = await c.req.json();
|
|
252
|
+
const result = await validateDingtalkConfig(body.clientId || '', body.clientSecret || '');
|
|
253
|
+
return c.json({
|
|
254
|
+
ok: result.state.ready,
|
|
255
|
+
error: result.state.ready ? null : result.state.detail,
|
|
256
|
+
app: result.app,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
// Validate WeChat Work (企业微信) credentials
|
|
260
|
+
app.post('/api/validate-wecom-config', async (c) => {
|
|
261
|
+
const body = await c.req.json();
|
|
262
|
+
const result = await validateWecomConfig(body.botId || '', body.botSecret || '');
|
|
263
|
+
return c.json({
|
|
264
|
+
ok: result.state.ready,
|
|
265
|
+
error: result.state.ready ? null : result.state.detail,
|
|
266
|
+
bot: result.bot,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
// Start Weixin QR login
|
|
270
|
+
app.post('/api/weixin-login/start', async (c) => {
|
|
271
|
+
const body = await c.req.json();
|
|
272
|
+
const result = await startWeixinQrLogin({
|
|
273
|
+
baseUrl: normalizeWeixinBaseUrl(body.baseUrl || ''),
|
|
274
|
+
sessionKey: body.sessionKey || undefined,
|
|
275
|
+
});
|
|
276
|
+
return c.json(result, result.ok ? 200 : 500);
|
|
277
|
+
});
|
|
278
|
+
// Wait for Weixin QR login
|
|
279
|
+
app.post('/api/weixin-login/wait', async (c) => {
|
|
280
|
+
const body = await c.req.json();
|
|
281
|
+
const result = await waitForWeixinQrLogin({
|
|
282
|
+
baseUrl: normalizeWeixinBaseUrl(body.baseUrl || ''),
|
|
283
|
+
sessionKey: String(body.sessionKey || '').trim(),
|
|
284
|
+
});
|
|
285
|
+
return c.json(result, result.ok ? 200 : 500);
|
|
286
|
+
});
|
|
287
|
+
// Open macOS preferences
|
|
288
|
+
app.post('/api/open-preferences', async (c) => {
|
|
289
|
+
const body = await c.req.json();
|
|
290
|
+
const permission = String(body.permission || '');
|
|
291
|
+
if (!isValidPermissionKey(permission)) {
|
|
292
|
+
return c.json({
|
|
293
|
+
ok: false,
|
|
294
|
+
action: 'unsupported',
|
|
295
|
+
granted: false,
|
|
296
|
+
requiresManualGrant: false,
|
|
297
|
+
error: 'Invalid permission.',
|
|
298
|
+
}, 400);
|
|
299
|
+
}
|
|
300
|
+
const result = requestPermission(permission);
|
|
301
|
+
runtime.log(`[permissions] permission=${permission} action=${result.action} granted=${result.granted} manual=${result.requiresManualGrant} ok=${result.ok}`);
|
|
302
|
+
return c.json(result, result.ok ? 200 : 500);
|
|
303
|
+
});
|
|
304
|
+
// Restart process
|
|
305
|
+
app.post('/api/restart', (c) => {
|
|
306
|
+
// A successful restart exits this process, so its result can never reach the
|
|
307
|
+
// client — the route fires it async and returns ok:true. But the restart is
|
|
308
|
+
// REFUSED while any turn is still running (requestProcessRestart guards on
|
|
309
|
+
// active tasks so it won't kill in-flight work). Surface that refusal
|
|
310
|
+
// synchronously: otherwise the dashboard sends ok:true into a "reconnecting"
|
|
311
|
+
// wait for a restart that never happens and shows a generic failure, with no
|
|
312
|
+
// hint that a running task is the reason.
|
|
313
|
+
const activeTasks = getActiveTaskCount();
|
|
314
|
+
if (activeTasks > 0) {
|
|
315
|
+
return c.json({
|
|
316
|
+
ok: false,
|
|
317
|
+
activeTasks,
|
|
318
|
+
error: `${activeTasks} task(s) still running — can't restart. Wait for them to finish or stop them, then retry.`,
|
|
319
|
+
}, 409);
|
|
320
|
+
}
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
void requestProcessRestart({ log: message => runtime.log(message) });
|
|
323
|
+
}, 50);
|
|
324
|
+
return c.json({ ok: true });
|
|
325
|
+
});
|
|
326
|
+
// Switch workdir
|
|
327
|
+
app.post('/api/switch-workdir', async (c) => {
|
|
328
|
+
const body = await c.req.json();
|
|
329
|
+
const newPath = body.path;
|
|
330
|
+
if (!newPath)
|
|
331
|
+
return c.json({ ok: false, error: 'Missing path' }, 400);
|
|
332
|
+
const resolvedPath = path.resolve(expandTilde(String(newPath)));
|
|
333
|
+
const botRef = runtime.getBotRef();
|
|
334
|
+
if (botRef) {
|
|
335
|
+
botRef.switchWorkdir(resolvedPath);
|
|
336
|
+
return c.json({ ok: true, workdir: botRef.workdir });
|
|
337
|
+
}
|
|
338
|
+
const { setUserWorkdir } = await import('../../core/config/user-config.js');
|
|
339
|
+
const saved = setUserWorkdir(resolvedPath);
|
|
340
|
+
return c.json({ ok: true, workdir: saved.workdir });
|
|
341
|
+
});
|
|
342
|
+
// Browser profile status
|
|
343
|
+
app.get('/api/browser', async (c) => {
|
|
344
|
+
const config = loadUserConfig();
|
|
345
|
+
const data = await buildBrowserStatusResponse(config);
|
|
346
|
+
return c.json(data);
|
|
347
|
+
});
|
|
348
|
+
// Launch managed browser profile for login/setup
|
|
349
|
+
app.post('/api/browser/setup', async (c) => {
|
|
350
|
+
runtime.log('[browser] setup requested');
|
|
351
|
+
try {
|
|
352
|
+
const config = loadUserConfig();
|
|
353
|
+
const gui = resolveGuiIntegrationConfig(config);
|
|
354
|
+
if (!gui.browserEnabled) {
|
|
355
|
+
return c.json({
|
|
356
|
+
ok: false,
|
|
357
|
+
error: 'Browser automation is disabled. Enable it first if you want pikiloop to launch the managed browser profile.',
|
|
358
|
+
}, 400);
|
|
359
|
+
}
|
|
360
|
+
// Remote mode: pikiloop attaches to an external CDP endpoint and owns no
|
|
361
|
+
// local Chrome. There is nothing to launch — return the (enabled) status as
|
|
362
|
+
// a clean no-op instead of failing with "Chrome is not available".
|
|
363
|
+
if (getConfiguredRemoteCdpUrl()) {
|
|
364
|
+
runtime.log('[browser] setup skipped: PIKILOOP_BROWSER_CDP_URL configured (external CDP, no local browser to launch)');
|
|
365
|
+
return c.json({ ok: true, ...(await buildBrowserStatusResponse(config)) });
|
|
366
|
+
}
|
|
367
|
+
const launch = launchManagedBrowserSetup();
|
|
368
|
+
runtime.log(`[browser] launched managed profile at ${launch.profileDir} pid=${launch.pid ?? 'unknown'}`);
|
|
369
|
+
const payload = await buildBrowserStatusResponse(config, launch);
|
|
370
|
+
return c.json({
|
|
371
|
+
ok: true,
|
|
372
|
+
browser: {
|
|
373
|
+
...payload.browser,
|
|
374
|
+
detail: launch.running
|
|
375
|
+
? 'Managed browser is open. Sign in to the sites you want pikiloop to reuse. If it is still open later, pikiloop will close it automatically before browser automation starts.'
|
|
376
|
+
: payload.browser.detail,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
382
|
+
runtime.log(`[browser] setup failed: ${detail}`);
|
|
383
|
+
return c.json({ ok: false, error: detail }, 500);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
// List directory entries for tree browser
|
|
387
|
+
app.get('/api/ls-dir', (c) => {
|
|
388
|
+
const dir = c.req.query('path') || os.homedir();
|
|
389
|
+
const includeFiles = c.req.query('files') === '1';
|
|
390
|
+
const includeHidden = c.req.query('hidden') === '1';
|
|
391
|
+
try {
|
|
392
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
393
|
+
const dirs = entries
|
|
394
|
+
.filter(e => (includeHidden || !e.name.startsWith('.')) && (includeFiles || e.isDirectory()))
|
|
395
|
+
.map(e => ({ name: e.name, path: path.join(dir, e.name), isDir: e.isDirectory() }))
|
|
396
|
+
.sort((a, b) => {
|
|
397
|
+
if (a.isDir !== b.isDir)
|
|
398
|
+
return a.isDir ? -1 : 1;
|
|
399
|
+
return a.name.localeCompare(b.name);
|
|
400
|
+
});
|
|
401
|
+
const isGit = fs.existsSync(path.join(dir, '.git'));
|
|
402
|
+
return c.json({ ok: true, path: dir, parent: path.dirname(dir), dirs, isGit });
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 400);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
// Git changes for a directory (uncommitted + staged)
|
|
409
|
+
app.get('/api/git-changes', (c) => {
|
|
410
|
+
const dir = c.req.query('path');
|
|
411
|
+
if (!dir)
|
|
412
|
+
return c.json({ ok: false, error: 'path is required' }, 400);
|
|
413
|
+
try {
|
|
414
|
+
if (!fs.existsSync(path.join(dir, '.git'))) {
|
|
415
|
+
return c.json({ ok: true, changes: [], isGit: false });
|
|
416
|
+
}
|
|
417
|
+
// --no-optional-locks avoids contention with other git processes
|
|
418
|
+
const result = spawnSync('git', ['diff', '--name-status', 'HEAD', '--no-renames'], {
|
|
419
|
+
cwd: dir,
|
|
420
|
+
timeout: 5_000,
|
|
421
|
+
encoding: 'utf-8',
|
|
422
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
423
|
+
});
|
|
424
|
+
const lines = (result.stdout || '').trim().split('\n').filter(Boolean);
|
|
425
|
+
const changes = lines.map(line => {
|
|
426
|
+
const [status, ...rest] = line.split('\t');
|
|
427
|
+
const file = rest.join('\t');
|
|
428
|
+
return {
|
|
429
|
+
status: status === 'A' ? 'added'
|
|
430
|
+
: status === 'D' ? 'deleted'
|
|
431
|
+
: 'modified',
|
|
432
|
+
file,
|
|
433
|
+
path: path.join(dir, file),
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
return c.json({ ok: true, changes, isGit: true });
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
// Git status summary for a workspace: branch, ahead/behind, dirty count.
|
|
443
|
+
// Backed by the same readGitStatus helper that feeds the IM /status command.
|
|
444
|
+
app.get('/api/workspace-git', (c) => {
|
|
445
|
+
const dir = c.req.query('path');
|
|
446
|
+
if (!dir)
|
|
447
|
+
return c.json({ ok: false, error: 'path is required' }, 400);
|
|
448
|
+
const git = readGitStatus(dir);
|
|
449
|
+
return c.json({ ok: true, isGit: git !== null, git });
|
|
450
|
+
});
|
|
451
|
+
// Open file/directory in a selected editor or file browser
|
|
452
|
+
app.post('/api/open-in-editor', async (c) => {
|
|
453
|
+
try {
|
|
454
|
+
const body = await c.req.json();
|
|
455
|
+
const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
|
|
456
|
+
const target = isOpenTarget(body?.target) ? body.target : 'vscode';
|
|
457
|
+
if (!filePath)
|
|
458
|
+
return c.json({ ok: false, error: 'filePath is required' }, 400);
|
|
459
|
+
if (!fs.existsSync(filePath))
|
|
460
|
+
return c.json({ ok: false, error: 'Path not found' }, 404);
|
|
461
|
+
const stat = fs.statSync(filePath);
|
|
462
|
+
openPathWithTarget(filePath, target, stat.isDirectory());
|
|
463
|
+
return c.json({ ok: true });
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
467
|
+
runtime.log(`[open-in-editor] failed: ${detail}`);
|
|
468
|
+
return c.json({ ok: false, error: detail }, 500);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
// Open git diff for a file in the selected editor
|
|
472
|
+
app.post('/api/open-diff', async (c) => {
|
|
473
|
+
try {
|
|
474
|
+
const body = await c.req.json();
|
|
475
|
+
const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
|
|
476
|
+
const target = isOpenTarget(body?.target) ? body.target : 'vscode';
|
|
477
|
+
if (!filePath)
|
|
478
|
+
return c.json({ ok: false, error: 'filePath is required' }, 400);
|
|
479
|
+
const dir = path.dirname(filePath);
|
|
480
|
+
const relFile = path.basename(filePath);
|
|
481
|
+
// Write the original (HEAD) version to a temp file
|
|
482
|
+
const origResult = spawnSync('git', ['show', `HEAD:${path.relative(findGitRoot(dir), filePath)}`], {
|
|
483
|
+
cwd: dir,
|
|
484
|
+
timeout: 5_000,
|
|
485
|
+
encoding: 'buffer',
|
|
486
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
487
|
+
});
|
|
488
|
+
if (origResult.status !== 0) {
|
|
489
|
+
// New file — no HEAD version, just open the file
|
|
490
|
+
openPathWithTarget(filePath, target, false);
|
|
491
|
+
return c.json({ ok: true });
|
|
492
|
+
}
|
|
493
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pikiloop-diff-'));
|
|
494
|
+
const origPath = path.join(tmpDir, `${relFile}.orig`);
|
|
495
|
+
fs.writeFileSync(origPath, origResult.stdout);
|
|
496
|
+
// Use editor CLI diff command (fire-and-forget)
|
|
497
|
+
const cli = target === 'cursor' ? 'cursor' : target === 'windsurf' ? 'windsurf' : 'code';
|
|
498
|
+
const child = spawn(cli, ['--diff', origPath, filePath], {
|
|
499
|
+
cwd: dir,
|
|
500
|
+
stdio: 'ignore',
|
|
501
|
+
detached: true,
|
|
502
|
+
});
|
|
503
|
+
child.unref();
|
|
504
|
+
// Clean up temp after a delay
|
|
505
|
+
setTimeout(() => fs.promises.rm(tmpDir, { recursive: true, force: true }).catch(() => { }), 30_000);
|
|
506
|
+
return c.json({ ok: true });
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
510
|
+
runtime.log(`[open-diff] failed: ${detail}`);
|
|
511
|
+
return c.json({ ok: false, error: detail }, 500);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
function findGitRoot(dir) {
|
|
515
|
+
let current = dir;
|
|
516
|
+
while (current !== path.dirname(current)) {
|
|
517
|
+
if (fs.existsSync(path.join(current, '.git')))
|
|
518
|
+
return current;
|
|
519
|
+
current = path.dirname(current);
|
|
520
|
+
}
|
|
521
|
+
return dir;
|
|
522
|
+
}
|
|
523
|
+
export default app;
|