@devcxl/opencode-cabbage 1.3.0 → 2.0.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/.codex-plugin/plugin.json +1 -1
- package/README.md +35 -23
- package/README.zh.md +22 -15
- package/assets/agents/dev-lifecycle.md +6 -0
- package/assets/prompts/bootstrap.md +4 -0
- package/assets/skills/flow-change/SKILL.md +109 -0
- package/assets/skills/flow-design/SKILL.md +15 -0
- package/assets/skills/flow-docs/SKILL.md +112 -0
- package/assets/skills/flow-incident/SKILL.md +98 -0
- package/assets/skills/flow-research/SKILL.md +9 -0
- package/assets/skills/flow-setup/SKILL.md +13 -0
- package/codex-skills/agent-dev-lifecycle/SKILL.md +2 -0
- package/codex-skills/flow-change/SKILL.md +109 -0
- package/codex-skills/flow-design/SKILL.md +18 -3
- package/codex-skills/flow-docs/SKILL.md +112 -0
- package/codex-skills/flow-incident/SKILL.md +98 -0
- package/codex-skills/flow-research/SKILL.md +12 -3
- package/codex-skills/flow-setup/SKILL.md +13 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/plugin/agents.d.ts +20 -0
- package/dist/plugin/agents.js +79 -0
- package/dist/plugin/agents.js.map +1 -1
- package/dist/plugin/cabbage-cli.d.ts +28 -0
- package/dist/plugin/cabbage-cli.js +92 -0
- package/dist/plugin/cabbage-cli.js.map +1 -0
- package/dist/plugin/cabbage-tool.d.ts +103 -0
- package/dist/plugin/cabbage-tool.js +169 -0
- package/dist/plugin/cabbage-tool.js.map +1 -0
- package/dist/plugin/commands.d.ts +10 -0
- package/dist/plugin/commands.js +1 -1
- package/dist/plugin/commands.js.map +1 -1
- package/dist/plugin/continuation.d.ts +22 -0
- package/dist/plugin/continuation.js +233 -0
- package/dist/plugin/continuation.js.map +1 -0
- package/dist/plugin/goal-store.d.ts +42 -49
- package/dist/plugin/goal-store.js +35 -32
- package/dist/plugin/goal-store.js.map +1 -1
- package/dist/plugin/goal.d.ts +92 -28
- package/dist/plugin/goal.js +201 -158
- package/dist/plugin/goal.js.map +1 -1
- package/dist/plugin/install-dir.d.ts +13 -0
- package/dist/plugin/install-dir.js +46 -0
- package/dist/plugin/install-dir.js.map +1 -0
- package/dist/plugin/server.d.ts +4 -13
- package/dist/plugin/server.js +278 -423
- package/dist/plugin/server.js.map +1 -1
- package/dist/plugin/session-abort.d.ts +5 -0
- package/dist/plugin/session-abort.js +26 -0
- package/dist/plugin/session-abort.js.map +1 -0
- package/dist/plugin/skills.d.ts +15 -0
- package/dist/plugin/skills.js +47 -50
- package/dist/plugin/skills.js.map +1 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +6 -2
- package/dist/plugin.js.map +1 -1
- package/dist/util/paths.d.ts +0 -6
- package/dist/util/paths.js +0 -15
- package/dist/util/paths.js.map +1 -1
- package/package.json +4 -7
- package/vendor/cabbage/MANIFEST.json +8 -0
- package/vendor/cabbage/cabbage_cli/__init__.py +1 -0
- package/vendor/cabbage/cabbage_cli/__main__.py +2 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/.vitepress/config.ts +78 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/README.md +31 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/package.json +16 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/pnpm-lock.yaml +2504 -0
- package/vendor/cabbage/cabbage_cli/assets/integrations/cabbage.yml +127 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/adr.md +47 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/api-design.md +45 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/change-record.md +21 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/database-design.md +37 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/generic.md +20 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/impact.md +41 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/incident.md +48 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/postmortem.md +43 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/prd.md +71 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/release-plan.md +45 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/rfc.md +44 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/security-review.md +35 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/tasks.md +62 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/tech-spec.md +87 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/test-plan.md +44 -0
- package/vendor/cabbage/cabbage_cli/assets/workflows/change.yaml +11 -0
- package/vendor/cabbage/cabbage_cli/assets/workflows/incident.yaml +4 -0
- package/vendor/cabbage/cabbage_cli/cli.py +283 -0
- package/vendor/cabbage/cabbage_cli/core.py +606 -0
- package/vendor/cabbage/cabbage_cli/scaffold.py +313 -0
package/dist/plugin/server.js
CHANGED
|
@@ -3,19 +3,22 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { initPrompts } from "./prompts.js";
|
|
4
4
|
import { initBootstrap, getBootstrapContent } from "./bootstrap.js";
|
|
5
5
|
import { loadCommands } from "./commands.js";
|
|
6
|
-
import { setupSkillsDir } from "./skills.js";
|
|
7
|
-
import {
|
|
6
|
+
import { setupSkillsDir, loadSkillEntries } from "./skills.js";
|
|
7
|
+
import { buildAgentPayloads } from "./agents.js";
|
|
8
8
|
import { createAgentShellEnv } from "./shell.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { buildGoalTool } from "./goal.js";
|
|
10
|
+
import { mutateGoal } from "./goal.js";
|
|
11
|
+
import { createSessionGoalStore } from "./goal-store.js";
|
|
12
|
+
import { setupCabbageCli } from "./cabbage-cli.js";
|
|
13
|
+
import { buildCabbageTool } from "./cabbage-tool.js";
|
|
14
|
+
import { queueContinuation, autoResume, SessionActivityTracker } from "./continuation.js";
|
|
15
|
+
import { markAborted, clearAborted, clearAllAborted } from "./session-abort.js";
|
|
16
|
+
import { readIndex, updateFlowSession } from "../kernel/session-index.js";
|
|
17
|
+
import { getContextBlock, CONTEXT_MARKER } from "../kernel/context.js";
|
|
13
18
|
import { readProjectProfile } from "../kernel/profile.js";
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
/** autoResume 跳过窗口:索引 30s 内更新过的 flow 不恢复(可能刚续接过,立即恢复会双发) */
|
|
18
|
-
const AUTO_RESUME_SKIP_WINDOW_MS = 30_000;
|
|
19
|
+
/** bootstrap 注入标记(重复注入检测用) */
|
|
20
|
+
const BOOTSTRAP_MARKER = "EXTREMELY_IMPORTANT";
|
|
21
|
+
const ABORTED_SESSION_CLEANUP_INTERVAL = 1000 * 60 * 30;
|
|
19
22
|
/** 从插件包根读取版本号(skills 安装目录按版本隔离) */
|
|
20
23
|
function readPluginVersion(packageRoot) {
|
|
21
24
|
try {
|
|
@@ -27,7 +30,7 @@ function readPluginVersion(packageRoot) {
|
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* 决定系统上下文注入内容:
|
|
31
34
|
* - Primary(goal 激活)→ bootstrap + Project Context 块
|
|
32
35
|
* - 子 agent → 仅 Project Context 块
|
|
33
36
|
* - 其他 → 不注入
|
|
@@ -41,444 +44,296 @@ export function buildFirstUserInjection(input) {
|
|
|
41
44
|
}
|
|
42
45
|
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
43
46
|
}
|
|
44
|
-
export function configureGoalTools(config) {
|
|
45
|
-
config.tools = { ...config.tools, goal: false };
|
|
46
|
-
for (const [agentName, agent] of Object.entries(config.agent ?? {})) {
|
|
47
|
-
if (!agent || typeof agent !== "object" || Array.isArray(agent))
|
|
48
|
-
continue;
|
|
49
|
-
const agentConfig = agent;
|
|
50
|
-
const canUseGoal = agentName === "dev-lifecycle" || agentName === "goal-verify";
|
|
51
|
-
agentConfig.tools = { ...agentConfig.tools, goal: canUseGoal };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
47
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* 下一次 busy→idle 循环再触发。查询失败视为 unknown → 保守跳过(不续接)。
|
|
48
|
+
* 刷新 goal 的会话配置快照(用户切换 agent/模型、或用户新输入时调用)。
|
|
49
|
+
* 数据源为 SessionInfo.agent/model(当前会话选择),V2 形状 {providerID, id, variant?}。
|
|
58
50
|
*/
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// 查询失败 → 保守跳过(fail-closed);告警使长期故障可观测(否则续接会静默永久停止)
|
|
76
|
-
console.warn(`[cabbage] child-session check failed for ${sessionID}:`, error?.message ?? error);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
51
|
+
async function refreshSessionProfile(store, sessionID) {
|
|
52
|
+
await mutateGoal(store, sessionID, async (goal, session) => {
|
|
53
|
+
if (!goal)
|
|
54
|
+
return { goal, value: undefined };
|
|
55
|
+
const agent = session?.agent ?? goal.sessionProfile?.agent;
|
|
56
|
+
if (!agent)
|
|
57
|
+
return { goal, value: undefined };
|
|
58
|
+
const sessionModel = session?.model;
|
|
59
|
+
const model = sessionModel && sessionModel.providerID && sessionModel.id
|
|
60
|
+
? { providerID: sessionModel.providerID, id: sessionModel.id, ...(sessionModel.variant ? { variant: sessionModel.variant } : {}) }
|
|
61
|
+
: goal.sessionProfile?.model;
|
|
62
|
+
goal.sessionProfile = { agent, ...(model ? { model } : {}) };
|
|
63
|
+
return { goal, value: undefined };
|
|
64
|
+
}).catch((err) => {
|
|
65
|
+
console.warn("[cabbage] session profile refresh failed:", err?.message ?? err);
|
|
66
|
+
});
|
|
79
67
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 事件分发(V2 事件词汇):
|
|
70
|
+
* - execution.* → 子会话活动跟踪 + 轮次结束驱动续接
|
|
71
|
+
* - execution.interrupted(reason=user) → 中断即持久化暂停(不依赖内存标记)
|
|
72
|
+
* - inbox.enqueued(user) → 重置计数 + 刷新 profile + 清除中断标记
|
|
73
|
+
* - session.created → 父子关系跟踪
|
|
74
|
+
*/
|
|
75
|
+
async function dispatchEvent(ctx, store, tracker, projectDir, evt) {
|
|
76
|
+
const data = evt.data ?? {};
|
|
77
|
+
const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
|
|
78
|
+
switch (evt.type) {
|
|
79
|
+
case "session.created": {
|
|
80
|
+
if (sessionID)
|
|
81
|
+
tracker.onCreated(sessionID, typeof data.parentID === "string" ? data.parentID : undefined);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
case "session.execution.started":
|
|
85
|
+
case "session.retry.scheduled": {
|
|
86
|
+
if (sessionID)
|
|
87
|
+
tracker.onBusy(sessionID);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case "session.execution.succeeded": {
|
|
91
|
+
if (!sessionID)
|
|
92
|
+
return;
|
|
93
|
+
tracker.onIdle(sessionID);
|
|
94
|
+
const { session, goal } = await store.read(sessionID);
|
|
95
|
+
if (!session || session.parentID)
|
|
96
|
+
return;
|
|
87
97
|
if (!goal || goal.status !== "active")
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
if (
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const lastMessageInfo = messages.length > 0 ? messages[messages.length - 1]?.info : null;
|
|
116
|
-
if (lastMessageInfo?.role === "user")
|
|
117
|
-
return { goal, value: null };
|
|
118
|
-
let lastAssistantMessage = null;
|
|
119
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
120
|
-
const message = messages[i];
|
|
121
|
-
if (message?.info?.role === "assistant") {
|
|
122
|
-
lastAssistantMessage = message;
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const lastAssistantInfo = lastAssistantMessage?.info ?? null;
|
|
127
|
-
const assistantCompleted = typeof lastAssistantInfo?.time === "object"
|
|
128
|
-
&& lastAssistantInfo.time !== null
|
|
129
|
-
&& lastAssistantInfo.time.completed
|
|
130
|
-
&& lastAssistantInfo.time.completed > 0;
|
|
131
|
-
if (!lastAssistantInfo)
|
|
132
|
-
return { goal, value: null };
|
|
133
|
-
if (!assistantCompleted && !lastAssistantInfo.error)
|
|
134
|
-
return { goal, value: null };
|
|
135
|
-
if (!lastAssistantInfo.id)
|
|
136
|
-
return { goal, value: null };
|
|
137
|
-
// [BLOCKED:] 阻塞报告 / [DONE:] 完成报告:agent 明确信号 → 暂停 goal 请用户/验证介入
|
|
138
|
-
// (防空转烧 token;完成报告后应由 dev-lifecycle 派 goal-verify 做最终独立验证)。
|
|
139
|
-
// resume 后首次 tick 豁免(statusReason='resumed' 被消费清除),
|
|
140
|
-
// 否则 resume 会立即被上一轮旧报告再次暂停成死路。
|
|
141
|
-
const assistantText = (lastAssistantMessage?.parts ?? [])
|
|
142
|
-
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
143
|
-
.map((part) => part.text)
|
|
144
|
-
.join("\n");
|
|
145
|
-
const agentSignaled = isBlockedReport(assistantText) || isDoneReport(assistantText);
|
|
146
|
-
if (agentSignaled) {
|
|
147
|
-
if (goal.statusReason === "resumed") {
|
|
148
|
-
goal.statusReason = "";
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
goal.status = "paused";
|
|
152
|
-
goal.statusReason = isBlockedReport(assistantText) ? "blocked report" : "done report";
|
|
153
|
-
if (projectDir)
|
|
154
|
-
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "paused" });
|
|
155
|
-
return { goal, value: null };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
else if (goal.statusReason === "resumed") {
|
|
159
|
-
// 豁免消费:resume 后的首次 tick 无论是否命中信号都清除 kickoff 标记。
|
|
160
|
-
// 若不消费,首 tick 正常推进后 statusReason 残留,后续真实信号会被误豁免一次。
|
|
161
|
-
goal.statusReason = "";
|
|
162
|
-
}
|
|
163
|
-
// Token 记账(快照法):最新完成 assistant 轮的 input+cache.read+output 即整段成本
|
|
164
|
-
// (OpenCode 每轮 cache.read 已含此前所有付费 token)。goal 相对化:首次 tick
|
|
165
|
-
// 用创建时刻前最后完成的 assistant 轮初始化 baseline;单调不减,context 收缩不回退。
|
|
166
|
-
// compaction 不做分段(预算为护栏非账单)。
|
|
167
|
-
let tokensBaseline = goal.tokensBaseline ?? 0;
|
|
168
|
-
if (!(tokensBaseline > 0) && typeof goal.createdAt === "number") {
|
|
169
|
-
for (const message of messages) {
|
|
170
|
-
const info = message?.info;
|
|
171
|
-
if (info?.role !== "assistant")
|
|
172
|
-
continue;
|
|
173
|
-
const completed = info.time?.completed;
|
|
174
|
-
if (!(completed && completed > 0) || completed > goal.createdAt)
|
|
175
|
-
continue;
|
|
176
|
-
tokensBaseline = Math.max(tokensBaseline, snapshotTokenTotal(info.tokens));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const tokensUsed = Math.max(goal.tokensUsed ?? 0, Math.max(0, snapshotTokenTotal(lastAssistantInfo.tokens) - tokensBaseline));
|
|
180
|
-
goal.tokensBaseline = tokensBaseline;
|
|
181
|
-
goal.tokensUsed = tokensUsed;
|
|
182
|
-
// token 预算护栏:达限即暂停(用户需提高预算后 resume,护栏优先)
|
|
183
|
-
if (budgetReached(goal)) {
|
|
98
|
+
return;
|
|
99
|
+
await queueContinuation(ctx, store, sessionID, projectDir, tracker);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
case "session.execution.failed": {
|
|
103
|
+
// 普通轮次错误不自动续接(与 V1 口径一致:[BLOCKED:] 协议覆盖卡点检测,
|
|
104
|
+
// 错误可能自愈,空转续接反而烧 token;若实测发现卡死再补 error 暂停)
|
|
105
|
+
if (sessionID)
|
|
106
|
+
tracker.onIdle(sessionID);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
case "session.execution.interrupted": {
|
|
110
|
+
if (!sessionID)
|
|
111
|
+
return;
|
|
112
|
+
tracker.onIdle(sessionID);
|
|
113
|
+
// 用户中断的两种 reason(v2.0.26 实测,v2.0.28 复验):API session.interrupt → "user";TUI Esc → "shutdown"。
|
|
114
|
+
// 两者都视为用户中断并暂停 goal;服务端关闭走 execution.failed(不命中本分支),
|
|
115
|
+
// 因此不会误暂停 autoResume 可恢复的 goal(详见 ADR 0010)。
|
|
116
|
+
if (data.reason !== "user" && data.reason !== "shutdown")
|
|
117
|
+
return;
|
|
118
|
+
markAborted(sessionID);
|
|
119
|
+
// abort 立即持久化 pause:不依赖内存标记。
|
|
120
|
+
// 否则 30 分钟清理窗口内无轮次结束事件、或插件重启时标记丢失,
|
|
121
|
+
// 用户主动中断的 goal 会被下次轮次/autoResume 自动续接(继续烧 token)。
|
|
122
|
+
void mutateGoal(store, sessionID, async (goal) => {
|
|
123
|
+
if (!goal || goal.status !== "active")
|
|
124
|
+
return { goal, value: undefined };
|
|
184
125
|
goal.status = "paused";
|
|
185
|
-
goal.statusReason = "
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// 不会"prompt 已发但计数未落盘"导致重启 autoResume 双发。
|
|
192
|
-
goal.continuationCount++;
|
|
193
|
-
if (projectDir)
|
|
194
|
-
await updateFlowSession(projectDir, goal.parentIssueNumber, {
|
|
195
|
-
continuationCount: goal.continuationCount,
|
|
196
|
-
lastContinuedAt: Date.now(),
|
|
197
|
-
});
|
|
198
|
-
return { goal, value: { proceed: true, goal } };
|
|
199
|
-
});
|
|
200
|
-
if (!decision?.proceed || !decision.goal)
|
|
126
|
+
goal.statusReason = "aborted by user";
|
|
127
|
+
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "paused" }).catch(() => undefined);
|
|
128
|
+
return { goal, value: undefined };
|
|
129
|
+
}).catch((err) => {
|
|
130
|
+
console.warn("[cabbage] abort pause failed:", err?.message ?? err);
|
|
131
|
+
});
|
|
201
132
|
return;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
133
|
+
}
|
|
134
|
+
case "session.inbox.enqueued": {
|
|
135
|
+
if (!sessionID)
|
|
136
|
+
return;
|
|
137
|
+
const item = data.item;
|
|
138
|
+
if (item?.type !== "user")
|
|
139
|
+
return;
|
|
140
|
+
clearAborted(sessionID);
|
|
205
141
|
await mutateGoal(store, sessionID, async (goal) => {
|
|
206
142
|
if (!goal)
|
|
207
|
-
return { goal, value:
|
|
208
|
-
goal.
|
|
209
|
-
|
|
210
|
-
if (projectDir)
|
|
211
|
-
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "paused" });
|
|
212
|
-
return { goal, value: null };
|
|
143
|
+
return { goal, value: undefined };
|
|
144
|
+
goal.continuationCount = 0;
|
|
145
|
+
return { goal, value: undefined };
|
|
213
146
|
}).catch(() => undefined);
|
|
147
|
+
await refreshSessionProfile(store, sessionID);
|
|
214
148
|
return;
|
|
215
149
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await
|
|
220
|
-
|
|
221
|
-
parts: [{ type: "text", text: "[compact] The session history is growing long. Summarize completed work and continue.", synthetic: true }],
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
catch (err) {
|
|
225
|
-
console.warn("[cabbage] compaction prompt failed:", err);
|
|
226
|
-
}
|
|
150
|
+
case "session.agent.selected":
|
|
151
|
+
case "session.model.selected": {
|
|
152
|
+
if (sessionID)
|
|
153
|
+
await refreshSessionProfile(store, sessionID);
|
|
154
|
+
return;
|
|
227
155
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
156
|
+
default:
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function setupCabbage(ctx, packageRoot) {
|
|
161
|
+
const sourceSkillsDir = path.join(packageRoot, "assets", "skills");
|
|
162
|
+
const promptsDir = path.join(packageRoot, "assets", "prompts");
|
|
163
|
+
const commandsDir = path.join(packageRoot, "assets", "commands");
|
|
164
|
+
const agentsDir = path.join(packageRoot, "assets", "agents");
|
|
165
|
+
const projectDir = ctx.location.directory;
|
|
166
|
+
const pluginVersion = readPluginVersion(packageRoot);
|
|
167
|
+
const skillsDir = await setupSkillsDir(sourceSkillsDir, promptsDir, pluginVersion);
|
|
168
|
+
// 预读磁盘内容:transform 回调是同步的,所有 IO 必须先完成
|
|
169
|
+
const skillEntries = await loadSkillEntries(skillsDir);
|
|
170
|
+
const commands = loadCommands(commandsDir, skillsDir);
|
|
171
|
+
const profile = await readProjectProfile(projectDir);
|
|
172
|
+
const testCommandToken = profile.testCommand?.split(" ")[0] ?? "";
|
|
173
|
+
const agentPayloads = buildAgentPayloads(agentsDir, testCommandToken);
|
|
174
|
+
initPrompts(packageRoot, projectDir);
|
|
175
|
+
await initBootstrap();
|
|
176
|
+
const store = createSessionGoalStore(ctx.session);
|
|
177
|
+
const tracker = new SessionActivityTracker();
|
|
178
|
+
// 内置 Cabbage 引擎(阶段二):按插件版本释放捆绑的 Python 包;
|
|
179
|
+
// 缺 Python 时工具返回可读错误,其余功能不受影响。
|
|
180
|
+
const cabbage = await setupCabbageCli(packageRoot, pluginVersion);
|
|
181
|
+
if (!cabbage.available) {
|
|
182
|
+
console.warn(`[cabbage] built-in change records unavailable: ${cabbage.reason}`);
|
|
183
|
+
}
|
|
184
|
+
// ── skills:注册为原生 skill(内容指向按版本隔离的安装目录,references 可解析) ──
|
|
185
|
+
await ctx.skill.transform((editor) => {
|
|
186
|
+
for (const skill of skillEntries) {
|
|
187
|
+
editor.add({
|
|
188
|
+
id: skill.id,
|
|
189
|
+
name: skill.name,
|
|
190
|
+
...(skill.description ? { description: skill.description } : {}),
|
|
191
|
+
path: skill.path,
|
|
192
|
+
content: skill.content,
|
|
235
193
|
});
|
|
236
194
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
195
|
+
});
|
|
196
|
+
// ── commands:execute 式注册(模板文本作为 user prompt 投递,保留 delivery) ──
|
|
197
|
+
await ctx.command.transform((editor) => {
|
|
198
|
+
for (const cmd of commands) {
|
|
199
|
+
editor.add({
|
|
200
|
+
name: cmd.name,
|
|
201
|
+
...(cmd.description ? { description: cmd.description } : {}),
|
|
202
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
203
|
+
if (cmd.agent) {
|
|
204
|
+
await ctx.session.switchAgent({ sessionID, agent: cmd.agent }).catch((err) => {
|
|
205
|
+
console.warn(`[cabbage] command /${cmd.name}: switchAgent failed:`, err?.message ?? err);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (cmd.model || cmd.subtask) {
|
|
209
|
+
console.warn(`[cabbage] command /${cmd.name}: model/subtask frontmatter is not supported on OpenCode V2; ignoring`);
|
|
210
|
+
}
|
|
211
|
+
const args = (prompt?.text ?? "").trim();
|
|
212
|
+
const text = args ? `${cmd.template}\n\n${args}` : cmd.template;
|
|
213
|
+
await ctx.session.prompt({ sessionID, text, delivery });
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// ── agents:缺失则新增(保留用户已存在的同名 agent),并为所有 agent 装配 goal 权限规则 ──
|
|
219
|
+
// 依赖 editor.update 对缺失 id 的 upsert 语义(新增 agent):该语义未见于 V2 文档,
|
|
220
|
+
// 是 v2.0.16 / v2.0.26 / v2.0.28 的实测行为,单测按同语义锁定 mock。若宿主改为忽略缺失 id,
|
|
221
|
+
// 下方注册校验会告警(可观测)。决策与替代方案见 ADR 0010。
|
|
222
|
+
await ctx.agent.transform((editor) => {
|
|
223
|
+
for (const payload of agentPayloads) {
|
|
224
|
+
if (editor.get(payload.key))
|
|
225
|
+
continue;
|
|
226
|
+
editor.update(payload.key, (agent) => {
|
|
227
|
+
Object.assign(agent, payload.fields);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const ids = new Set([
|
|
231
|
+
...editor.list().map((a) => String(a.id)),
|
|
232
|
+
...agentPayloads.map((p) => p.key),
|
|
233
|
+
]);
|
|
234
|
+
// 工具门控双层策略的规则层:按 action 指定允许名单,其余 agent 显式 deny(后匹配优先)
|
|
235
|
+
const toolAccess = [
|
|
236
|
+
{ action: "goal", allowed: new Set(["dev-lifecycle", "goal-verify"]) },
|
|
237
|
+
{ action: "cabbage", allowed: new Set(["dev-lifecycle"]) },
|
|
238
|
+
];
|
|
239
|
+
for (const id of ids) {
|
|
240
|
+
editor.update(id, (agent) => {
|
|
241
|
+
const permissions = Array.isArray(agent.permissions) ? agent.permissions : [];
|
|
242
|
+
let changed = false;
|
|
243
|
+
for (const { action, allowed } of toolAccess) {
|
|
244
|
+
if (permissions.some((rule) => rule?.action === action))
|
|
245
|
+
continue;
|
|
246
|
+
permissions.push({ action, resource: "*", effect: allowed.has(id) ? "allow" : "deny" });
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
249
|
+
if (changed)
|
|
250
|
+
agent.permissions = permissions;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// 注册校验:自有 agent 必须全部出现在宿主注册表中(兜住 upsert 语义变化的宿主升级)
|
|
255
|
+
try {
|
|
256
|
+
const registered = await ctx.agent.list();
|
|
257
|
+
const registeredIDs = new Set((registered?.data ?? []).map((agent) => String(agent.id)));
|
|
258
|
+
const missing = agentPayloads.map((payload) => payload.key).filter((key) => !registeredIDs.has(key));
|
|
259
|
+
if (missing.length > 0) {
|
|
260
|
+
console.warn(`[cabbage] agent registration incomplete (missing: ${missing.join(", ")}); ` +
|
|
261
|
+
"the host may have changed the undocumented update-upsert semantics (see ADR 0010)");
|
|
248
262
|
}
|
|
249
263
|
}
|
|
250
264
|
catch (err) {
|
|
251
|
-
console.
|
|
252
|
-
}
|
|
253
|
-
finally {
|
|
254
|
-
continuationInFlight.delete(sessionID);
|
|
265
|
+
console.warn("[cabbage] agent registration verification failed:", err?.message ?? err);
|
|
255
266
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
// ── goal 工具与内置 cabbage 引擎:JSON Schema 输入 + permission gating + 工具内授权自检 ──
|
|
268
|
+
await ctx.tool.transform((editor) => {
|
|
269
|
+
editor.add(buildGoalTool(store, projectDir));
|
|
270
|
+
editor.add(buildCabbageTool(cabbage, projectDir));
|
|
271
|
+
});
|
|
272
|
+
// ── shell 环境:非交互 git、禁用凭据提示(V2 的 shell hook 为全局作用域) ──
|
|
273
|
+
await ctx.shell.hook("create.before", (event) => {
|
|
274
|
+
Object.assign(event.env, createAgentShellEnv());
|
|
275
|
+
});
|
|
276
|
+
// ── 系统上下文注入:goal 会话注入 bootstrap;goal/子 agent 注入 Project Context 块 ──
|
|
277
|
+
await ctx.session.hook("context", async (event) => {
|
|
278
|
+
let hasGoal = false;
|
|
279
|
+
let isSubAgent = false;
|
|
280
|
+
try {
|
|
281
|
+
const { goal, session } = await store.read(event.sessionID);
|
|
282
|
+
hasGoal = goal?.status === "active";
|
|
283
|
+
isSubAgent = !!session?.parentID;
|
|
269
284
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// 删除陈旧 entry,防止每次重启对同一会话重复续接
|
|
273
|
-
if (goal && goal.parentIssueNumber !== Number(parentIssueNumber)) {
|
|
274
|
-
console.warn(`[cabbage] auto-resume: index #${parentIssueNumber} points to goal of Flow #${goal.parentIssueNumber}; removing stale entry`);
|
|
275
|
-
await removeFlowSession(projectDir, Number(parentIssueNumber));
|
|
276
|
-
continue;
|
|
285
|
+
catch {
|
|
286
|
+
return;
|
|
277
287
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
288
|
+
const contextBlock = await getContextBlock(projectDir);
|
|
289
|
+
const injection = buildFirstUserInjection({
|
|
290
|
+
hasGoal,
|
|
291
|
+
isSubAgent,
|
|
292
|
+
contextBlock,
|
|
293
|
+
bootstrap: getBootstrapContent(),
|
|
294
|
+
});
|
|
295
|
+
if (!injection)
|
|
296
|
+
return;
|
|
297
|
+
// 幂等:每个模型请求重建 system 数组时,已含标记则跳过(防重复膨胀)
|
|
298
|
+
const alreadyInjected = event.system.some((part) => typeof part.text === "string" && (part.text.includes(BOOTSTRAP_MARKER) || part.text.includes(CONTEXT_MARKER)));
|
|
299
|
+
if (alreadyInjected)
|
|
300
|
+
return;
|
|
301
|
+
event.system.push({ type: "text", text: injection });
|
|
302
|
+
});
|
|
303
|
+
// ── 事件订阅:单一流驱动子会话跟踪与自动续接 ──
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
void (async () => {
|
|
306
|
+
try {
|
|
307
|
+
for await (const evt of ctx.event.subscribe({ signal: controller.signal })) {
|
|
308
|
+
await dispatchEvent(ctx, store, tracker, projectDir, evt).catch((err) => {
|
|
309
|
+
console.error("[cabbage] event dispatch failed:", err);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (!controller.signal.aborted) {
|
|
315
|
+
console.error("[cabbage] event stream ended:", err);
|
|
316
|
+
}
|
|
281
317
|
}
|
|
318
|
+
})();
|
|
319
|
+
// ── 启动恢复:扫描索引,续接插件重启前处于 active 的 goal ──
|
|
320
|
+
void (async () => {
|
|
282
321
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
type: "text",
|
|
287
|
-
text: `[auto-resume] Plugin restarted. Resuming previous goal:\n\n${formatGoal(goal)}\n\nContinue working.`,
|
|
288
|
-
synthetic: true,
|
|
289
|
-
}],
|
|
290
|
-
...(goal.sessionProfile ? { agent: goal.sessionProfile.agent, model: goal.sessionProfile.model } : {}),
|
|
291
|
-
});
|
|
322
|
+
// 校验索引可读(损坏时 readIndex 返回空索引,不阻塞启动)
|
|
323
|
+
await readIndex(projectDir);
|
|
324
|
+
await autoResume(ctx, store, projectDir);
|
|
292
325
|
}
|
|
293
326
|
catch (err) {
|
|
294
327
|
console.error("[cabbage] auto-resume failed:", err);
|
|
295
328
|
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
function startPeriodicCleanup() {
|
|
301
|
-
if (abortedCleanupTimer)
|
|
302
|
-
clearInterval(abortedCleanupTimer);
|
|
303
|
-
abortedCleanupTimer = setInterval(() => {
|
|
304
|
-
abortedSessions.clear();
|
|
329
|
+
})();
|
|
330
|
+
// 中断标记定期清理(防长生命周期进程内存泄漏)
|
|
331
|
+
const abortedCleanupTimer = setInterval(() => {
|
|
332
|
+
clearAllAborted();
|
|
305
333
|
}, ABORTED_SESSION_CLEANUP_INTERVAL);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const sourceSkillsDir = path.join(packageRoot, "assets", "skills");
|
|
310
|
-
const promptsDir = path.join(packageRoot, "assets", "prompts");
|
|
311
|
-
const commandsDir = path.join(packageRoot, "assets", "commands");
|
|
312
|
-
const skillsDir = await setupSkillsDir(sourceSkillsDir, promptsDir, readPluginVersion(packageRoot));
|
|
313
|
-
const projectDir = ctx.worktree || ctx.directory;
|
|
314
|
-
const v1Client = ctx.client._client;
|
|
315
|
-
const goalClient = createGoalClient(ctx.serverUrl, v1Client);
|
|
316
|
-
const goalStore = createMetadataGoalStore(goalClient);
|
|
317
|
-
const goalTool = createGoalTool(goalStore, projectDir);
|
|
318
|
-
const agentsDir = path.join(packageRoot, "assets", "agents");
|
|
319
|
-
await initPrompts(packageRoot, projectDir);
|
|
320
|
-
await initBootstrap();
|
|
321
|
-
startPeriodicCleanup();
|
|
322
|
-
void autoResume(goalClient, goalStore, projectDir).catch(err => {
|
|
323
|
-
console.error("[cabbage] auto-resume failed:", err);
|
|
324
|
-
});
|
|
325
|
-
return {
|
|
326
|
-
tool: {
|
|
327
|
-
goal: goalTool,
|
|
328
|
-
},
|
|
329
|
-
config: async (rawConfig) => {
|
|
330
|
-
const config = rawConfig;
|
|
331
|
-
config.skills = config.skills || {};
|
|
332
|
-
config.skills.paths = config.skills.paths || [];
|
|
333
|
-
if (!config.skills.paths.includes(skillsDir)) {
|
|
334
|
-
config.skills.paths.push(skillsDir);
|
|
335
|
-
}
|
|
336
|
-
config.command = config.command || {};
|
|
337
|
-
for (const cmd of loadCommands(commandsDir, skillsDir)) {
|
|
338
|
-
if (config.command[cmd.name])
|
|
339
|
-
continue;
|
|
340
|
-
config.command[cmd.name] = {
|
|
341
|
-
template: cmd.template,
|
|
342
|
-
description: cmd.description,
|
|
343
|
-
agent: cmd.agent,
|
|
344
|
-
model: cmd.model,
|
|
345
|
-
subtask: cmd.subtask,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
config.agent = config.agent || {};
|
|
349
|
-
// 渲染 permission 占位符:`<profile-test-command>` → Profile.testCommand 首 token(§9.1)
|
|
350
|
-
const profile = await readProjectProfile(projectDir);
|
|
351
|
-
const testCommandToken = profile.testCommand?.split(" ")[0] ?? "";
|
|
352
|
-
const renderPermission = (perm) => {
|
|
353
|
-
if (!perm)
|
|
354
|
-
return perm;
|
|
355
|
-
const renderRules = (rules) => {
|
|
356
|
-
if (!rules)
|
|
357
|
-
return rules;
|
|
358
|
-
const out = {};
|
|
359
|
-
for (const [pattern, action] of Object.entries(rules)) {
|
|
360
|
-
if (pattern.includes("<profile-test-command>") && testCommandToken === "") {
|
|
361
|
-
// Profile 未配置测试命令 → 删除占位规则,避免渲染成 "*" 覆盖兜底 deny
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
out[pattern.replaceAll("<profile-test-command>", testCommandToken)] = action;
|
|
365
|
-
}
|
|
366
|
-
return out;
|
|
367
|
-
};
|
|
368
|
-
const out = { ...perm };
|
|
369
|
-
if (perm.bash && typeof perm.bash === "object" && !Array.isArray(perm.bash)) {
|
|
370
|
-
out.bash = renderRules(perm.bash);
|
|
371
|
-
}
|
|
372
|
-
return out;
|
|
373
|
-
};
|
|
374
|
-
for (const agent of loadAgents(agentsDir)) {
|
|
375
|
-
if (config.agent[agent.key])
|
|
376
|
-
continue;
|
|
377
|
-
config.agent[agent.key] = {
|
|
378
|
-
description: agent.description,
|
|
379
|
-
mode: agent.mode,
|
|
380
|
-
color: agent.color,
|
|
381
|
-
prompt: agent.prompt,
|
|
382
|
-
permission: renderPermission(agent.permission),
|
|
383
|
-
shell: {
|
|
384
|
-
env: createAgentShellEnv(),
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
configureGoalTools(config);
|
|
389
|
-
},
|
|
390
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
391
|
-
if (!output.messages.length)
|
|
392
|
-
return;
|
|
393
|
-
const firstUser = output.messages.find(m => m.info.role === "user");
|
|
394
|
-
if (!firstUser || !firstUser.parts.length)
|
|
395
|
-
return;
|
|
396
|
-
if (firstUser.parts.some(p => p.type === "text" && (p.text.includes("EXTREMELY_IMPORTANT") || p.text.includes(CONTEXT_MARKER))))
|
|
397
|
-
return;
|
|
398
|
-
const sessionID = output.messages[0].info.sessionID;
|
|
399
|
-
const { goal, session } = await goalStore.read(sessionID);
|
|
400
|
-
const contextBlock = await getContextBlock(projectDir);
|
|
401
|
-
const injection = buildFirstUserInjection({
|
|
402
|
-
hasGoal: goal?.status === "active",
|
|
403
|
-
isSubAgent: !!session?.parentID,
|
|
404
|
-
contextBlock,
|
|
405
|
-
bootstrap: getBootstrapContent(),
|
|
406
|
-
});
|
|
407
|
-
if (injection) {
|
|
408
|
-
firstUser.parts.unshift({ type: "text", text: injection });
|
|
409
|
-
}
|
|
410
|
-
},
|
|
411
|
-
async event({ event }) {
|
|
412
|
-
const evt = event;
|
|
413
|
-
if (evt.type === "session.error") {
|
|
414
|
-
const errorName = evt.properties?.error?.name;
|
|
415
|
-
const sessionID = evt.properties.sessionID;
|
|
416
|
-
if (errorName === "MessageAbortedError" && sessionID) {
|
|
417
|
-
abortedSessions.add(sessionID);
|
|
418
|
-
// abort 立即持久化 pause:不依赖内存标记。
|
|
419
|
-
// 否则 30 分钟清理窗口内无 idle 事件、或插件重启时标记丢失,
|
|
420
|
-
// 用户主动中断的 goal 会被下次 idle/autoResume 自动续接(继续烧 token)。
|
|
421
|
-
void mutateGoal(goalStore, sessionID, async (goal) => {
|
|
422
|
-
if (!goal || goal.status !== "active")
|
|
423
|
-
return { goal, value: undefined };
|
|
424
|
-
goal.status = "paused";
|
|
425
|
-
goal.statusReason = "aborted by user";
|
|
426
|
-
if (projectDir)
|
|
427
|
-
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "paused" });
|
|
428
|
-
return { goal, value: undefined };
|
|
429
|
-
}).catch((err) => {
|
|
430
|
-
console.warn("[cabbage] abort pause failed:", err?.message ?? err);
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (evt.type === "session.status" && evt.properties?.status?.type === "idle") {
|
|
435
|
-
const sessionID = evt.properties.sessionID;
|
|
436
|
-
if (sessionID) {
|
|
437
|
-
try {
|
|
438
|
-
const { session } = await goalStore.read(sessionID);
|
|
439
|
-
if (session?.parentID)
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
catch (err) {
|
|
443
|
-
console.error("[cabbage] session lookup failed:", err);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
await queueContinuation(goalClient, goalStore, sessionID, projectDir).catch(err => {
|
|
447
|
-
console.error("[cabbage] continuation failed:", err);
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (evt.type === "message.updated" && evt.properties?.info?.role === "user") {
|
|
452
|
-
const sessionID = evt.properties.sessionID;
|
|
453
|
-
if (sessionID && !continuationInFlight.has(sessionID)) {
|
|
454
|
-
abortedSessions.delete(sessionID);
|
|
455
|
-
const info = evt.properties.info;
|
|
456
|
-
await mutateGoal(goalStore, sessionID, async (goal) => {
|
|
457
|
-
if (!goal)
|
|
458
|
-
return { goal, value: undefined };
|
|
459
|
-
goal.continuationCount = 0;
|
|
460
|
-
// 刷新用户最后使用的 agent/模型快照(用户在 TUI 切换 agent/模型后发消息)
|
|
461
|
-
if (info?.agent) {
|
|
462
|
-
goal.sessionProfile = {
|
|
463
|
-
agent: info.agent,
|
|
464
|
-
model: info.model?.providerID ? { providerID: info.model.providerID, modelID: info.model.modelID } : goal.sessionProfile?.model,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
return { goal, value: undefined };
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (evt.type === "session.updated") {
|
|
472
|
-
const sessionID = evt.properties?.sessionID;
|
|
473
|
-
if (sessionID) {
|
|
474
|
-
const { goal } = await goalStore.read(sessionID);
|
|
475
|
-
if (goal?.status === "complete") {
|
|
476
|
-
await updateFlowSession(projectDir, goal.parentIssueNumber, { status: "completed" });
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
};
|
|
334
|
+
return () => {
|
|
335
|
+
controller.abort();
|
|
336
|
+
clearInterval(abortedCleanupTimer);
|
|
482
337
|
};
|
|
483
338
|
}
|
|
484
339
|
//# sourceMappingURL=server.js.map
|