lark-remote 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.en.md +162 -0
- package/README.md +161 -0
- package/dist/binder.d.ts +57 -0
- package/dist/binder.js +105 -0
- package/dist/bridge/index.d.ts +338 -0
- package/dist/bridge/index.js +1281 -0
- package/dist/bridge/queue-manager.d.ts +277 -0
- package/dist/bridge/queue-manager.js +729 -0
- package/dist/card/bash-card-session.d.ts +78 -0
- package/dist/card/bash-card-session.js +174 -0
- package/dist/card/bash-renderer.d.ts +24 -0
- package/dist/card/bash-renderer.js +187 -0
- package/dist/card/card-budget.d.ts +31 -0
- package/dist/card/card-budget.js +280 -0
- package/dist/card/card-session.d.ts +83 -0
- package/dist/card/card-session.js +185 -0
- package/dist/card/card-shared.d.ts +28 -0
- package/dist/card/card-shared.js +99 -0
- package/dist/card/collapsible.d.ts +26 -0
- package/dist/card/collapsible.js +87 -0
- package/dist/card/run-card-session.d.ts +78 -0
- package/dist/card/run-card-session.js +165 -0
- package/dist/card/run-renderer.d.ts +66 -0
- package/dist/card/run-renderer.js +841 -0
- package/dist/card/run-state.d.ts +103 -0
- package/dist/card/run-state.js +313 -0
- package/dist/card/text-truncate.d.ts +56 -0
- package/dist/card/text-truncate.js +184 -0
- package/dist/card/time.d.ts +1 -0
- package/dist/card/time.js +14 -0
- package/dist/card/tool-render.d.ts +25 -0
- package/dist/card/tool-render.js +176 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.js +89 -0
- package/dist/common/guards.d.ts +37 -0
- package/dist/common/guards.js +65 -0
- package/dist/config/codex-config.d.ts +113 -0
- package/dist/config/codex-config.js +428 -0
- package/dist/config/dir.d.ts +17 -0
- package/dist/config/dir.js +81 -0
- package/dist/config/index.d.ts +219 -0
- package/dist/config/index.js +388 -0
- package/dist/config/kimi-config.d.ts +33 -0
- package/dist/config/kimi-config.js +111 -0
- package/dist/config/opencode-config.d.ts +20 -0
- package/dist/config/opencode-config.js +124 -0
- package/dist/config/pi-config.d.ts +49 -0
- package/dist/config/pi-config.js +151 -0
- package/dist/config/wizard.d.ts +12 -0
- package/dist/config/wizard.js +84 -0
- package/dist/connector/file-limits.d.ts +7 -0
- package/dist/connector/file-limits.js +8 -0
- package/dist/connector/index.d.ts +86 -0
- package/dist/connector/index.js +340 -0
- package/dist/error-classification.d.ts +15 -0
- package/dist/error-classification.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +580 -0
- package/dist/instance-lock.d.ts +30 -0
- package/dist/instance-lock.js +184 -0
- package/dist/logger/index.d.ts +57 -0
- package/dist/logger/index.js +178 -0
- package/dist/order/index.d.ts +27 -0
- package/dist/order/index.js +90 -0
- package/dist/persistence/atomic-write.d.ts +10 -0
- package/dist/persistence/atomic-write.js +65 -0
- package/dist/persistence/load-json-file.d.ts +9 -0
- package/dist/persistence/load-json-file.js +23 -0
- package/dist/restart.d.ts +24 -0
- package/dist/restart.js +73 -0
- package/dist/router/card-helpers.d.ts +28 -0
- package/dist/router/card-helpers.js +65 -0
- package/dist/router/config/claude.d.ts +14 -0
- package/dist/router/config/claude.js +72 -0
- package/dist/router/config/codex.d.ts +22 -0
- package/dist/router/config/codex.js +100 -0
- package/dist/router/config/common/render.d.ts +16 -0
- package/dist/router/config/common/render.js +158 -0
- package/dist/router/config/index.d.ts +18 -0
- package/dist/router/config/index.js +38 -0
- package/dist/router/config/kimi.d.ts +21 -0
- package/dist/router/config/kimi.js +63 -0
- package/dist/router/config/opencode.d.ts +12 -0
- package/dist/router/config/opencode.js +44 -0
- package/dist/router/config/pi.d.ts +12 -0
- package/dist/router/config/pi.js +50 -0
- package/dist/router/config/types.d.ts +43 -0
- package/dist/router/config/types.js +8 -0
- package/dist/router/index.d.ts +318 -0
- package/dist/router/index.js +2715 -0
- package/dist/router/order-exec-dispatch.d.ts +30 -0
- package/dist/router/order-exec-dispatch.js +48 -0
- package/dist/router/utils.d.ts +44 -0
- package/dist/router/utils.js +116 -0
- package/dist/runner/bash/index.d.ts +4 -0
- package/dist/runner/bash/index.js +5 -0
- package/dist/runner/bash/runner.d.ts +55 -0
- package/dist/runner/bash/runner.js +213 -0
- package/dist/runner/claude/index.d.ts +4 -0
- package/dist/runner/claude/index.js +5 -0
- package/dist/runner/claude/runner.d.ts +29 -0
- package/dist/runner/claude/runner.js +83 -0
- package/dist/runner/codex/argv.d.ts +33 -0
- package/dist/runner/codex/argv.js +40 -0
- package/dist/runner/codex/index.d.ts +1 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/jsonl.d.ts +37 -0
- package/dist/runner/codex/jsonl.js +221 -0
- package/dist/runner/codex/runner.d.ts +65 -0
- package/dist/runner/codex/runner.js +124 -0
- package/dist/runner/common/exec-translator.d.ts +73 -0
- package/dist/runner/common/exec-translator.js +105 -0
- package/dist/runner/common/jsonl-stream.d.ts +39 -0
- package/dist/runner/common/jsonl-stream.js +203 -0
- package/dist/runner/common/process-stopper.d.ts +28 -0
- package/dist/runner/common/process-stopper.js +79 -0
- package/dist/runner/common/runner-utils.d.ts +21 -0
- package/dist/runner/common/runner-utils.js +65 -0
- package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
- package/dist/runner/common/spawn-heartbeat.js +51 -0
- package/dist/runner/common/spawning-runner.d.ts +321 -0
- package/dist/runner/common/spawning-runner.js +634 -0
- package/dist/runner/common/usage.d.ts +31 -0
- package/dist/runner/common/usage.js +29 -0
- package/dist/runner/index.d.ts +19 -0
- package/dist/runner/index.js +24 -0
- package/dist/runner/kimi/index.d.ts +1 -0
- package/dist/runner/kimi/index.js +2 -0
- package/dist/runner/kimi/jsonl.d.ts +28 -0
- package/dist/runner/kimi/jsonl.js +143 -0
- package/dist/runner/kimi/runner.d.ts +50 -0
- package/dist/runner/kimi/runner.js +105 -0
- package/dist/runner/opencode/argv.d.ts +32 -0
- package/dist/runner/opencode/argv.js +35 -0
- package/dist/runner/opencode/index.d.ts +1 -0
- package/dist/runner/opencode/index.js +2 -0
- package/dist/runner/opencode/jsonl.d.ts +68 -0
- package/dist/runner/opencode/jsonl.js +295 -0
- package/dist/runner/opencode/runner.d.ts +63 -0
- package/dist/runner/opencode/runner.js +93 -0
- package/dist/runner/pi/index.d.ts +1 -0
- package/dist/runner/pi/index.js +2 -0
- package/dist/runner/pi/jsonl.d.ts +128 -0
- package/dist/runner/pi/jsonl.js +249 -0
- package/dist/runner/pi/runner.d.ts +37 -0
- package/dist/runner/pi/runner.js +76 -0
- package/dist/runner/registry.d.ts +54 -0
- package/dist/runner/registry.js +75 -0
- package/dist/runner/resolve-agent-choices.d.ts +7 -0
- package/dist/runner/resolve-agent-choices.js +76 -0
- package/dist/runner/sync-agent-choices.d.ts +6 -0
- package/dist/runner/sync-agent-choices.js +73 -0
- package/dist/runner/types.d.ts +250 -0
- package/dist/runner/types.js +8 -0
- package/dist/session/claude/index.d.ts +1 -0
- package/dist/session/claude/index.js +2 -0
- package/dist/session/claude/session-reader.d.ts +34 -0
- package/dist/session/claude/session-reader.js +46 -0
- package/dist/session/claude/sessions.d.ts +53 -0
- package/dist/session/claude/sessions.js +528 -0
- package/dist/session/codex/index.d.ts +1 -0
- package/dist/session/codex/index.js +2 -0
- package/dist/session/codex/rollout-reader.d.ts +88 -0
- package/dist/session/codex/rollout-reader.js +473 -0
- package/dist/session/codex/sessions.d.ts +21 -0
- package/dist/session/codex/sessions.js +40 -0
- package/dist/session/common/catch-up.d.ts +12 -0
- package/dist/session/common/catch-up.js +31 -0
- package/dist/session/common/constants.d.ts +11 -0
- package/dist/session/common/constants.js +12 -0
- package/dist/session/common/content-blocks.d.ts +37 -0
- package/dist/session/common/content-blocks.js +68 -0
- package/dist/session/common/jsonl.d.ts +77 -0
- package/dist/session/common/jsonl.js +351 -0
- package/dist/session/common/usage-accumulator.d.ts +58 -0
- package/dist/session/common/usage-accumulator.js +72 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +11 -0
- package/dist/session/kimi/index.d.ts +1 -0
- package/dist/session/kimi/index.js +2 -0
- package/dist/session/kimi/sessions.d.ts +81 -0
- package/dist/session/kimi/sessions.js +527 -0
- package/dist/session/opencode/index.d.ts +1 -0
- package/dist/session/opencode/index.js +2 -0
- package/dist/session/opencode/sessions.d.ts +81 -0
- package/dist/session/opencode/sessions.js +388 -0
- package/dist/session/pi/index.d.ts +1 -0
- package/dist/session/pi/index.js +2 -0
- package/dist/session/pi/sessions.d.ts +27 -0
- package/dist/session/pi/sessions.js +399 -0
- package/dist/session/registry.d.ts +12 -0
- package/dist/session/registry.js +20 -0
- package/dist/session/session-store.d.ts +86 -0
- package/dist/session/session-store.js +450 -0
- package/dist/startup-contact.d.ts +18 -0
- package/dist/startup-contact.js +51 -0
- package/dist/workspace/index.d.ts +19 -0
- package/dist/workspace/index.js +53 -0
- package/package.json +93 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { loadConfig, getAgentConfig, DEFAULT_STOP_GRACE_MS, } from './config/index.js';
|
|
2
|
+
import { resolveAgentChoices, } from './runner/index.js';
|
|
3
|
+
import { ensureConfig } from './config/wizard.js';
|
|
4
|
+
import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion, } from './config/dir.js';
|
|
5
|
+
import { FeishuConnector } from './connector/index.js';
|
|
6
|
+
import { ClaudeRunner, CodexExecRunner, OpencodeExecRunner, PiRunner, KimiRunner, } from './runner/index.js';
|
|
7
|
+
import { AgentRegistry } from './runner/registry.js';
|
|
8
|
+
import { SessionReaderRegistry, SessionStore } from './session/index.js';
|
|
9
|
+
import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, } from './session/index.js';
|
|
10
|
+
import { CommandRouter, isImmediateAction, formatUsageStats, } from './router/index.js';
|
|
11
|
+
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
12
|
+
import { Bridge } from './bridge/index.js';
|
|
13
|
+
import { initLogger, getLogger } from './logger/index.js';
|
|
14
|
+
import { StartupContactStore, sendStartupHello } from './startup-contact.js';
|
|
15
|
+
import { OwnerBinder, formatPinGuidance } from './binder.js';
|
|
16
|
+
import { InstanceAlreadyRunningError, InstanceLock } from './instance-lock.js';
|
|
17
|
+
import { spawnReplacementBridge, waitForPreviousInstance } from './restart.js';
|
|
18
|
+
import { classifyRejection } from './error-classification.js';
|
|
19
|
+
import { WorkspaceStore } from './workspace/index.js';
|
|
20
|
+
import { markdownDiv } from './card/collapsible.js';
|
|
21
|
+
import { sessionEventPanel } from './router/card-helpers.js';
|
|
22
|
+
import { newSessionButton, agentDisplayName } from './card/card-shared.js';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
/** Send auto-resume card for a restored session */
|
|
25
|
+
async function sendAutoResumeCard(connector, contact, cwd, session, state) {
|
|
26
|
+
if (!contact.userId)
|
|
27
|
+
return;
|
|
28
|
+
const recipient = contact.chatId ?? contact.userId;
|
|
29
|
+
if (!recipient)
|
|
30
|
+
return;
|
|
31
|
+
// Build header with displayTitle (aiTitle or last user message) and recap
|
|
32
|
+
let header = `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`;
|
|
33
|
+
const sections = [];
|
|
34
|
+
if (state.displayTitle) {
|
|
35
|
+
const label = state.aiTitle ? 'AI 标题' : '最近输入';
|
|
36
|
+
sections.push(`🏷️ **${label}**\n${state.displayTitle}`);
|
|
37
|
+
}
|
|
38
|
+
if (state.recap) {
|
|
39
|
+
const recapPreview = state.recap.length > 200 ? state.recap.slice(0, 197) + '...' : state.recap;
|
|
40
|
+
sections.push(`📝 **Recap**\n${recapPreview}`);
|
|
41
|
+
}
|
|
42
|
+
if (sections.length > 0) {
|
|
43
|
+
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
44
|
+
}
|
|
45
|
+
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
46
|
+
// Limit history events to prevent card size exceeded error (11310)
|
|
47
|
+
const MAX_HISTORY_EVENTS = 5;
|
|
48
|
+
const eventsToShow = state.events.slice(-MAX_HISTORY_EVENTS);
|
|
49
|
+
const hiddenCount = state.events.length - MAX_HISTORY_EVENTS;
|
|
50
|
+
// Add indicator if events were hidden
|
|
51
|
+
if (hiddenCount > 0) {
|
|
52
|
+
elements.push(markdownDiv(`📜 还有 ${hiddenCount} 个更早的事件未显示`));
|
|
53
|
+
}
|
|
54
|
+
// Fold session history into collapsible panels: the last 2 events stay expanded
|
|
55
|
+
const agentKind = state.agentKind ?? 'claude';
|
|
56
|
+
eventsToShow.forEach((ev, i) => {
|
|
57
|
+
elements.push(sessionEventPanel(ev, i, eventsToShow.length, 2, agentKind));
|
|
58
|
+
});
|
|
59
|
+
// Add usage/stats at the end
|
|
60
|
+
if (state.usage) {
|
|
61
|
+
const usageStr = formatUsageStats(state.usage);
|
|
62
|
+
elements.push(markdownDiv(usageStr));
|
|
63
|
+
}
|
|
64
|
+
// Add new session button - use column_set+column (div+elements not supported in 2.0)
|
|
65
|
+
const newSessionBtn = newSessionButton();
|
|
66
|
+
elements.push({
|
|
67
|
+
tag: 'column_set',
|
|
68
|
+
columns: [
|
|
69
|
+
{
|
|
70
|
+
tag: 'column',
|
|
71
|
+
width: 'auto',
|
|
72
|
+
elements: [newSessionBtn],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
const card = {
|
|
77
|
+
schema: '2.0',
|
|
78
|
+
config: {
|
|
79
|
+
wide_screen_mode: true,
|
|
80
|
+
},
|
|
81
|
+
header: {
|
|
82
|
+
title: {
|
|
83
|
+
tag: 'plain_text',
|
|
84
|
+
content: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
body: { elements },
|
|
88
|
+
};
|
|
89
|
+
await connector.sendWithRetry(recipient, { card });
|
|
90
|
+
}
|
|
91
|
+
/** Initialize CLI args, config, and logger. Returns { config, configDir, logger, instanceLock }. */
|
|
92
|
+
async function initializeCliAndConfig() {
|
|
93
|
+
const cliArgs = parseCliArgs();
|
|
94
|
+
const configDir = resolveConfigDir(cliArgs.configDir);
|
|
95
|
+
setConfigDir(configDir);
|
|
96
|
+
const configPath = path.join(configDir, 'config.yaml');
|
|
97
|
+
await ensureConfig(configPath);
|
|
98
|
+
let config = loadConfig(configPath);
|
|
99
|
+
// Apply agentChoices: restore last used config for current agent
|
|
100
|
+
config = resolveAgentChoices(config);
|
|
101
|
+
const logDir = path.join(configDir, 'logs');
|
|
102
|
+
initLogger({
|
|
103
|
+
level: config.logging.level,
|
|
104
|
+
dir: logDir,
|
|
105
|
+
});
|
|
106
|
+
const logger = getLogger();
|
|
107
|
+
const instanceLock = new InstanceLock(path.join(configDir, 'lark-remote.pid'));
|
|
108
|
+
return { config, configDir, logger, instanceLock };
|
|
109
|
+
}
|
|
110
|
+
/** Acquire instance lock and setup global exception handlers. */
|
|
111
|
+
function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
|
|
112
|
+
try {
|
|
113
|
+
instanceLock.acquire();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (err instanceof InstanceAlreadyRunningError) {
|
|
117
|
+
logger.error(`[startup] another lark-remote is already running for configDir=${configDir} pid=${err.pid}`);
|
|
118
|
+
console.error(`[lark-remote] already running (pid ${err.pid})`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
instanceLock.registerExitHandlers();
|
|
124
|
+
process.on('uncaughtException', (err) => {
|
|
125
|
+
logger.error('[fatal] uncaught exception:', err);
|
|
126
|
+
instanceLock.release();
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
process.on('unhandledRejection', (reason) => {
|
|
130
|
+
if (classifyRejection(reason) === 'recoverable') {
|
|
131
|
+
logger.error('[fatal] unhandled rejection (recoverable, will continue):', reason);
|
|
132
|
+
// Don't exit — the error is logged but the process continues.
|
|
133
|
+
// Covers transient network issues (502/503/504/ETIMEDOUT/ECONNRESET) and
|
|
134
|
+
// feishu business errors on streaming patches (e.g. 230027 external-chat
|
|
135
|
+
// permission), which escape via SDK throttle detach — see error-classification.ts.
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
logger.error('[fatal] unhandled rejection:', reason);
|
|
139
|
+
instanceLock.release();
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Initialize per-workspace runner factory + agent registry. */
|
|
145
|
+
function initializeRunner(config, configDir, cliArgs) {
|
|
146
|
+
const createRunner = (workspace) => {
|
|
147
|
+
const runner = new ClaudeRunner({
|
|
148
|
+
binary: config.claude.binary,
|
|
149
|
+
model: config.claude.model,
|
|
150
|
+
effort: config.claude.effort,
|
|
151
|
+
stopGraceMs: config.claude.stopGraceMs,
|
|
152
|
+
settings: cliArgs.settings,
|
|
153
|
+
pidDir: configDir,
|
|
154
|
+
workspace,
|
|
155
|
+
});
|
|
156
|
+
return runner;
|
|
157
|
+
};
|
|
158
|
+
const runner = createRunner('default');
|
|
159
|
+
runner.killOrphan();
|
|
160
|
+
runner.registerExitHandlers();
|
|
161
|
+
// Wire multi-agent registries. All five agents (claude/codex/opencode/pi/kimi)
|
|
162
|
+
// are already registered. Bridge and Router depend on these registries instead of
|
|
163
|
+
// importing session readers directly, so adding a new agent is "register factory + reader"
|
|
164
|
+
// with zero upstream changes.
|
|
165
|
+
const agentRegistry = new AgentRegistry();
|
|
166
|
+
agentRegistry.register('claude', (ws) => createRunner(ws));
|
|
167
|
+
// Register CodexExecRunner (codex exec --json, approval_policy=never).
|
|
168
|
+
// Factory reads latest config from container.current so runtime config changes
|
|
169
|
+
// (reasoningEffort, model, etc.) take effect after bridge.setConfig() + clearRunners().
|
|
170
|
+
const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
|
|
171
|
+
agentRegistry.register('codex', (ws) => {
|
|
172
|
+
const container = agentRegistry.getConfigContainer();
|
|
173
|
+
const latestConfig = container?.current ?? config;
|
|
174
|
+
const codexConfig = getAgentConfig(latestConfig, 'codex');
|
|
175
|
+
return new CodexExecRunner({
|
|
176
|
+
binary: codexConfig?.binary ?? 'codex',
|
|
177
|
+
model: codexConfig?.model,
|
|
178
|
+
modelProvider: codexConfig?.modelProvider,
|
|
179
|
+
reasoningEffort: codexConfig?.reasoningEffort,
|
|
180
|
+
stopGraceMs: codexConfig?.stopGraceMs ?? latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
|
|
181
|
+
pidDir: configDir,
|
|
182
|
+
workspace: ws,
|
|
183
|
+
sessionReader: codexSessionReader,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// Register OpencodeExecRunner (run mode)
|
|
187
|
+
// Using `opencode run --format json --auto` instead of HTTP+SSE server mode
|
|
188
|
+
const opencodeConfig = getAgentConfig(config, 'opencode');
|
|
189
|
+
// Register session reader (same instance as used by runner)
|
|
190
|
+
const sessionReaderRegistry = new SessionReaderRegistry();
|
|
191
|
+
sessionReaderRegistry.register('claude', new ClaudeSessionReader());
|
|
192
|
+
sessionReaderRegistry.register('codex', codexSessionReader);
|
|
193
|
+
// Register OpencodeSessionReader (CLI version)
|
|
194
|
+
// Using `opencode session list` and `opencode export` instead of HTTP API
|
|
195
|
+
const opencodeSessionReader = new OpencodeSessionReader({
|
|
196
|
+
binary: opencodeConfig?.binary ?? 'opencode',
|
|
197
|
+
});
|
|
198
|
+
agentRegistry.register('opencode', (ws) => {
|
|
199
|
+
// Get latest config from container (set below for pi)
|
|
200
|
+
const container = agentRegistry.getConfigContainer();
|
|
201
|
+
const latestConfig = container?.current;
|
|
202
|
+
const ocConfig = getAgentConfig(latestConfig, 'opencode');
|
|
203
|
+
// Build model string with validation: provider/model format
|
|
204
|
+
let model;
|
|
205
|
+
if (ocConfig?.modelID) {
|
|
206
|
+
const provider = ocConfig.providerID ?? 'anthropic';
|
|
207
|
+
// Validate: if modelID contains '/', it's already in provider/model format
|
|
208
|
+
if (ocConfig.modelID.includes('/')) {
|
|
209
|
+
model = ocConfig.modelID;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
model = `${provider}/${ocConfig.modelID}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return new OpencodeExecRunner({
|
|
216
|
+
binary: ocConfig?.binary ?? 'opencode',
|
|
217
|
+
model,
|
|
218
|
+
stopGraceMs: config.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
|
|
219
|
+
pidDir: configDir,
|
|
220
|
+
workspace: ws,
|
|
221
|
+
sessionReader: opencodeSessionReader,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
sessionReaderRegistry.register('opencode', opencodeSessionReader);
|
|
225
|
+
// Register PiRunner and PiSessionReader
|
|
226
|
+
// 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
|
|
227
|
+
// 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
|
|
228
|
+
// 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
|
|
229
|
+
const configContainer = { current: config };
|
|
230
|
+
agentRegistry.setConfigContainer(configContainer);
|
|
231
|
+
const piSessionReader = new PiSessionReader();
|
|
232
|
+
agentRegistry.register('pi', (ws) => {
|
|
233
|
+
// 每次 factory 调用时从 registry 获取最新 config
|
|
234
|
+
const container = agentRegistry.getConfigContainer();
|
|
235
|
+
const latestConfig = container?.current;
|
|
236
|
+
const piConf = getAgentConfig(latestConfig, 'pi');
|
|
237
|
+
return new PiRunner({
|
|
238
|
+
binary: piConf?.binary ?? 'pi',
|
|
239
|
+
provider: piConf?.provider ?? 'Volcano',
|
|
240
|
+
model: piConf?.model ?? 'glm-5.2',
|
|
241
|
+
thinking: piConf?.thinking ?? 'medium',
|
|
242
|
+
tools: (piConf?.tools ?? 'read,bash,edit,write,grep,find,ls')
|
|
243
|
+
.split(',')
|
|
244
|
+
.map((t) => t.trim())
|
|
245
|
+
.filter(Boolean),
|
|
246
|
+
pidDir: configDir,
|
|
247
|
+
workspace: ws,
|
|
248
|
+
sessionReader: piSessionReader,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
sessionReaderRegistry.register('pi', piSessionReader);
|
|
252
|
+
// Register KimiRunner and KimiSessionReader
|
|
253
|
+
const kimiSessionReader = new KimiSessionReader();
|
|
254
|
+
agentRegistry.register('kimi', (ws) => {
|
|
255
|
+
// 每次 factory 调用时从 registry 获取最新 config
|
|
256
|
+
const container = agentRegistry.getConfigContainer();
|
|
257
|
+
const latestConfig = container?.current;
|
|
258
|
+
const kimiConf = getAgentConfig(latestConfig, 'kimi');
|
|
259
|
+
return new KimiRunner({
|
|
260
|
+
binary: kimiConf?.binary ?? 'kimi',
|
|
261
|
+
model: kimiConf?.model ?? 'kimi-code/k3',
|
|
262
|
+
thinkingEffort: kimiConf?.thinkingEffort ?? 'max',
|
|
263
|
+
stopGraceMs: config.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
|
|
264
|
+
pidDir: configDir,
|
|
265
|
+
workspace: ws,
|
|
266
|
+
sessionReader: kimiSessionReader,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
sessionReaderRegistry.register('kimi', kimiSessionReader);
|
|
270
|
+
// 2026-07-17: 注册所有 agent 的显示名,实现单点真相
|
|
271
|
+
agentRegistry.registerDisplayName('claude', 'Claude');
|
|
272
|
+
agentRegistry.registerDisplayName('codex', 'Codex');
|
|
273
|
+
agentRegistry.registerDisplayName('opencode', 'Opencode');
|
|
274
|
+
agentRegistry.registerDisplayName('pi', 'Pi');
|
|
275
|
+
agentRegistry.registerDisplayName('kimi', 'Kimi');
|
|
276
|
+
// 设置全局 registry,供 agentDisplayName 等全局函数使用
|
|
277
|
+
AgentRegistry.setGlobalInstance(agentRegistry);
|
|
278
|
+
return { createRunner, runner, agentRegistry, sessionReaderRegistry };
|
|
279
|
+
}
|
|
280
|
+
/** Setup message and card action handlers. */
|
|
281
|
+
function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger) {
|
|
282
|
+
connector.setMessageHandler((msg) => {
|
|
283
|
+
// 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
|
|
284
|
+
const decision = binder.classify(msg.userId, msg.content, msg.chatId);
|
|
285
|
+
if (decision.kind === 'rejected')
|
|
286
|
+
return;
|
|
287
|
+
// 未绑定且 PIN 错误:完全静默丢弃(不回复、不提醒)
|
|
288
|
+
if (decision.kind === 'pin_wrong')
|
|
289
|
+
return;
|
|
290
|
+
if (decision.kind === 'bind_success') {
|
|
291
|
+
void connector
|
|
292
|
+
.sendWithRetry(msg.chatId, { text: '✅ 已绑定到本账号,此后仅你可使用本应用' })
|
|
293
|
+
.catch((err) => logger.error('[binder] bind-success reply failed:', err));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
|
|
297
|
+
logger.info(`message from ${msg.userId}: ${msg.content.slice(0, 100)}`);
|
|
298
|
+
// Add Typing reaction to indicate "still alive"
|
|
299
|
+
void connector.addReaction(msg.messageId, 'Typing');
|
|
300
|
+
const trimmedLower = msg.content.trim().toLowerCase();
|
|
301
|
+
if (trimmedLower === '/stop' || trimmedLower === '/t') {
|
|
302
|
+
void (async () => {
|
|
303
|
+
const stopped = await bridge.interruptCurrentRun({
|
|
304
|
+
userId: msg.userId,
|
|
305
|
+
chatId: msg.chatId,
|
|
306
|
+
});
|
|
307
|
+
if (!stopped) {
|
|
308
|
+
await bridge.sendResult({ text: '当前没有运行中的进程' }, {
|
|
309
|
+
userId: msg.userId,
|
|
310
|
+
chatId: msg.chatId,
|
|
311
|
+
messageId: msg.messageId,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
})().catch((err) => logger.error('[control] /stop failed:', err));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (msg.content.trim().startsWith('/')) {
|
|
318
|
+
void router
|
|
319
|
+
.handle(msg.content, {
|
|
320
|
+
userId: msg.userId,
|
|
321
|
+
chatId: msg.chatId,
|
|
322
|
+
messageId: msg.messageId,
|
|
323
|
+
})
|
|
324
|
+
.catch((err) => logger.error('[control] slash command failed:', err));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// `!` bash commands bypass the serial queue: they don't start claude and
|
|
328
|
+
// don't touch session state, so they must run in parallel with claude runs
|
|
329
|
+
// in the same workspace (design.md §9.6: the queue exists to prevent
|
|
330
|
+
// concurrent claude runs, not to serialize bash). router.handle dispatches
|
|
331
|
+
// `!` to bridge.executeBash, which tracks the run independently.
|
|
332
|
+
if (msg.content.trim().startsWith('!')) {
|
|
333
|
+
void router
|
|
334
|
+
.handle(msg.content, {
|
|
335
|
+
userId: msg.userId,
|
|
336
|
+
chatId: msg.chatId,
|
|
337
|
+
messageId: msg.messageId,
|
|
338
|
+
})
|
|
339
|
+
.catch((err) => logger.error('[control] bang command failed:', err));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Compute workspace with same fallback logic as executeBash:
|
|
343
|
+
// 1. sessionStore cwd → 2. first saved workspace
|
|
344
|
+
let workspace = sessionStore.getCwd(msg.userId) ?? '';
|
|
345
|
+
if (!workspace && workspaceStore) {
|
|
346
|
+
const workspaces = workspaceStore.list();
|
|
347
|
+
if (workspaces.length > 0) {
|
|
348
|
+
workspace = workspaces[0][1];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Step4/D4: 入队时刻(T0)快照 agent+session,随任务闭包带到 T1 执行时刻,
|
|
352
|
+
// 避免排队期间 /new、/config 改写 live 状态导致语义漂移(方案 D4)。唯一
|
|
353
|
+
// 捕获点 Bridge.currentBinding。
|
|
354
|
+
const binding = bridge.currentBinding(msg.userId);
|
|
355
|
+
bridge.enqueue(workspace, async () => {
|
|
356
|
+
await router.handle(msg.content, {
|
|
357
|
+
userId: msg.userId,
|
|
358
|
+
chatId: msg.chatId,
|
|
359
|
+
messageId: msg.messageId,
|
|
360
|
+
},
|
|
361
|
+
// P1-14: lane 与执行 cwd 同源 —— 入队时捕获的 workspace 显式传给
|
|
362
|
+
// forwardToClaude,排队期间 /cd 不再导致旧 lane 消息被 busy-drop。
|
|
363
|
+
// D4/Step4: binding 同源 —— 入队时快照的 agent+session 一并透传,
|
|
364
|
+
// 排队期间 /new、/config 不再导致旧 lane 消息语义漂移。
|
|
365
|
+
{ cwdOverride: workspace, binding });
|
|
366
|
+
}, {
|
|
367
|
+
taskMeta: {
|
|
368
|
+
userId: msg.userId,
|
|
369
|
+
chatId: msg.chatId,
|
|
370
|
+
messageId: msg.messageId,
|
|
371
|
+
messagePreview: msg.content.slice(0, 3000),
|
|
372
|
+
// D3/Step4: binding 随 taskMeta 存进 QueuedTask,供替换闭包
|
|
373
|
+
// (queue.edit/queue.immediate)复用,不重新快照。
|
|
374
|
+
binding,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
connector.setCardActionHandler(async (action) => {
|
|
379
|
+
// 仅已绑定的 owner 可触发卡片操作;其余静默丢弃(计数 + debug)
|
|
380
|
+
if (!binder.isOwner(action.operator.openId)) {
|
|
381
|
+
binder.recordRejectedCardAction(action.operator.openId);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const actionValue = action.action.value;
|
|
385
|
+
if (!actionValue?.cmd)
|
|
386
|
+
return;
|
|
387
|
+
logger.info(`card action: ${actionValue.cmd}`);
|
|
388
|
+
const userId = action.operator.openId;
|
|
389
|
+
const chatId = action.chatId;
|
|
390
|
+
const messageId = action.messageId;
|
|
391
|
+
// Add Typing reaction to indicate "still alive"
|
|
392
|
+
void connector.addReaction(messageId, 'Typing');
|
|
393
|
+
if (actionValue.cmd === 'stop') {
|
|
394
|
+
if (!actionValue.runId) {
|
|
395
|
+
logger.warn('[control] ignored stop card action without runId');
|
|
396
|
+
// Card button clicks must have visible feedback (AGENTS.md 红线:卡片按钮
|
|
397
|
+
// 点击必须有可见反馈,见 design.md §6.2)。
|
|
398
|
+
void bridge
|
|
399
|
+
.sendResult({ text: '⚠️ 无效的停止请求,缺少必要信息' }, { userId, chatId, messageId })
|
|
400
|
+
.catch((err) => logger.error('[control] card stop feedback failed:', err));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
void (async () => {
|
|
404
|
+
const stopped = await bridge.interruptCurrentRun({
|
|
405
|
+
userId,
|
|
406
|
+
chatId,
|
|
407
|
+
runId: actionValue.runId,
|
|
408
|
+
});
|
|
409
|
+
if (!stopped) {
|
|
410
|
+
// runId mismatch or run already exited — mirror /stop text miss path.
|
|
411
|
+
await bridge.sendResult({ text: '该任务已结束,无需终止' }, { userId, chatId, messageId });
|
|
412
|
+
}
|
|
413
|
+
})().catch((err) => logger.error('[control] card stop failed:', err));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const isImmediate = isImmediateAction(actionValue.cmd);
|
|
417
|
+
const workspace = sessionStore.getCwd(userId) ?? '';
|
|
418
|
+
// Build full value object — spread all fields from actionValue,
|
|
419
|
+
// then override option/formValue/inputValue from action for CardKit 2.0 compatibility.
|
|
420
|
+
// 2026-07-04: inputValue 来自 CardKit 2.0 input 组件自带提交图标触发 callback
|
|
421
|
+
const fullValue = {
|
|
422
|
+
...actionValue,
|
|
423
|
+
option: action.action.option,
|
|
424
|
+
formValue: action.action.formValue,
|
|
425
|
+
// CardKit 2.0 input ✓ 提交图标:输入值走 raw.action.input_value(SDK normalizer
|
|
426
|
+
// 丢弃 action.action.input_value,需 connector includeRawEvent: true)。
|
|
427
|
+
inputValue: action.raw?.action?.input_value ??
|
|
428
|
+
action.action.input_value,
|
|
429
|
+
};
|
|
430
|
+
// queue.input 和 config.save 返回 CardActionResponse toast 给点击用户即时反馈。
|
|
431
|
+
// 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是 fire-and-forget,
|
|
432
|
+
// 会吞掉返回值。queue.input 只做一次卡片更新 + toast,快速且不需要串行队列序列化。
|
|
433
|
+
// config.save 内部已有 enqueueConfigAction 串行化保证,但返回值需要直接返回给飞书。
|
|
434
|
+
if (actionValue.cmd === 'queue.input' || actionValue.cmd === 'config.save') {
|
|
435
|
+
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
436
|
+
}
|
|
437
|
+
// order.exec → equivalent queued message (Plan A): resolve the order text
|
|
438
|
+
// at the enqueue boundary and route it through router.handle, exactly like
|
|
439
|
+
// a hand-typed message. See order-exec-dispatch.ts for the contract and
|
|
440
|
+
// why an internal key replaces the Feishu card messageId.
|
|
441
|
+
if (actionValue.cmd === 'order.exec') {
|
|
442
|
+
void dispatchOrderExecForQueue({
|
|
443
|
+
router,
|
|
444
|
+
bridge,
|
|
445
|
+
workspace,
|
|
446
|
+
orderId: actionValue.orderId,
|
|
447
|
+
ctx: { userId, chatId, messageId },
|
|
448
|
+
}).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (isImmediate) {
|
|
452
|
+
bridge.enqueueImmediate(workspace, async () => {
|
|
453
|
+
await router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
bridge.enqueue(workspace, async () => {
|
|
458
|
+
await router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
459
|
+
}, {
|
|
460
|
+
taskMeta: {
|
|
461
|
+
userId,
|
|
462
|
+
chatId,
|
|
463
|
+
messageId,
|
|
464
|
+
messagePreview: `card action: ${actionValue.cmd}`,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async function main() {
|
|
471
|
+
const cliArgs = parseCliArgs();
|
|
472
|
+
// --help/--version must bypass config loading and singleton lock — it's a pure
|
|
473
|
+
// informational query that must work even when another instance is running
|
|
474
|
+
// or no config exists yet.
|
|
475
|
+
if (cliArgs.help) {
|
|
476
|
+
printHelp();
|
|
477
|
+
process.exit(0);
|
|
478
|
+
}
|
|
479
|
+
if (cliArgs.version) {
|
|
480
|
+
printVersion();
|
|
481
|
+
process.exit(0);
|
|
482
|
+
}
|
|
483
|
+
const { config, configDir, logger, instanceLock } = await initializeCliAndConfig();
|
|
484
|
+
// Restart handoff: if spawned as a /restart replacement, wait for the old
|
|
485
|
+
// bridge to die (and release the instance lock) before acquiring it.
|
|
486
|
+
await waitForPreviousInstance();
|
|
487
|
+
setupInstanceLockAndHandlers(instanceLock, logger, configDir);
|
|
488
|
+
logger.info('config loaded');
|
|
489
|
+
logger.info(`configDir = ${configDir}`);
|
|
490
|
+
logger.info(`feishu.appId = ${config.feishu.appId}`);
|
|
491
|
+
logger.info(`claude.binary = ${config.claude.binary}`);
|
|
492
|
+
if (cliArgs.settings) {
|
|
493
|
+
logger.info(`claude.settings = ${cliArgs.settings}`);
|
|
494
|
+
}
|
|
495
|
+
const { runner, agentRegistry, sessionReaderRegistry } = initializeRunner(config, configDir, cliArgs);
|
|
496
|
+
const connector = new FeishuConnector(config);
|
|
497
|
+
const startupContactStore = new StartupContactStore(path.join(configDir, 'startup-contact.json'));
|
|
498
|
+
const binder = new OwnerBinder(startupContactStore);
|
|
499
|
+
if (binder.pendingPin) {
|
|
500
|
+
// 未绑定:控制台(stderr)展示 PIN。守护模式下被 watchdog 重定向到 daemon 日志
|
|
501
|
+
console.error(formatPinGuidance(binder.pendingPin));
|
|
502
|
+
logger.info('[binder] awaiting first binding (PIN printed to stderr)');
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
logger.info(`[binder] bound to owner openId=${binder.boundOpenId()}`);
|
|
506
|
+
}
|
|
507
|
+
const sessionStore = new SessionStore(path.join(configDir, 'last-session.json'), config.defaultAgent);
|
|
508
|
+
const workspaceStore = new WorkspaceStore(path.join(configDir, 'workspace.json'));
|
|
509
|
+
const bridge = new Bridge({
|
|
510
|
+
runner,
|
|
511
|
+
connector,
|
|
512
|
+
sessionStore,
|
|
513
|
+
config,
|
|
514
|
+
workspaceStore,
|
|
515
|
+
agentRegistry,
|
|
516
|
+
sessionReaderRegistry,
|
|
517
|
+
});
|
|
518
|
+
const router = new CommandRouter({
|
|
519
|
+
sessionStore,
|
|
520
|
+
bridge,
|
|
521
|
+
config,
|
|
522
|
+
configPath: path.join(configDir, 'config.yaml'),
|
|
523
|
+
workspacePath: path.join(configDir, 'workspace.json'),
|
|
524
|
+
ordersPath: path.join(configDir, 'orders.json'),
|
|
525
|
+
restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
|
|
526
|
+
sessionReaderRegistry,
|
|
527
|
+
});
|
|
528
|
+
setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger);
|
|
529
|
+
try {
|
|
530
|
+
await connector.connect();
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
logger.error('failed to connect to Feishu, check appId/appSecret');
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
if (binder.isBound()) {
|
|
537
|
+
// 仅已绑定时发送启动通知;未绑定时不打扰(PIN 引导已在控制台输出)
|
|
538
|
+
await sendStartupHello(connector, startupContactStore);
|
|
539
|
+
}
|
|
540
|
+
// Auto-restore: resume the persisted sessionId if available
|
|
541
|
+
const restoredContact = startupContactStore.getContact();
|
|
542
|
+
if (restoredContact?.userId) {
|
|
543
|
+
const restoredCwd = sessionStore.getCwd(restoredContact.userId);
|
|
544
|
+
if (restoredCwd) {
|
|
545
|
+
const sessionIdToResume = sessionStore.getSessionId(restoredContact.userId, config.defaultAgent);
|
|
546
|
+
if (sessionIdToResume) {
|
|
547
|
+
logger.info(`[startup] auto-resuming persisted session ${sessionIdToResume} in ${restoredCwd}`);
|
|
548
|
+
const startupReader = sessionReaderRegistry.get(config.defaultAgent);
|
|
549
|
+
sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
|
|
550
|
+
const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
|
|
551
|
+
// 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
|
|
552
|
+
// 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
|
|
553
|
+
const usage = content.usage;
|
|
554
|
+
// Build auto-resume card and send to the user. Auto-resume is a
|
|
555
|
+
// non-critical startup nicety: a sendWithRetry failure here (e.g.
|
|
556
|
+
// transient feishu API error) must not crash the bridge via
|
|
557
|
+
// main().catch → process.exit(1). Logger is already initialized.
|
|
558
|
+
try {
|
|
559
|
+
await sendAutoResumeCard(connector, restoredContact, restoredCwd, { sessionId: sessionIdToResume, summary: '' }, {
|
|
560
|
+
events: content.events,
|
|
561
|
+
usage,
|
|
562
|
+
aiTitle: content.aiTitle,
|
|
563
|
+
recap: content.recap,
|
|
564
|
+
displayTitle: content.displayTitle,
|
|
565
|
+
agentKind: config.defaultAgent,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
getLogger().warn('[startup] sendAutoResumeCard failed:', err);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
logger.info('bridge is running, press Ctrl+C to exit');
|
|
575
|
+
}
|
|
576
|
+
main().catch((err) => {
|
|
577
|
+
getLogger().error('fatal error:', err);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
});
|
|
580
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare class InstanceAlreadyRunningError extends Error {
|
|
2
|
+
readonly pid: number;
|
|
3
|
+
readonly lockPath: string;
|
|
4
|
+
constructor(pid: number, lockPath: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class InstanceLock {
|
|
7
|
+
private readonly lockPath;
|
|
8
|
+
/**
|
|
9
|
+
* P2-20: injectable process-running probe for testing, and a process-name
|
|
10
|
+
* verifier to defend against PID reuse (a stale pid recycled by an unrelated
|
|
11
|
+
* process would otherwise be misjudged as "our instance still running").
|
|
12
|
+
* Default implementation checks the process name via `ps` when available.
|
|
13
|
+
*/
|
|
14
|
+
private readonly isProcessRunning;
|
|
15
|
+
constructor(lockPath: string, opts?: {
|
|
16
|
+
isProcessRunning?: (pid: number, expectedComm: string | undefined) => boolean;
|
|
17
|
+
});
|
|
18
|
+
acquire(): void;
|
|
19
|
+
release(): void;
|
|
20
|
+
registerExitHandlers(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Lock file stores `pid\ncomm` where comm is the owning process's command
|
|
23
|
+
* name (captured at acquire time). On stale-lock checks we verify the
|
|
24
|
+
* recorded pid is still alive AND still running a process whose comm
|
|
25
|
+
* matches — defending against PID reuse where an unrelated process recycled
|
|
26
|
+
* the stale pid.
|
|
27
|
+
*/
|
|
28
|
+
private lockContents;
|
|
29
|
+
private readPidAndComm;
|
|
30
|
+
}
|