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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * constants.ts — Centralized timeout, retry, and numeric constants.
3
+ *
4
+ * Grouped by domain / module so each subsystem can import only the
5
+ * bucket it needs.
6
+ */
7
+ import path from 'node:path';
8
+ // ---------------------------------------------------------------------------
9
+ // MCP bridge
10
+ // ---------------------------------------------------------------------------
11
+ /** Timeouts for the per-stream MCP callback server and tool operations. */
12
+ export const MCP_TIMEOUTS = {
13
+ /** Max time to wait for the sendFile callback to complete. */
14
+ sendFile: 60_000,
15
+ /** Max time to receive the HTTP request body on the callback server. */
16
+ requestBody: 10_000,
17
+ /** Server-level: max time for an entire request lifecycle. */
18
+ serverRequest: 90_000,
19
+ /** Server-level: max time to receive request headers. */
20
+ serverHeaders: 10_000,
21
+ /** Timeout for `codex mcp add` registration commands. */
22
+ codexMcpAdd: 10_000,
23
+ /** Timeout for `codex mcp remove` cleanup commands. */
24
+ codexMcpRemove: 5_000,
25
+ };
26
+ /** Maximum artifact file size the MCP bridge will accept (20 MB). */
27
+ export const MCP_ARTIFACT_MAX_BYTES = 20 * 1024 * 1024;
28
+ // ---------------------------------------------------------------------------
29
+ // Dashboard
30
+ // ---------------------------------------------------------------------------
31
+ /** Timeouts used by the dashboard HTTP server and its status endpoints. */
32
+ export const DASHBOARD_TIMEOUTS = {
33
+ /** Timeout for agent model discovery (Codex cold-start can be slow). */
34
+ agentStatusModels: 4_000,
35
+ /** Timeout for agent usage data fetch. */
36
+ agentStatusUsage: 1_500,
37
+ /** Timeout for channel credential validation requests. */
38
+ channelStatusValidation: 3_000,
39
+ /** How long validated channel states are cached before re-checking. */
40
+ channelStatusCacheTtl: 20_000,
41
+ /** How long the full agent-status response is cached (SWR). */
42
+ agentStatusCacheTtl: 30_000,
43
+ /** Timeout for agent npm install via the dashboard. */
44
+ agentInstall: 10 * 60_000,
45
+ /** Default timeout for dashboard-spawned shell commands. */
46
+ runCommand: 30_000,
47
+ };
48
+ // ---------------------------------------------------------------------------
49
+ // Browser automation
50
+ // ---------------------------------------------------------------------------
51
+ // ---------------------------------------------------------------------------
52
+ // Brand / state-dir / env identifiers — single source of truth
53
+ // ---------------------------------------------------------------------------
54
+ /** Home state directory name: `~/.pikiloop`. */
55
+ export const STATE_DIR_NAME = '.pikiloop';
56
+ /** Env var prefix for all pikiloop-specific variables. */
57
+ export const ENV_PREFIX = 'PIKILOOP_';
58
+ /**
59
+ * Pre-rename (pikiclaw) identifiers. Kept ONLY for the one-time state-dir
60
+ * migration, the env-var fallback (see core/legacy-compat.ts) and project-skill
61
+ * discovery, so installs created before the rename are never orphaned. Safe to
62
+ * delete a couple of releases after the rename has propagated.
63
+ */
64
+ export const LEGACY_STATE_DIR_NAME = '.pikiclaw';
65
+ export const LEGACY_ENV_PREFIX = 'PIKICLAW_';
66
+ /**
67
+ * Stable relative path for the managed Chrome profile under the home directory.
68
+ * Keep this outside config-specific directories so `npm run dev` and the main
69
+ * runtime share the same browser login state.
70
+ */
71
+ export const MANAGED_BROWSER_PROFILE_SUBPATH = path.join(STATE_DIR_NAME, 'browser', 'chrome-profile');
72
+ /** Base Playwright MCP args for the managed browser integration. */
73
+ export const PLAYWRIGHT_MCP_PACKAGE_NAME = '@playwright/mcp';
74
+ export const PLAYWRIGHT_MCP_PACKAGE_VERSION = '0.0.75';
75
+ export const PLAYWRIGHT_MCP_PACKAGE_SPEC = `${PLAYWRIGHT_MCP_PACKAGE_NAME}@${PLAYWRIGHT_MCP_PACKAGE_VERSION}`;
76
+ export const PLAYWRIGHT_MCP_BROWSER_ARGS = ['--browser', 'chrome', '--viewport-size', '1920x1080'];
77
+ /**
78
+ * Env var name for pointing pikiloop at an external Chrome DevTools Protocol
79
+ * endpoint (e.g. `http://chromium:9222`) instead of launching a local Chrome.
80
+ * Primary use cases: Docker deployments that run a sidecar like
81
+ * `lscr.io/linuxserver/chromium`, or attaching to a remote browser the user
82
+ * already manages. When set, browser-supervisor skips every local-launch
83
+ * codepath (no Chrome detection, no pid SIGKILL on restart) and pipes the URL
84
+ * through to Playwright MCP's `--cdp-endpoint`.
85
+ */
86
+ export const PIKILOOP_BROWSER_CDP_URL_ENV = 'PIKILOOP_BROWSER_CDP_URL';
87
+ /** Dashboard session pagination limits. */
88
+ export const DASHBOARD_PAGINATION = {
89
+ defaultPageSize: 6,
90
+ maxPageSize: 30,
91
+ };
92
+ /** Timeouts for macOS permission checks and JXA scripts (dashboard). */
93
+ export const DASHBOARD_PERMISSION_TIMEOUTS = {
94
+ /** Default timeout for osascript / JXA calls. */
95
+ jxaDefault: 5_000,
96
+ /** Timeout for screencapture permission probe. */
97
+ screenRecordingProbe: 5_000,
98
+ /** Timeout for CGPreflight screen capture check. */
99
+ screenRecordingPreflight: 4_000,
100
+ /** Timeout for CGRequest screen capture request. */
101
+ screenRecordingRequest: 6_000,
102
+ /** Timeout for `open` command to launch System Preferences. */
103
+ openSystemPreferences: 3_000,
104
+ /** Timeout for parent process tree detection. */
105
+ detectTerminal: 3_000,
106
+ };
107
+ /**
108
+ * TTL for cached dashboard permission / host-terminal probes. `/api/state` is
109
+ * polled (~1.5s while a channel validates) and each probe spawns subprocesses
110
+ * (screencapture, an `ls` shell, a `ps` process-tree walk), so the raw checks
111
+ * must not run per request. The host terminal is immutable per process;
112
+ * permission grants change rarely and `requestPermission` invalidates the cache
113
+ * on user action, so a short TTL surfaces grants without per-request spawns.
114
+ */
115
+ export const DASHBOARD_PERMISSION_CACHE_TTL_MS = 30_000;
116
+ // ---------------------------------------------------------------------------
117
+ // CLI / Daemon
118
+ // ---------------------------------------------------------------------------
119
+ /** Daemon (watchdog) restart timing constants. */
120
+ export const DAEMON_TIMEOUTS = {
121
+ /** Initial delay before restarting a crashed child. */
122
+ restartDelay: 3_000,
123
+ /** Maximum back-off delay for repeated rapid crashes. */
124
+ maxRestartDelay: 60_000,
125
+ /** If the child runs shorter than this, treat it as a rapid crash. */
126
+ rapidCrashWindow: 10_000,
127
+ /** Polling interval while waiting for dashboard config to become ready. */
128
+ configPollInterval: 1_000,
129
+ };
130
+ // ---------------------------------------------------------------------------
131
+ // Bot orchestration / shutdown
132
+ // ---------------------------------------------------------------------------
133
+ /** Time to wait before force-exiting during bot shutdown. */
134
+ export const BOT_SHUTDOWN_FORCE_EXIT_MS = 3_000;
135
+ // ---------------------------------------------------------------------------
136
+ // Bot runtime
137
+ // ---------------------------------------------------------------------------
138
+ /** Bot-level timing constants. */
139
+ export const BOT_TIMEOUTS = {
140
+ /** Default run timeout for agent streams (seconds). */
141
+ defaultRunTimeoutS: 7200,
142
+ /** Interval for macOS user-activity caffeinate pulses. */
143
+ macosUserActivityPulseInterval: 20_000,
144
+ /** Timeout (seconds) for the caffeinate assertion per pulse. */
145
+ macosUserActivityPulseTimeoutS: 30,
146
+ };
147
+ // ---------------------------------------------------------------------------
148
+ // Live preview (stream feedback)
149
+ // ---------------------------------------------------------------------------
150
+ /** Timing constants for the channel-agnostic live preview controller. */
151
+ export const STREAM_PREVIEW_TIMEOUTS = {
152
+ /** Interval between heartbeat edits that refresh the elapsed timer. */
153
+ heartbeat: 5_000,
154
+ /** Interval between typing indicator pulses. */
155
+ typing: 4_000,
156
+ /** After this idle time, a "stalled" notice is shown. */
157
+ stalledNotice: 15_000,
158
+ };
159
+ // ---------------------------------------------------------------------------
160
+ // Channels — Telegram
161
+ // ---------------------------------------------------------------------------
162
+ /** Telegram channel transport constants. */
163
+ export const TELEGRAM_LIMITS = {
164
+ /** Maximum text length per Telegram message. */
165
+ maxMessageLength: 4096,
166
+ /** Maximum file size for send/receive (20 MB). */
167
+ fileMaxBytes: 20 * 1024 * 1024,
168
+ /** Maximum back-off delay for polling/connect retries. */
169
+ maxRetryDelay: 60_000,
170
+ };
171
+ // ---------------------------------------------------------------------------
172
+ // Channels — Feishu
173
+ // ---------------------------------------------------------------------------
174
+ /** Feishu channel transport constants. */
175
+ export const FEISHU_LIMITS = {
176
+ /** Card markdown budget (card JSON limit ~30 KB). */
177
+ cardMax: 28_000,
178
+ /** Maximum file size for send/receive (20 MB). */
179
+ fileMaxBytes: 20 * 1024 * 1024,
180
+ /** Maximum back-off delay for WebSocket reconnection retries. */
181
+ wsStartRetryMaxDelay: 60_000,
182
+ /** Initial retry delay for Feishu WebSocket connection. */
183
+ wsStartRetryInitialDelay: 3_000,
184
+ };
185
+ /** Feishu bot rendering limit for card payloads. */
186
+ export const FEISHU_BOT_CARD_MAX = 25_000;
187
+ // ---------------------------------------------------------------------------
188
+ // Channels — Weixin
189
+ // ---------------------------------------------------------------------------
190
+ /** Weixin channel transport constants. */
191
+ export const WEIXIN_LIMITS = {
192
+ /** Conservative text split budget for plain-text replies. */
193
+ maxMessageLength: 1200,
194
+ /** Long-poll timeout for getupdates. */
195
+ longPollTimeout: 35_000,
196
+ /** Maximum back-off delay for polling retries. */
197
+ maxRetryDelay: 60_000,
198
+ };
199
+ // ---------------------------------------------------------------------------
200
+ // Channels — Slack
201
+ // ---------------------------------------------------------------------------
202
+ /** Slack channel transport constants. */
203
+ export const SLACK_LIMITS = {
204
+ /** Slack chat.postMessage hard limit is 40000; cap at 35k for safety. */
205
+ maxMessageLength: 35_000,
206
+ /** Slack file upload size cap (1 GB); we keep parity with other channels at 20 MB. */
207
+ fileMaxBytes: 20 * 1024 * 1024,
208
+ /** Maximum back-off delay for socket-mode reconnect retries. */
209
+ maxRetryDelay: 60_000,
210
+ /** Initial back-off delay for socket-mode reconnect. */
211
+ initialRetryDelay: 3_000,
212
+ };
213
+ // ---------------------------------------------------------------------------
214
+ // Channels — Discord
215
+ // ---------------------------------------------------------------------------
216
+ /** Discord channel transport constants. */
217
+ export const DISCORD_LIMITS = {
218
+ /** Hard message cap is 2000 chars; leave a small buffer. */
219
+ maxMessageLength: 1900,
220
+ /** File size cap for non-Nitro guilds is 25 MB; we keep parity at 20 MB. */
221
+ fileMaxBytes: 20 * 1024 * 1024,
222
+ /** Maximum back-off delay for gateway reconnect retries. */
223
+ maxRetryDelay: 60_000,
224
+ /** Initial back-off delay for gateway reconnect. */
225
+ initialRetryDelay: 3_000,
226
+ };
227
+ // ---------------------------------------------------------------------------
228
+ // Channels — DingTalk
229
+ // ---------------------------------------------------------------------------
230
+ /** DingTalk channel transport constants. */
231
+ export const DINGTALK_LIMITS = {
232
+ /** Conservative text limit per message (markdown segments split here). */
233
+ maxMessageLength: 5_000,
234
+ /** Maximum back-off delay for stream reconnect retries. */
235
+ maxRetryDelay: 60_000,
236
+ /** Initial back-off delay for stream reconnect. */
237
+ initialRetryDelay: 3_000,
238
+ };
239
+ // ---------------------------------------------------------------------------
240
+ // Channels — WeChat Work (企业微信 智能机器人)
241
+ // ---------------------------------------------------------------------------
242
+ /** WeChat Work Smart Bot WebSocket transport constants. */
243
+ export const WECOM_LIMITS = {
244
+ /** Smart Bot text message hard cap is roughly 5KB chars. */
245
+ maxMessageLength: 4_000,
246
+ /** Heartbeat interval to keep the websocket alive. */
247
+ heartbeatInterval: 30_000,
248
+ /** Maximum back-off delay for websocket reconnect retries. */
249
+ maxRetryDelay: 60_000,
250
+ /** Initial back-off delay for websocket reconnect. */
251
+ initialRetryDelay: 1_000,
252
+ /** Default smart bot websocket endpoint. */
253
+ defaultEndpoint: 'wss://openws.work.weixin.qq.com/wssvr/',
254
+ };
255
+ // ---------------------------------------------------------------------------
256
+ // Config validation
257
+ // ---------------------------------------------------------------------------
258
+ /** Timeouts for channel credential validation flows. */
259
+ export const VALIDATION_TIMEOUTS = {
260
+ /** Default timeout for Feishu credential validation. */
261
+ feishuDefault: 15_000,
262
+ /** Timeout for fetching Feishu bot info after credential validation. */
263
+ feishuBotInfo: 5_000,
264
+ /** Timeout for Telegram token validation (setup wizard). */
265
+ telegramToken: 8_000,
266
+ /** Default timeout for Weixin credential validation. */
267
+ weixinDefault: 8_000,
268
+ /** Long-poll timeout for dashboard QR login wait calls. */
269
+ weixinQrPoll: 35_000,
270
+ /** Default timeout for Slack credential validation. */
271
+ slackDefault: 8_000,
272
+ /** Default timeout for Discord credential validation. */
273
+ discordDefault: 8_000,
274
+ /** Default timeout for DingTalk credential validation (gettoken endpoint). */
275
+ dingtalkDefault: 8_000,
276
+ /** Default timeout for WeChat Work bot validation handshake. */
277
+ wecomDefault: 8_000,
278
+ };
279
+ // ---------------------------------------------------------------------------
280
+ // Agent auto-update
281
+ // ---------------------------------------------------------------------------
282
+ /** Timeouts for the background agent auto-update system. */
283
+ export const AGENT_UPDATE_TIMEOUTS = {
284
+ /** After this duration a stale lock file is removed. */
285
+ lockStale: 60 * 60_000,
286
+ /** Maximum time for an agent update command to run. */
287
+ commandTimeout: 15 * 60_000,
288
+ /** Timeout for `npm prefix -g`. */
289
+ npmPrefix: 10_000,
290
+ /** Timeout for `npm view <pkg> version`. */
291
+ npmView: 20_000,
292
+ };
293
+ // ---------------------------------------------------------------------------
294
+ // Code agent (shared layer)
295
+ // ---------------------------------------------------------------------------
296
+ /** Caching TTLs for agent detection and version lookups. */
297
+ export const AGENT_DETECT_TIMEOUTS = {
298
+ /** How long a binary-detection result is cached. */
299
+ detectTtl: 1_000,
300
+ /** How long a version string is cached. */
301
+ versionTtl: 5 * 60_000,
302
+ /** Timeout for the `--version` command itself. */
303
+ versionCommand: 3_000,
304
+ };
305
+ /** Grace period added to the user-configured timeout before hard-killing the agent. */
306
+ export const AGENT_STREAM_HARD_KILL_GRACE_MS = 10_000;
307
+ /**
308
+ * On user abort, wait this long for the agent CLI to flush its session JSONL
309
+ * (including any `[Request interrupted]` marker) before falling back to
310
+ * SIGTERM. Keeps the partial assistant response persisted so the next task,
311
+ * resumed via --resume, can see it in the transcript.
312
+ */
313
+ export const AGENT_GRACEFUL_ABORT_GRACE_MS = 2_000;
314
+ /**
315
+ * claude-tui stall watchdog — claude CLI is known to freeze mid-turn (observed
316
+ * 2026-06-02 on 2.1.160: after a tool_result lands, the next assistant segment
317
+ * never starts; the process stays alive, the JSONL goes permanently quiet, no
318
+ * Stop hook ever fires). When every live signal (main JSONL, hook tool events,
319
+ * sub-agent sidecars, hook lifecycle state) is silent past the threshold the
320
+ * driver SIGTERMs the PTY and the dispatch wrapper auto-resumes the session
321
+ * once. Quiet threshold must sit safely above the longest healthy gap between
322
+ * JSONL events — a single max-effort inference can take a few minutes before
323
+ * its first content block lands.
324
+ */
325
+ export const CLAUDE_TUI_STALL_QUIET_MS = 10 * 60_000;
326
+ /**
327
+ * Stall threshold while a hook-reported tool is still executing (PreToolUse
328
+ * seen, no matching PostToolUse). Claude's own Bash timeout caps foreground
329
+ * commands at ~10 minutes and fires PostToolUse either way, so a pending tool
330
+ * silent for this long means the freeze hit mid-execution.
331
+ */
332
+ export const CLAUDE_TUI_STALL_PENDING_TOOL_MS = 30 * 60_000;
333
+ /**
334
+ * Fast-path stall: a healthy claude TUI repaints continuously while a turn is
335
+ * in flight (spinner frames, stream ticks, status line) — the PTY never goes
336
+ * byte-silent for minutes. If NO PTY output arrives for this long AND every
337
+ * structured signal is equally quiet, the process event loop itself is gone
338
+ * (the 2.1.160 mid-turn freeze: attachment lands → next API call never
339
+ * assembles). Declare the stall now instead of waiting out the 10/30-minute
340
+ * quiet thresholds — turns a 10-30 分钟「卡死」into a ~3 分钟自愈。
341
+ * False-positive safe: long thinking / long Bash keep painting frames, which
342
+ * refreshes the PTY signal and defers this path to the slow thresholds.
343
+ */
344
+ export const CLAUDE_TUI_STALL_PTY_DEAD_MS = 3 * 60_000;
345
+ /**
346
+ * Settle window after the TUI paints the "selected model is unavailable" banner
347
+ * (a 404 model_not_found). The notice is terminal — claude paints it then idles
348
+ * at the REPL forever: no JSONL is written, no Stop hook fires. We wait this
349
+ * brief window to cross-validate that nothing substantive followed (the banner
350
+ * alone is evidence, not a verdict — same discipline as resolveClaudeTuiLimitOutcome)
351
+ * before ending the turn, instead of waiting out the 3–10 minute stall watchdog.
352
+ */
353
+ export const CLAUDE_TUI_MODEL_ERROR_SETTLE_MS = 2_500;
354
+ /**
355
+ * TTL for the post-Stop `hold-background` path. The hold protects
356
+ * run_in_background agents living inside the claude process — but a live
357
+ * agent keeps emitting hook/sidecar/JSONL traffic. If the hold sees no
358
+ * activity on ANY channel for this long, the pending count is phantom (lost
359
+ * <task-notification>, agents already finished): release as a NORMAL Stop.
360
+ * Without this TTL the stall watchdog eventually fires instead, mislabels the
361
+ * cleanly-finished turn 'stalled', and injects a confusing auto-resume prompt
362
+ * (the「回合明明答完了还被注入 Continue」symptom).
363
+ */
364
+ export const CLAUDE_TUI_STOP_HOLD_QUIET_TTL_MS = 10 * 60_000;
365
+ /** Codex-specific grace period added to the user-configured timeout. */
366
+ export const CODEX_STREAM_HARD_KILL_GRACE_MS = 5_000;
367
+ /**
368
+ * If a session file was modified more recently than this threshold,
369
+ * consider the session "running". Shared across Claude, Codex, and Gemini drivers.
370
+ */
371
+ export const SESSION_RUNNING_THRESHOLD_MS = 10_000;
372
+ // ---------------------------------------------------------------------------
373
+ // Driver — Codex
374
+ // ---------------------------------------------------------------------------
375
+ /** Timeout for the Codex app-server to become ready after spawn. */
376
+ export const CODEX_APPSERVER_SPAWN_TIMEOUT_MS = 15_000;
377
+ // ---------------------------------------------------------------------------
378
+ // Driver — Gemini
379
+ // ---------------------------------------------------------------------------
380
+ /** Timeouts for Gemini usage / quota queries. */
381
+ export const GEMINI_USAGE_TIMEOUTS = {
382
+ /** Max time for the curl quota request. */
383
+ request: 5_000,
384
+ /** Extra buffer added to the curl timeout for the execSync wrapper. */
385
+ execSyncBuffer: 3_000,
386
+ };
387
+ // ---------------------------------------------------------------------------
388
+ // User config sync
389
+ // ---------------------------------------------------------------------------
390
+ /** Default interval for the user config file sync poll. */
391
+ export const USER_CONFIG_SYNC_DEFAULT_INTERVAL_MS = 1_000;
392
+ // ---------------------------------------------------------------------------
393
+ // Git status
394
+ // ---------------------------------------------------------------------------
395
+ /**
396
+ * Upper bound for a single `git status` invocation. Bounded so a huge repo or a
397
+ * stuck `.git/index.lock` can never block `/status` or a workspace poll. Matches
398
+ * the timeout used by the existing `/api/git-changes` endpoint.
399
+ */
400
+ export const GIT_STATUS_TIMEOUT_MS = 5_000;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Git working-tree status as a cross-platform OS primitive.
3
+ *
4
+ * One bounded `git status --porcelain=v2 --branch` invocation, parsed into a
5
+ * structured {@link GitStatus}. Porcelain v2 is machine-stable across git
6
+ * versions and locales, so we count entry lines rather than scraping human
7
+ * output. A single reader feeds both the IM `/status` command and the Dashboard
8
+ * workspace view — no git logic is duplicated in the channels or the SPA.
9
+ */
10
+ import { spawnSync } from 'node:child_process';
11
+ import { GIT_STATUS_TIMEOUT_MS } from './constants.js';
12
+ /**
13
+ * Read the git status of `dir`. Returns `null` for a non-repo, a missing git
14
+ * binary, or a timeout — callers simply omit the git section. Never throws.
15
+ *
16
+ * Walks up from `dir` like git itself, so a workspace nested below the repo root
17
+ * is still recognised. Uses `GIT_OPTIONAL_LOCKS=0` to avoid contending with
18
+ * other git processes for the index lock.
19
+ */
20
+ export function readGitStatus(dir) {
21
+ if (!dir)
22
+ return null;
23
+ try {
24
+ const result = spawnSync('git', ['status', '--porcelain=v2', '--branch', '--untracked-files=normal'], {
25
+ cwd: dir,
26
+ timeout: GIT_STATUS_TIMEOUT_MS,
27
+ encoding: 'utf-8',
28
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
29
+ });
30
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string')
31
+ return null;
32
+ return parseGitStatusV2(result.stdout);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /** Parse `git status --porcelain=v2 --branch` output. Exported for testing. */
39
+ export function parseGitStatusV2(stdout) {
40
+ let branch = null;
41
+ let detached = false;
42
+ let shortSha = null;
43
+ let upstream = null;
44
+ let ahead = 0;
45
+ let behind = 0;
46
+ let staged = 0;
47
+ let unstaged = 0;
48
+ let untracked = 0;
49
+ for (const line of stdout.split('\n')) {
50
+ if (!line)
51
+ continue;
52
+ if (line.startsWith('# branch.head ')) {
53
+ const value = line.slice('# branch.head '.length).trim();
54
+ if (value === '(detached)') {
55
+ detached = true;
56
+ branch = null;
57
+ }
58
+ else {
59
+ branch = value;
60
+ }
61
+ }
62
+ else if (line.startsWith('# branch.oid ')) {
63
+ const value = line.slice('# branch.oid '.length).trim();
64
+ if (value && value !== '(initial)')
65
+ shortSha = value.slice(0, 7);
66
+ }
67
+ else if (line.startsWith('# branch.upstream ')) {
68
+ upstream = line.slice('# branch.upstream '.length).trim() || null;
69
+ }
70
+ else if (line.startsWith('# branch.ab ')) {
71
+ const m = line.match(/\+(\d+)\s+-(\d+)/);
72
+ if (m) {
73
+ ahead = parseInt(m[1], 10);
74
+ behind = parseInt(m[2], 10);
75
+ }
76
+ }
77
+ else if (line.startsWith('1 ') || line.startsWith('2 ')) {
78
+ // Ordinary / renamed entry: field 2 is the XY status (X=index, Y=worktree).
79
+ const xy = line.split(' ')[1] || '..';
80
+ if (xy[0] && xy[0] !== '.')
81
+ staged++;
82
+ if (xy[1] && xy[1] !== '.')
83
+ unstaged++;
84
+ }
85
+ else if (line.startsWith('u ')) {
86
+ // Unmerged (conflict) — count as a working-tree change.
87
+ unstaged++;
88
+ }
89
+ else if (line.startsWith('? ')) {
90
+ untracked++;
91
+ }
92
+ }
93
+ return {
94
+ branch,
95
+ detached,
96
+ shortSha,
97
+ upstream,
98
+ ahead,
99
+ behind,
100
+ staged,
101
+ unstaged,
102
+ untracked,
103
+ changed: staged + unstaged + untracked,
104
+ };
105
+ }
106
+ /**
107
+ * Render a {@link GitStatus} into a single friendly line (no channel-specific
108
+ * markup), e.g.:
109
+ *
110
+ * main ↑2 ↓1 · 5 changed (3 staged · 2 untracked)
111
+ * feature/x · no upstream · clean
112
+ * (detached a1b2c3d) · 1 changed
113
+ *
114
+ * Returns `null` when there is no git status to show, so callers can omit the
115
+ * line entirely.
116
+ */
117
+ export function formatGitStatusLine(git) {
118
+ if (!git)
119
+ return null;
120
+ const head = git.detached
121
+ ? `(detached${git.shortSha ? ` ${git.shortSha}` : ''})`
122
+ : git.branch || '(unknown)';
123
+ let lead = head;
124
+ if (git.ahead || git.behind) {
125
+ const ab = [git.ahead ? `↑${git.ahead}` : '', git.behind ? `↓${git.behind}` : '']
126
+ .filter(Boolean)
127
+ .join(' ');
128
+ lead += ` ${ab}`;
129
+ }
130
+ const segments = [lead];
131
+ if (!git.detached && !git.upstream)
132
+ segments.push('no upstream');
133
+ if (git.changed > 0) {
134
+ const detail = [
135
+ git.staged ? `${git.staged} staged` : '',
136
+ git.unstaged ? `${git.unstaged} unstaged` : '',
137
+ git.untracked ? `${git.untracked} untracked` : '',
138
+ ].filter(Boolean);
139
+ segments.push(`${git.changed} changed${detail.length ? ` (${detail.join(' · ')})` : ''}`);
140
+ }
141
+ else {
142
+ segments.push('clean');
143
+ }
144
+ return segments.join(' · ');
145
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One-time backward-compat shims for the pikiclaw → pikiloop rename.
3
+ *
4
+ * Both run once at process startup — BEFORE any config is read or any lock /
5
+ * PID file is taken — so existing installs keep their settings, credentials,
6
+ * managed browser profile and skills with zero user action.
7
+ *
8
+ * Remove this file (and the LEGACY_* constants) a couple of releases after the
9
+ * rename has propagated.
10
+ */
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { STATE_DIR_NAME, LEGACY_STATE_DIR_NAME, ENV_PREFIX, LEGACY_ENV_PREFIX, } from './constants.js';
15
+ /**
16
+ * Mirror every `PIKICLAW_*` env var onto the matching `PIKILOOP_*` name when the
17
+ * new name is unset. Covers user-set vars (shell profiles, docker-compose,
18
+ * systemd units) AND internal ones a still-old parent process may have set
19
+ * across an upgrade boundary (e.g. PIKICLAW_DAEMON_CHILD, PIKICLAW_FROM_LAUNCHD).
20
+ */
21
+ export function hydrateLegacyEnv() {
22
+ for (const [key, value] of Object.entries(process.env)) {
23
+ if (value === undefined)
24
+ continue;
25
+ if (!key.startsWith(LEGACY_ENV_PREFIX))
26
+ continue;
27
+ const mapped = ENV_PREFIX + key.slice(LEGACY_ENV_PREFIX.length);
28
+ if (process.env[mapped] === undefined)
29
+ process.env[mapped] = value;
30
+ }
31
+ }
32
+ /**
33
+ * Migrate `~/.pikiclaw` → `~/.pikiloop` exactly once.
34
+ *
35
+ * No-op when the new dir already exists (migrated or fresh install) or the old
36
+ * one is absent (brand-new user). A same-volume rename is atomic; on a
37
+ * cross-device failure we fall back to a recursive copy and deliberately leave
38
+ * the old dir in place so a partial/failed copy can never lose user data.
39
+ */
40
+ export function migrateLegacyStateDir() {
41
+ try {
42
+ const home = os.homedir();
43
+ const next = path.join(home, STATE_DIR_NAME);
44
+ const prev = path.join(home, LEGACY_STATE_DIR_NAME);
45
+ if (fs.existsSync(next))
46
+ return;
47
+ if (!fs.existsSync(prev))
48
+ return;
49
+ try {
50
+ fs.renameSync(prev, next);
51
+ }
52
+ catch {
53
+ // Cross-device or in-use: copy and keep the original as a safety net.
54
+ fs.cpSync(prev, next, { recursive: true });
55
+ }
56
+ }
57
+ catch {
58
+ // Best-effort only — never block startup on migration.
59
+ }
60
+ }