larkin 0.2.70

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 (144) hide show
  1. package/CONTRIBUTING.md +48 -0
  2. package/LICENSE +201 -0
  3. package/README.md +122 -0
  4. package/SECURITY.md +14 -0
  5. package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
  6. package/dist/agent/agent-cli-capabilities.cjs +43 -0
  7. package/dist/agent/agent-cli-capabilities.mjs +35 -0
  8. package/dist/agent/agent-state-store.cjs +961 -0
  9. package/dist/agent/agent-state-store.mjs +923 -0
  10. package/dist/agent/agent-transport.cjs +201 -0
  11. package/dist/agent/agent-transport.mjs +194 -0
  12. package/dist/agent/config-cli-contract.cjs +19 -0
  13. package/dist/agent/config-cli-contract.mjs +16 -0
  14. package/dist/agent/context-prompt.cjs +159 -0
  15. package/dist/agent/context-prompt.mjs +155 -0
  16. package/dist/agent/freshness-gate.mjs +24 -0
  17. package/dist/agent/history-projection.cjs +72 -0
  18. package/dist/agent/history-projection.mjs +65 -0
  19. package/dist/agent/host-reminder-orchestrator.cjs +289 -0
  20. package/dist/agent/host-reminder-orchestrator.mjs +252 -0
  21. package/dist/agent/inbox-projection.cjs +92 -0
  22. package/dist/agent/inbox-projection.mjs +85 -0
  23. package/dist/agent/interaction-state-machine.cjs +710 -0
  24. package/dist/agent/interaction-state-machine.mjs +705 -0
  25. package/dist/agent/reminder-routes.cjs +192 -0
  26. package/dist/agent/reminder-routes.mjs +156 -0
  27. package/dist/agent/reminder-store.cjs +220 -0
  28. package/dist/agent/reminder-store.mjs +205 -0
  29. package/dist/agent/transport-business-context.cjs +486 -0
  30. package/dist/agent/transport-business-context.mjs +446 -0
  31. package/dist/agent/transport-shell.cjs +45 -0
  32. package/dist/agent/transport-shell.mjs +39 -0
  33. package/dist/app/agent-cli.mjs +596 -0
  34. package/dist/app/agent-config.mjs +633 -0
  35. package/dist/app/agent-lark-cli-workspace.cjs +95 -0
  36. package/dist/app/agent-lark-cli-workspace.mjs +85 -0
  37. package/dist/app/agent-readiness.cjs +42 -0
  38. package/dist/app/agent-readiness.mjs +38 -0
  39. package/dist/app/binary-entry.mjs +86 -0
  40. package/dist/app/cli.mjs +176 -0
  41. package/dist/app/dashboard.mjs +205 -0
  42. package/dist/app/internal-command.cjs +57 -0
  43. package/dist/app/internal-command.mjs +48 -0
  44. package/dist/app/lark-channel-secret.mjs +22 -0
  45. package/dist/app/lark-cli.mjs +887 -0
  46. package/dist/app/lark.mjs +45 -0
  47. package/dist/app/local-control.mjs +728 -0
  48. package/dist/app/official-lark-cli.cjs +138 -0
  49. package/dist/app/official-lark-cli.mjs +128 -0
  50. package/dist/app/pi-auth-cli.mjs +67 -0
  51. package/dist/app/run.mjs +364 -0
  52. package/dist/app/runtime-agent-config.mjs +416 -0
  53. package/dist/app/runtime-model-directory.mjs +78 -0
  54. package/dist/app/runtime-process.mjs +113 -0
  55. package/dist/app/session-cli.mjs +83 -0
  56. package/dist/app/setup-dashboard.mjs +26 -0
  57. package/dist/app/setup.mjs +166 -0
  58. package/dist/app/telemetry.mjs +83 -0
  59. package/dist/dashboard/dashboard-assets.mjs +21 -0
  60. package/dist/dashboard/dashboard-avatar.mjs +128 -0
  61. package/dist/dashboard/dashboard-config-controller.mjs +525 -0
  62. package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
  63. package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
  64. package/dist/dashboard/dashboard-template.mjs +29 -0
  65. package/dist/dashboard/dashboard-view-model.mjs +556 -0
  66. package/dist/dashboard/dashboard-workspace.mjs +104 -0
  67. package/dist/dashboard/web/assets/dashboard.css +3 -0
  68. package/dist/dashboard/web/assets/dashboard.js +49 -0
  69. package/dist/feishu/channel-transport.cjs +151 -0
  70. package/dist/feishu/channel-transport.mjs +148 -0
  71. package/dist/feishu/document-comment.cjs +120 -0
  72. package/dist/feishu/document-comment.mjs +109 -0
  73. package/dist/feishu/host-business-state.cjs +477 -0
  74. package/dist/feishu/host-business-state.mjs +462 -0
  75. package/dist/feishu/host-channel-business.cjs +161 -0
  76. package/dist/feishu/host-channel-business.mjs +157 -0
  77. package/dist/feishu/host-processing-eye.cjs +185 -0
  78. package/dist/feishu/host-processing-eye.mjs +181 -0
  79. package/dist/feishu/host-shell.cjs +1425 -0
  80. package/dist/feishu/host-shell.mjs +1385 -0
  81. package/dist/feishu/im-freshness-adapter.mjs +89 -0
  82. package/dist/feishu/interaction-orchestrator.cjs +279 -0
  83. package/dist/feishu/interaction-orchestrator.mjs +242 -0
  84. package/dist/feishu/lark-passthrough.mjs +70 -0
  85. package/dist/feishu/message-policy.cjs +111 -0
  86. package/dist/feishu/message-policy.mjs +98 -0
  87. package/dist/feishu/outbound-transport.cjs +221 -0
  88. package/dist/feishu/outbound-transport.mjs +215 -0
  89. package/dist/platform/build-info.cjs +25 -0
  90. package/dist/platform/build-info.mjs +18 -0
  91. package/dist/platform/callback-capability.cjs +301 -0
  92. package/dist/platform/callback-capability.mjs +252 -0
  93. package/dist/platform/check-bun-version.cjs +12 -0
  94. package/dist/platform/config.cjs +751 -0
  95. package/dist/platform/config.mjs +694 -0
  96. package/dist/platform/process-inspect.cjs +89 -0
  97. package/dist/platform/process-state.cjs +237 -0
  98. package/dist/platform/process-state.mjs +223 -0
  99. package/dist/platform/release-artifacts.mjs +61 -0
  100. package/dist/platform/root-layout.cjs +111 -0
  101. package/dist/platform/root-layout.mjs +72 -0
  102. package/dist/platform/telemetry-config.cjs +64 -0
  103. package/dist/platform/telemetry-config.mjs +56 -0
  104. package/dist/platform/telemetry-spool.cjs +661 -0
  105. package/dist/platform/telemetry-spool.mjs +653 -0
  106. package/dist/platform/telemetry-tracing.cjs +602 -0
  107. package/dist/platform/telemetry-tracing.mjs +596 -0
  108. package/dist/platform/telemetry-uploader.cjs +150 -0
  109. package/dist/platform/telemetry-uploader.mjs +146 -0
  110. package/dist/platform/workspace-service.cjs +469 -0
  111. package/dist/platform/workspace-service.mjs +430 -0
  112. package/dist/runtime/builtin-pi-assets.mjs +33 -0
  113. package/dist/runtime/claude-model-catalog.cjs +102 -0
  114. package/dist/runtime/claude-model-catalog.mjs +99 -0
  115. package/dist/runtime/codex-model-catalog.cjs +108 -0
  116. package/dist/runtime/codex-model-catalog.mjs +105 -0
  117. package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
  118. package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
  119. package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
  120. package/dist/runtime/pi-model-catalog.cjs +116 -0
  121. package/dist/runtime/pi-model-catalog.mjs +75 -0
  122. package/dist/runtime/pi-official-auth.mjs +480 -0
  123. package/dist/runtime/pi-provider-config.cjs +252 -0
  124. package/dist/runtime/pi-provider-config.mjs +239 -0
  125. package/dist/runtime/pi-rpc-client.cjs +255 -0
  126. package/dist/runtime/pi-rpc-client.mjs +251 -0
  127. package/dist/runtime/pi-subagent-injection.mjs +129 -0
  128. package/dist/runtime/pi-subagents.bundle.js +8990 -0
  129. package/dist/runtime/runtime-adapters.mjs +1019 -0
  130. package/dist/runtime/runtime-contracts.cjs +2 -0
  131. package/dist/runtime/runtime-contracts.mjs +1 -0
  132. package/dist/runtime/runtime-host.cjs +930 -0
  133. package/dist/runtime/runtime-host.mjs +922 -0
  134. package/dist/runtime/runtime-model-catalog.cjs +43 -0
  135. package/dist/runtime/runtime-model-catalog.mjs +40 -0
  136. package/dist/runtime/runtime-readiness.cjs +177 -0
  137. package/dist/runtime/runtime-readiness.mjs +133 -0
  138. package/dist/setup/bot-register.mjs +655 -0
  139. package/dist/setup/grant-scopes.mjs +133 -0
  140. package/dist/setup/run-credential-preflight.mjs +89 -0
  141. package/dist/setup/setup-agent-choice.mjs +172 -0
  142. package/dist/setup/setup-bind.mjs +337 -0
  143. package/dist/setup/setup-binding.mjs +52 -0
  144. package/package.json +112 -0
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from "node:child_process";
3
+ import { SpanKind } from "@opentelemetry/api";
4
+ import * as fs from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as path from "node:path";
7
+ import { createAgentStateStore } from "../agent/agent-state-store.mjs";
8
+ import * as larkinConfig from "../platform/config.mjs";
9
+ import { projectInboxCheck, projectInboxEvents } from "../agent/inbox-projection.mjs";
10
+ import { createReminderRoutes } from "../agent/reminder-routes.mjs";
11
+ import { InteractionStateMachine } from "../agent/interaction-state-machine.mjs";
12
+ import { issueCallbackProbe, readCallbackCapability } from "../platform/callback-capability.mjs";
13
+ import { requestAgentUpsert } from "./local-control.mjs";
14
+ export { AGENT_CLI_CAPABILITIES } from "../agent/agent-cli-capabilities.mjs";
15
+ import { AGENT_CLI_CAPABILITIES } from "../agent/agent-cli-capabilities.mjs";
16
+ import { CONFIG_CLI_USAGE, CONFIG_CLI_VALUES } from "../agent/config-cli-contract.mjs";
17
+ import { internalCommandSpec } from "./internal-command.mjs";
18
+ import { packageVersion } from "../platform/build-info.mjs";
19
+ import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
20
+ import { telemetrySingleton } from "../platform/telemetry-tracing.mjs";
21
+ import { feishuImFreshnessAdapter, feishuImTarget, mergeFeishuImCursor, serializeFeishuImTarget, } from "../feishu/im-freshness-adapter.mjs";
22
+ function discoverRuntimeModelDirectorySync(input, env) {
23
+ const childSpec = internalCommandSpec("runtime-model-directory", [input.runtime, input.cwd], env);
24
+ const result = spawnSync(childSpec.command, childSpec.args, {
25
+ encoding: "utf8", env: { ...process.env, ...env }, timeout: 20_000, maxBuffer: 1024 * 1024,
26
+ });
27
+ if (result.error || result.status !== 0)
28
+ throw new Error(`${input.runtime} 模型目录加载失败`);
29
+ let value;
30
+ try {
31
+ value = JSON.parse(result.stdout);
32
+ }
33
+ catch {
34
+ throw new Error(`${input.runtime} 模型目录响应无效`);
35
+ }
36
+ const rows = value?.models;
37
+ if (!Array.isArray(rows) || rows.length === 0 || rows.length > 512)
38
+ throw new Error(`${input.runtime} 模型目录响应无效`);
39
+ const models = rows.flatMap((row) => {
40
+ if (!row || typeof row !== "object" || Array.isArray(row))
41
+ return [];
42
+ const item = row;
43
+ if (typeof item.id !== "string" || !item.id || (item.label !== undefined && typeof item.label !== "string"))
44
+ return [];
45
+ const supportedReasoningEfforts = Array.isArray(item.supportedReasoningEfforts)
46
+ ? item.supportedReasoningEfforts.filter((effort) => typeof effort === "string") : undefined;
47
+ return [{ id: item.id, ...(typeof item.label === "string" ? { label: item.label } : {}),
48
+ ...(supportedReasoningEfforts ? { supportedReasoningEfforts } : {}),
49
+ ...(typeof item.defaultReasoningEffort === "string" ? { defaultReasoningEffort: item.defaultReasoningEffort } : {}) }];
50
+ });
51
+ if (models.length !== rows.length || !models.some((model) => model.id === "default" && /^default: \S/.test(model.label || ""))) {
52
+ throw new Error(`${input.runtime} 模型目录响应无效`);
53
+ }
54
+ return models;
55
+ }
56
+ function defaultIo() {
57
+ return {
58
+ stdout: (text) => process.stdout.write(text),
59
+ stderr: (text) => process.stderr.write(text),
60
+ };
61
+ }
62
+ function parseOptions(argv, booleanFlags = new Set()) {
63
+ const positionals = [];
64
+ const values = new Map();
65
+ const booleans = new Set();
66
+ for (let index = 0; index < argv.length; index += 1) {
67
+ const argument = argv[index];
68
+ if (!argument.startsWith("--")) {
69
+ positionals.push(argument);
70
+ continue;
71
+ }
72
+ const equals = argument.indexOf("=");
73
+ const key = equals >= 0 ? argument.slice(0, equals) : argument;
74
+ if (booleanFlags.has(key)) {
75
+ if (equals >= 0)
76
+ throw new Error(`${key} 不接受值`);
77
+ if (booleans.has(key))
78
+ throw new Error(`${key} 只能指定一次`);
79
+ booleans.add(key);
80
+ continue;
81
+ }
82
+ const value = equals >= 0 ? argument.slice(equals + 1) : argv[++index];
83
+ if (value === undefined || value.startsWith("--"))
84
+ throw new Error(`${key} 需要值`);
85
+ if (values.has(key))
86
+ throw new Error(`${key} 只能指定一次`);
87
+ values.set(key, value);
88
+ }
89
+ return { positionals, values, booleans };
90
+ }
91
+ function numberOption(options, key) {
92
+ const raw = options.values.get(key);
93
+ if (raw === undefined)
94
+ return undefined;
95
+ const value = Number(raw);
96
+ if (!Number.isFinite(value))
97
+ throw new Error(`${key} 必须是有限数字`);
98
+ return value;
99
+ }
100
+ function emitJson(io, value) {
101
+ io.stdout(`${JSON.stringify(value, null, 2)}\n`);
102
+ }
103
+ function query(requestPath, name) {
104
+ return new URL(requestPath, "http://local").searchParams.get(name);
105
+ }
106
+ function migrationError(group, subcommand) {
107
+ if (group === "message") {
108
+ return "message 已移除:只读查看收件箱用 `larkin inbox check`,领取完整消息用 `larkin inbox poll`;飞书发送、回复和查询请使用 `larkin im +messages-send`、`+messages-reply`、`+chat-messages-list` 或 `+messages-mget`。";
109
+ }
110
+ if (group === "channel") {
111
+ return "channel 已移除:群聊操作请使用 `larkin im +chat-list`、`+chat-search`、`chats get`、`+chat-create`、`+chat-update` 或 `chat.members get/create/delete`。";
112
+ }
113
+ if (group === "attachment") {
114
+ return "attachment 已移除:发送附件请使用 `larkin im +messages-send --file/--image/--video/--audio`;下载请使用 `larkin im +messages-resources-download`。";
115
+ }
116
+ if (group === "server")
117
+ return "server 已移除;飞书群与消息信息请通过 `larkin im ...` 查询。";
118
+ if (group === "im")
119
+ return "请通过 `larkin im ...` 使用飞书命令;Runtime 会自动绑定当前 Bot identity,并在写入前执行 freshness gate。可运行 `larkin im --help` 查看帮助。";
120
+ if (group === "profile" && subcommand !== "show") {
121
+ return "profile 只保留只读的 `larkin profile show`;身份和凭证由 `larkin setup` 管理,不支持 update。";
122
+ }
123
+ return null;
124
+ }
125
+ function help() {
126
+ return {
127
+ usage: "larkin <inbox|comment|reminder|interaction|profile|config> ...",
128
+ capabilities: AGENT_CLI_CAPABILITIES,
129
+ };
130
+ }
131
+ function configHelp() {
132
+ return {
133
+ usage: CONFIG_CLI_USAGE,
134
+ values: CONFIG_CLI_VALUES,
135
+ boundary: "Safe user configuration only; credentials, Feishu identity, internal paths, serverId, activeAgent, and raw config remain unavailable.",
136
+ };
137
+ }
138
+ function agentConfigRequest(argv, env, config, agent, dependencies) {
139
+ const [operation = "show", ...rest] = argv;
140
+ const authority = { kind: "agent", agentId: agent.agentId };
141
+ const options = parseOptions(rest, new Set(["--json"]));
142
+ const unknownFlags = [...options.values.keys()].filter((flag) => !["--agent", "--chat", "--model"].includes(flag));
143
+ if (unknownFlags.length)
144
+ throw new Error(`config 不支持参数:${unknownFlags.join(", ")};运行 larkin config --help`);
145
+ const assertOnlyFlags = (valueFlags, booleanFlags = []) => {
146
+ const allowedValues = new Set(valueFlags);
147
+ const allowedBooleans = new Set(booleanFlags);
148
+ const unsupported = [
149
+ ...[...options.values.keys()].filter((flag) => !allowedValues.has(flag)),
150
+ ...[...options.booleans].filter((flag) => !allowedBooleans.has(flag)),
151
+ ];
152
+ if (unsupported.length)
153
+ throw new Error(`当前 config 操作不支持参数:${unsupported.join(", ")};运行 larkin config --help`);
154
+ };
155
+ const targetId = options.values.get("--agent") || agent.agentId;
156
+ const target = config.agents[targetId];
157
+ if (!target)
158
+ throw new Error(`Agent 不存在:${targetId};运行 larkin config show --json 查看可用 Agent`);
159
+ const currentDirectory = (runtime = target.runtime) => (dependencies.modelDirectory ?? ((input) => discoverRuntimeModelDirectorySync(input, env)))({
160
+ agentId: target.agentId, cwd: target.workspaceDir, runtime,
161
+ });
162
+ if (operation === "show") {
163
+ assertOnlyFlags(["--agent", "--chat"], ["--json"]);
164
+ if (options.positionals.length)
165
+ throw new Error("config show 只接受 --agent、--chat 与 --json;运行 larkin config --help");
166
+ const chatId = options.values.get("--chat");
167
+ return larkinConfig.safeConfigView(config, targetId, chatId, larkinConfig.configApplyState(env, config));
168
+ }
169
+ let mutation;
170
+ if (operation === "runtime") {
171
+ assertOnlyFlags(["--agent", "--model"]);
172
+ const runtime = options.positionals[0];
173
+ if (!runtime || options.positionals.length > 1)
174
+ throw new Error("用法: larkin config runtime <runtime> [--model <model>] [--agent <App ID>]");
175
+ const model = options.values.get("--model");
176
+ if (model && !currentDirectory(runtime).some((item) => item.id === model))
177
+ throw new Error(`model 不在 ${runtime} 当前目录中:${model}`);
178
+ mutation = { kind: "set-agent-runtime", agentId: targetId, runtime, ...(model ? { model } : {}) };
179
+ }
180
+ else if (operation === "model") {
181
+ assertOnlyFlags(["--agent"]);
182
+ const model = options.positionals[0];
183
+ if (!model || options.positionals.length > 1)
184
+ throw new Error("用法: larkin config model <model|default> [--agent <App ID>]");
185
+ const directory = currentDirectory();
186
+ if (!directory.some((item) => item.id === model))
187
+ throw new Error(`model 不在 ${target.runtime} 当前目录中:${model}`);
188
+ mutation = { kind: "set-agent-model", agentId: targetId, model };
189
+ }
190
+ else if (operation === "effort") {
191
+ assertOnlyFlags(["--agent"]);
192
+ const effort = options.positionals[0];
193
+ if (!effort || options.positionals.length > 1)
194
+ throw new Error("用法: larkin config effort <level|default|clear> [--agent <App ID>]");
195
+ if (effort === "default" || effort === "clear")
196
+ mutation = { kind: "set-agent-effort", agentId: targetId, effort: null };
197
+ else {
198
+ if (target.model === "default")
199
+ throw new Error("model=default 时不能设置 effort");
200
+ const selected = currentDirectory().find((item) => item.id === target.model);
201
+ if (!selected)
202
+ throw new Error(`当前 model 不在 ${target.runtime} 当前目录中:${target.model}`);
203
+ if (!selected.supportedReasoningEfforts?.includes(effort))
204
+ throw new Error(`effort=${effort} 未被当前模型 ${target.runtime}/${target.model} 声明支持`);
205
+ mutation = { kind: "set-agent-effort", agentId: targetId, effort };
206
+ }
207
+ }
208
+ else if (operation === "mention") {
209
+ const [scope, first, second] = options.positionals;
210
+ if (scope === "global")
211
+ assertOnlyFlags([]);
212
+ else if (scope === "agent" || scope === "chat")
213
+ assertOnlyFlags(["--agent"]);
214
+ else
215
+ assertOnlyFlags([]);
216
+ if (scope === "global" && options.positionals.length === 2 && ["require", "free"].includes(first || "") && second === undefined) {
217
+ mutation = { kind: "set-global-mention", value: first };
218
+ }
219
+ else if (scope === "agent" && options.positionals.length === 2 && ["inherit", "require", "free"].includes(first || "") && second === undefined) {
220
+ mutation = { kind: "set-agent-mention", agentId: targetId, value: first };
221
+ }
222
+ else if (scope === "chat" && options.positionals.length === 3 && first?.startsWith("oc_") && ["inherit", "require", "free"].includes(second || "")) {
223
+ mutation = { kind: "set-chat-mention", agentId: targetId, chatId: first, value: second };
224
+ }
225
+ else
226
+ throw new Error("用法: larkin config mention global <require|free> | mention agent <inherit|require|free> | mention chat <oc_id> <inherit|require|free> [--agent <App ID>]");
227
+ }
228
+ else if (operation === "apply") {
229
+ assertOnlyFlags(["--agent"]);
230
+ if (options.positionals.length)
231
+ throw new Error("用法: larkin config apply [--agent <App ID>]");
232
+ const expectedSignature = larkinConfig.runtimeConfigSignature(config, targetId);
233
+ return (dependencies.requestAgentUpsert ?? requestAgentUpsert)({ larkinHome: config.larkinHome, agentId: targetId }).then((result) => {
234
+ if (!result.ok)
235
+ throw new Error(result.error || "daemon 拒绝应用配置");
236
+ larkinConfig.markConfigApplied(env, targetId, expectedSignature);
237
+ return { ok: true, agentId: targetId, applyState: "applied", result };
238
+ }).catch((error) => { throw new Error(`配置已保存但未应用:${error instanceof Error ? error.message : String(error)}`); });
239
+ }
240
+ else
241
+ throw new Error("config 只支持 show/runtime/model/effort/mention/apply;运行 larkin config --help");
242
+ const result = larkinConfig.mutateConfig(env, mutation, authority);
243
+ return { ok: true, revision: result.revision, persisted: true, applyState: result.applyState, changedScope: result.changedScope };
244
+ }
245
+ function requireAgent(env) {
246
+ const agentId = larkinConfig.resolveRuntimeAuthority(env);
247
+ if (!agentId)
248
+ throw new Error("缺少 Runtime 注入的 Agent authority marker;Agent CLI 不会回退到 activeAgent");
249
+ let config;
250
+ try {
251
+ ({ config } = larkinConfig.loadConfig(env));
252
+ }
253
+ catch {
254
+ throw new Error("无法加载 Larkin 配置;请检查配置格式和文件权限,或运行 larkin setup");
255
+ }
256
+ return { config, agent: larkinConfig.selectAgent(config, { ...env, LARKIN_AGENT_ID: agentId }) };
257
+ }
258
+ function reminderRequest(groupArgv, stateStore, agentId, deps) {
259
+ // ReminderStore's lock lives beside reminders.json. Materialize the canonical
260
+ // state directory through AgentStateStore so first use keeps its path and mode
261
+ // guarantees instead of creating an ad-hoc directory.
262
+ if (!fs.existsSync(stateStore.paths.reminders))
263
+ stateStore.writeJson("reminders", { reminders: [] });
264
+ const [operation, ...rest] = groupArgv;
265
+ const options = parseOptions(rest, new Set(["--all", "--json"]));
266
+ if (options.positionals.length)
267
+ throw new Error(`reminder ${operation || ""} 不接受位置参数:${options.positionals.join(" ")}`);
268
+ const id = options.values.get("--id");
269
+ let method = "GET";
270
+ let requestPath = "/reminders";
271
+ const body = {};
272
+ switch (operation) {
273
+ case "schedule":
274
+ method = "POST";
275
+ body.title = options.values.get("--title") || "";
276
+ if (options.values.has("--fire-at"))
277
+ body.fireAt = options.values.get("--fire-at");
278
+ if (options.values.has("--delay-seconds"))
279
+ body.delaySeconds = numberOption(options, "--delay-seconds");
280
+ if (options.values.has("--repeat"))
281
+ body.repeat = options.values.get("--repeat");
282
+ if (options.values.has("--tz"))
283
+ body.tz = options.values.get("--tz");
284
+ if (options.values.has("--message-id"))
285
+ body.msgId = options.values.get("--message-id");
286
+ if (options.values.has("--channel"))
287
+ body.channel = options.values.get("--channel");
288
+ break;
289
+ case "list": {
290
+ const search = new URLSearchParams();
291
+ if (options.values.has("--status"))
292
+ search.set("status", options.values.get("--status"));
293
+ if (options.booleans.has("--all"))
294
+ search.set("all", "true");
295
+ requestPath += search.size ? `?${search}` : "";
296
+ break;
297
+ }
298
+ case "snooze":
299
+ if (!id)
300
+ throw new Error("reminder snooze 需要 --id");
301
+ method = "POST";
302
+ requestPath += `/${encodeURIComponent(id)}/snooze`;
303
+ body.delaySeconds = numberOption(options, "--delay-seconds");
304
+ break;
305
+ case "update":
306
+ if (!id)
307
+ throw new Error("reminder update 需要 --id");
308
+ method = "PATCH";
309
+ requestPath += `/${encodeURIComponent(id)}`;
310
+ if (options.values.has("--title"))
311
+ body.title = options.values.get("--title");
312
+ if (options.values.has("--fire-at"))
313
+ body.fireAt = options.values.get("--fire-at");
314
+ if (options.values.has("--delay-seconds"))
315
+ body.delaySeconds = numberOption(options, "--delay-seconds");
316
+ if (options.values.has("--repeat"))
317
+ body.repeat = options.values.get("--repeat");
318
+ if (options.values.has("--tz"))
319
+ body.tz = options.values.get("--tz");
320
+ break;
321
+ case "cancel":
322
+ if (!id)
323
+ throw new Error("reminder cancel 需要 --id");
324
+ method = "DELETE";
325
+ requestPath += `/${encodeURIComponent(id)}`;
326
+ break;
327
+ case "log":
328
+ if (!id)
329
+ throw new Error("reminder log 需要 --id");
330
+ requestPath += `/${encodeURIComponent(id)}/log`;
331
+ break;
332
+ default:
333
+ throw new Error("reminder 只支持 schedule/list/snooze/update/cancel/log");
334
+ }
335
+ const routes = createReminderRoutes({
336
+ stateFile: stateStore.paths.reminders,
337
+ agentId,
338
+ query,
339
+ log: () => undefined,
340
+ ...(deps.now ? { now: deps.now } : {}),
341
+ ...(deps.timeZone ? { timeZone: deps.timeZone } : {}),
342
+ });
343
+ return routes.handle({ path: requestPath, pathNoQuery: requestPath.split("?")[0], method, body });
344
+ }
345
+ function readBoundedJsonFile(file, label) {
346
+ const resolved = path.resolve(file);
347
+ const stat = fs.lstatSync(resolved);
348
+ if (!stat.isFile() || stat.isSymbolicLink())
349
+ throw new Error(`${label} 必须是非 symlink 普通文件`);
350
+ if (stat.size > 64 * 1024)
351
+ throw new Error(`${label} 超过 64 KiB`);
352
+ try {
353
+ return JSON.parse(fs.readFileSync(resolved, "utf8"));
354
+ }
355
+ catch (error) {
356
+ throw new Error(`${label} JSON 解析失败:${error.message}`);
357
+ }
358
+ }
359
+ function interactionRequest(groupArgv, stateStore, agentId, larkinHome, deps) {
360
+ const [operation, ...rest] = groupArgv;
361
+ const options = parseOptions(rest);
362
+ if (options.positionals.length)
363
+ throw new Error(`interaction ${operation || ""} 不接受位置参数:${options.positionals.join(" ")}`);
364
+ const machine = new InteractionStateMachine({ stateStore, agentId, ...(deps.now ? { now: deps.now } : {}) });
365
+ if (operation === "callback-status") {
366
+ if (options.values.size)
367
+ throw new Error("interaction callback-status 不接受参数");
368
+ const capability = readCallbackCapability(larkinHome, agentId);
369
+ return { status: capability?.status || "missing", capability };
370
+ }
371
+ if (operation === "callback-probe") {
372
+ if (options.values.size)
373
+ throw new Error("interaction callback-probe 不接受参数");
374
+ const issued = issueCallbackProbe(larkinHome, agentId, deps.now?.());
375
+ if (issued.capability.status === "verified-effective")
376
+ return { status: "verified-effective", message_content: null };
377
+ const card = {
378
+ schema: "2.0",
379
+ header: { title: { tag: "plain_text", content: "验证 Larkin 卡片回调" } },
380
+ body: { elements: [
381
+ { tag: "markdown", content: "点击按钮验证 card.action.trigger 已发布并通过长连接实际送达。此卡片不会执行任何业务操作。" },
382
+ {
383
+ tag: "button",
384
+ type: "primary_filled",
385
+ width: "fill",
386
+ text: { tag: "plain_text", content: "验证回调" },
387
+ behaviors: [{ type: "callback", value: { larkin_callback_probe: issued.nonce } }],
388
+ },
389
+ ] },
390
+ };
391
+ return { status: "probe-issued", message_content: JSON.stringify(card), card };
392
+ }
393
+ if (operation === "create") {
394
+ const specFile = options.values.get("--spec-file");
395
+ const chatId = options.values.get("--chat-id");
396
+ if (!specFile || !chatId)
397
+ throw new Error("interaction create 需要 --spec-file 和 --chat-id");
398
+ const credentialFile = path.join(larkinHome, "bots", `${agentId}.json`);
399
+ let callbackReady = false;
400
+ try {
401
+ const stat = fs.lstatSync(credentialFile);
402
+ if (!stat.isFile() || stat.isSymbolicLink())
403
+ throw new Error("invalid bot credential path");
404
+ callbackReady = readCallbackCapability(larkinHome, agentId)?.status === "verified-effective";
405
+ }
406
+ catch { /* actionable fail-closed error below */ }
407
+ if (!callbackReady)
408
+ throw new Error("card.action.trigger 尚未 verified-effective;请确认订阅已发布,运行 interaction callback-probe,将 message_content 作为 interactive 消息发送并真实点击,再用 interaction callback-status 确认后创建业务卡片");
409
+ const created = machine.create({ definition: readBoundedJsonFile(specFile, "--spec-file"), expected_chat_id: chatId });
410
+ return { ...created, message_content: JSON.stringify(created.card) };
411
+ }
412
+ if (operation === "get") {
413
+ const instanceId = options.values.get("--instance-id");
414
+ const runId = options.values.get("--run-id");
415
+ if (Boolean(instanceId) === Boolean(runId))
416
+ throw new Error("interaction get 必须且只能指定 --instance-id 或 --run-id");
417
+ return machine.get({ ...(instanceId ? { instance_id: instanceId } : {}), ...(runId ? { run_id: runId } : {}) });
418
+ }
419
+ if (operation === "resolve") {
420
+ const runId = options.values.get("--run-id");
421
+ const status = options.values.get("--status");
422
+ const summary = options.values.get("--summary");
423
+ const expectedVersion = numberOption(options, "--expected-version");
424
+ if (!runId || !summary || expectedVersion === undefined || !["succeeded", "failed"].includes(status || "")) {
425
+ throw new Error("interaction resolve 需要 --run-id、--expected-version、--status succeeded|failed 和 --summary");
426
+ }
427
+ let data = {};
428
+ if (options.values.has("--data-json")) {
429
+ try {
430
+ const parsed = JSON.parse(options.values.get("--data-json"));
431
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
432
+ throw new Error("must be an object");
433
+ data = parsed;
434
+ }
435
+ catch (error) {
436
+ throw new Error(`--data-json 解析失败:${error.message}`);
437
+ }
438
+ }
439
+ return machine.resolve({ run_id: runId, expected_version: expectedVersion, status: status, summary, data, agent_id: agentId });
440
+ }
441
+ throw new Error("interaction 只支持 callback-status/callback-probe/create/get/resolve");
442
+ }
443
+ export function runAgentCli(argv, env = process.env, dependencies = {}) {
444
+ const io = dependencies.io ?? defaultIo();
445
+ const [group = "help", subcommand, ...rest] = argv;
446
+ const migration = migrationError(group, subcommand);
447
+ if (migration) {
448
+ io.stderr(`larkin: ${migration}\n`);
449
+ return 2;
450
+ }
451
+ if (group === "--version" || group === "-V") {
452
+ const version = packageVersion(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."));
453
+ io.stdout(`larkin ${version}\n`);
454
+ return 0;
455
+ }
456
+ if (["help", "--help", "-h"].includes(group)) {
457
+ emitJson(io, subcommand === "config" ? configHelp() : help());
458
+ return 0;
459
+ }
460
+ if (group === "config" && ["--help", "-h"].includes(subcommand || "")) {
461
+ emitJson(io, configHelp());
462
+ return 0;
463
+ }
464
+ if (group === "config" && AGENT_CLI_CAPABILITIES.commands.config.includes(subcommand)
465
+ && rest.some((argument) => argument === "--help" || argument === "-h")) {
466
+ emitJson(io, configHelp());
467
+ return 0;
468
+ }
469
+ try {
470
+ const { config, agent } = requireAgent(env);
471
+ const stateStore = dependencies.stateStore ?? createAgentStateStore(config.larkinHome, agent.agentId);
472
+ if (group === "inbox") {
473
+ if (!["check", "poll"].includes(subcommand || "")) {
474
+ throw new Error("inbox 只支持 `larkin inbox check` 与 `larkin inbox poll`");
475
+ }
476
+ const options = parseOptions(rest, new Set(["--json"]));
477
+ if (options.positionals.length || [...options.values.keys()].some((flag) => !["--target", "--limit"].includes(flag))) {
478
+ throw new Error(`inbox ${subcommand} 只接受 --target、${subcommand === "poll" ? "--limit、" : ""}--json`);
479
+ }
480
+ const target = options.values.get("--target");
481
+ if (subcommand === "check") {
482
+ if (options.values.has("--limit"))
483
+ throw new Error("inbox check 不接受 --limit");
484
+ emitJson(io, projectInboxCheck(stateStore.readNdjson("inbox"), target));
485
+ return 0;
486
+ }
487
+ const poll = () => {
488
+ const rawLimit = options.values.get("--limit");
489
+ const limit = rawLimit === undefined ? undefined : Number(rawLimit);
490
+ const polled = stateStore.pollInbox({ ...(target ? { target } : {}), ...(limit !== undefined ? { limit } : {}) });
491
+ const providerMessages = new Map();
492
+ for (const envelope of polled.envelopes) {
493
+ if (typeof envelope.message_id !== "string" || typeof envelope.create_time !== "string")
494
+ continue;
495
+ const localTarget = typeof envelope.target === "string" ? envelope.target
496
+ : (typeof envelope.chat_id === "string" && envelope.chat_id
497
+ ? (typeof envelope.thread_id === "string" && envelope.thread_id
498
+ ? `thread:${envelope.chat_id}:${envelope.thread_id}` : `chat:${envelope.chat_id}`)
499
+ : null);
500
+ if (!localTarget)
501
+ continue;
502
+ const key = serializeFeishuImTarget(feishuImTarget(localTarget));
503
+ const rows = providerMessages.get(key) ?? [];
504
+ rows.push(envelope);
505
+ providerMessages.set(key, rows);
506
+ }
507
+ for (const [key, messages] of providerMessages) {
508
+ const cursor = feishuImFreshnessAdapter.cursor({ messages });
509
+ if (cursor)
510
+ stateStore.mergeFreshnessCursor(key, cursor, mergeFeishuImCursor, env.LARKIN_RUNTIME_OBSERVATION_GENERATION || "external");
511
+ }
512
+ const projected = projectInboxEvents(polled.envelopes);
513
+ const hasMore = polled.pendingCount > 0;
514
+ emitJson(io, { version: 2, delivery: "direct_ack", at_most_once: true, ...projected,
515
+ pending_count: polled.pendingCount, has_more: hasMore,
516
+ ...(hasMore ? { next_action: "Continue polling the same Inbox scope until has_more is false." } : {}),
517
+ seen_through_seq: polled.seenThroughSeq, consumed_delivery_ids: polled.consumedDeliveryIds });
518
+ return 0;
519
+ };
520
+ let telemetry = dependencies.telemetry;
521
+ try {
522
+ telemetry ??= telemetrySingleton(loadTelemetryConfig(env), {
523
+ serviceVersion: packageVersion(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")),
524
+ });
525
+ }
526
+ catch { /* telemetry must not alter Inbox behavior */ }
527
+ if (!telemetry)
528
+ return poll();
529
+ const observed = telemetry.externalPhase(agent.agentId, stateStore.paths.root, "inbox.consume", SpanKind.CONSUMER, poll, "agent_cli");
530
+ return observed && typeof observed.then === "function"
531
+ ? Promise.resolve(observed).catch((error) => { io.stderr(`larkin: ${error.message}\n`); return 2; })
532
+ : observed;
533
+ }
534
+ if (group === "reminder") {
535
+ const result = reminderRequest([subcommand || "", ...rest], stateStore, agent.agentId, dependencies);
536
+ if (!result.ok) {
537
+ io.stderr(`larkin: ${result.error || `reminder 请求失败 (${result.status})`}\n`);
538
+ return result.status >= 500 ? 1 : 2;
539
+ }
540
+ emitJson(io, result.data);
541
+ return 0;
542
+ }
543
+ if (group === "interaction") {
544
+ emitJson(io, interactionRequest([subcommand || "", ...rest], stateStore, agent.agentId, config.larkinHome, dependencies));
545
+ return 0;
546
+ }
547
+ if (group === "profile") {
548
+ if (subcommand !== "show" || rest.some((argument) => argument !== "--json")) {
549
+ throw new Error("profile 只支持 `larkin profile show`");
550
+ }
551
+ const identity = stateStore.readJson("botIdentity", null);
552
+ const name = String(identity?.name || agent.name);
553
+ emitJson(io, {
554
+ kind: "agent", id: agent.agentId, isSelf: true, name, displayName: name,
555
+ openId: identity?.open_id ?? identity?.openId ?? null,
556
+ avatarUrl: identity?.avatar_url ?? identity?.avatarUrl ?? null,
557
+ runtime: agent.runtime, model: agent.model, reasoningEffort: agent.effort ?? null,
558
+ createdAt: agent.createdAt ?? "1970-01-01T00:00:00.000Z",
559
+ });
560
+ return 0;
561
+ }
562
+ if (group === "config") {
563
+ const result = agentConfigRequest([subcommand || "show", ...rest], env, config, agent, dependencies);
564
+ if (result instanceof Promise)
565
+ return result.then((value) => { emitJson(io, value); return 0; }).catch((error) => {
566
+ const message = error.message;
567
+ io.stderr(`larkin: ${message}${message.includes("larkin config --help") ? "" : ";运行 larkin config --help"}\n`);
568
+ return 2;
569
+ });
570
+ emitJson(io, result);
571
+ return 0;
572
+ }
573
+ throw new Error(`不支持的 Agent 命令:${group}。可用能力见 larkin help`);
574
+ }
575
+ catch (error) {
576
+ const message = error.message;
577
+ io.stderr(`larkin: ${message}${group === "config" && !message.includes("larkin config --help") ? ";运行 larkin config --help" : ""}\n`);
578
+ return 2;
579
+ }
580
+ }
581
+ export async function main(argv = process.argv.slice(2), env = process.env) {
582
+ process.exit(await runAgentCli(argv, env));
583
+ }
584
+ function isMainEntry(argvPath = process.argv[1]) {
585
+ if (!argvPath)
586
+ return false;
587
+ const modulePath = fileURLToPath(import.meta.url);
588
+ try {
589
+ return fs.realpathSync(argvPath) === fs.realpathSync(modulePath);
590
+ }
591
+ catch {
592
+ return path.resolve(argvPath) === path.resolve(modulePath);
593
+ }
594
+ }
595
+ if (isMainEntry())
596
+ await main();