opencode-acp 1.14.20 → 1.14.21-pr.317.34
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/README.md +18 -6
- package/README.zh-CN.md +18 -6
- package/dist/index.js +350 -22
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/export.d.ts +57 -0
- package/dist/lib/commands/export.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -235,13 +235,10 @@ ACP provides an `/acp` slash command (also accepts `/dcp` for backward compatibi
|
|
|
235
235
|
|
|
236
236
|
| Command | Description |
|
|
237
237
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
238
|
-
| `/acp` |
|
|
238
|
+
| `/acp` | Show compression status (same as `/acp stats`). Use `/acp help` for the command list |
|
|
239
239
|
| `/acp context` | Token usage breakdown by category (system, user, assistant, tools, etc.) and how much has been saved through pruning |
|
|
240
|
-
| `/acp stats` |
|
|
241
|
-
| `/acp
|
|
242
|
-
| `/acp compress [focus]` | Trigger a single compress tool execution. Optional focus text directs what content to compress, following the active `compress.mode` |
|
|
243
|
-
| `/acp decompress <n>` | Restore a specific active compression by ID. Running without an argument shows available compression IDs, token sizes, and topics |
|
|
244
|
-
| `/acp recompress <n>` | Re-apply a user-decompressed compression by ID. Running without an argument shows recompressible IDs, token sizes, and topics |
|
|
240
|
+
| `/acp stats` | Compression status: blocks, context usage, ranges (same report as the `acp_status` tool) |
|
|
241
|
+
| `/acp export` | Export active compression blocks to a markdown file. Options: `--output <path>`, `--tier t1,t2,t3`, `--stdout`, `--append` |
|
|
245
242
|
|
|
246
243
|
---
|
|
247
244
|
|
|
@@ -493,6 +490,21 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
493
490
|
|
|
494
491
|
## Changelog
|
|
495
492
|
|
|
493
|
+
### v1.14.21 — /acp command fixes and completion (permission gate, export, help) (#286)
|
|
494
|
+
|
|
495
|
+
**Problem**: Three gaps in the `/acp` command suite: (1) when the compress permission was `deny`, ALL `/acp` subcommands were silently swallowed (the gate is a pre-#233 leftover from when `/acp` triggered compression; every current subcommand is read-only or user-initiated); (2) bare `/acp` showed behavior inconsistent with the README and diverged from pi-acp (whose bare `/acp` shows the status report, same handler as `/acp-status`); (3) the requested `/acp export` command (#265) was unimplemented.
|
|
496
|
+
|
|
497
|
+
**Fix**:
|
|
498
|
+
- Removed the compress-permission `deny` gate from the command handler — `/acp` subcommands (context/stats/export) work regardless of compress permission; they are all read-only or user-initiated file writes.
|
|
499
|
+
- Bare `/acp` (and `/dcp`) now shows the compression status report (same as `/acp stats`), aligned with pi-acp; `/acp help` shows the command list.
|
|
500
|
+
- New `/acp export`: exports active compression blocks to markdown (`--output <path>`, `--tier t1,t2,t3`, `--stdout`, `--append`).
|
|
501
|
+
|
|
502
|
+
Files: `lib/hooks.ts`, `lib/commands/export.ts`, `AGENTS.md`, `README.md`, `README.zh-CN.md`. Tests: `tests/hooks-permission.test.ts` updated.
|
|
503
|
+
|
|
504
|
+
Closes: #285, #265.
|
|
505
|
+
|
|
506
|
+
**Install**: `opencode plugin opencode-acp@latest --global`
|
|
507
|
+
|
|
496
508
|
### v1.14.20 — Post-v1.14.19 fix batch (modelContextLimit lifecycle, inactive-block decompress, logging)
|
|
497
509
|
|
|
498
510
|
**Problem**: Five clusters of post-v1.14.19 issues: (1) `modelContextLimit` stayed stale after a model switch (#312) — within one LLM request the messages hook runs before the system hook refreshes the limit, and on a catalog miss (fresh instance + failed hydration) the previous model's window survived, so every percentage threshold misfired (#315); (2) `decompress` failed on standalone inactive blocks and inactive blocks were invisible in `acp_status` (#193); (3) a preemptive `acknowledgeRisk: true` (carried over from a non-quality error) hard-failed with "no quality gate rejection is pending" (#301); (4) debug nudge notifications used `sendIgnoredMessage` causing phantom turns, the debug recommendation-filter log spammed every turn, and ERROR/WARN lines only reached the daily log when debug was on (#311, #278, #279).
|
package/README.zh-CN.md
CHANGED
|
@@ -190,13 +190,10 @@ ACP 提供 `/acp` 斜杠命令(为向后兼容也接受 `/dcp`):
|
|
|
190
190
|
|
|
191
191
|
| 命令 | 说明 |
|
|
192
192
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
193
|
-
| `/acp` |
|
|
193
|
+
| `/acp` | 显示压缩状态(同 `/acp stats`);`/acp help` 查看命令列表 |
|
|
194
194
|
| `/acp context` | 按类别(system、user、assistant、tools 等)显示 token 用量明细,以及通过剪枝节省的量 |
|
|
195
|
-
| `/acp stats` |
|
|
196
|
-
| `/acp
|
|
197
|
-
| `/acp compress [focus]` | 触发一次 `compress` 工具执行。可选的焦点文本指示要压缩的内容,遵循当前 `compress.mode` |
|
|
198
|
-
| `/acp decompress <n>` | 按 ID 恢复特定的活动压缩。不带参数运行时显示可用的压缩 ID、token 大小和主题 |
|
|
199
|
-
| `/acp recompress <n>` | 按 ID 重新应用用户解压的压缩。不带参数运行时显示可重新压缩的 ID、token 大小和主题 |
|
|
195
|
+
| `/acp stats` | 压缩状态:压缩块、上下文用量、推荐范围(与 `acp_status` 工具同一报告) |
|
|
196
|
+
| `/acp export` | 导出活动压缩块到 markdown 文件。选项:`--output <path>`、`--tier t1,t2,t3`、`--stdout`、`--append` |
|
|
200
197
|
|
|
201
198
|
---
|
|
202
199
|
|
|
@@ -447,6 +444,21 @@ ACP 是 DCP 的直接替代品。迁移步骤:
|
|
|
447
444
|
|
|
448
445
|
## 更新日志
|
|
449
446
|
|
|
447
|
+
### v1.14.21 — /acp 命令修复与补全(权限门、export、help)(#286)
|
|
448
|
+
|
|
449
|
+
**问题**:`/acp` 命令族的三个缺口:(1)压缩权限为 `deny` 时,**所有** `/acp` 子命令被静默吞掉(该门是 #233 之前 `/acp` 会触发压缩的残留;现在的子命令全是只读或用户发起的);(2)无参 `/acp` 行为与 README 描述不一致,且与 pi-acp 不对齐(pi-acp 无参 `/acp` 显示状态报告,与 `/acp-status` 同一 handler);(3)需求 `/acp export` 导出命令(#265)未实现。
|
|
450
|
+
|
|
451
|
+
**修复**:
|
|
452
|
+
- 从命令处理器移除压缩权限 `deny` 门——`/acp` 子命令(context/stats/export)现在无论压缩权限如何都能用,它们全部只读或用户发起的文件写入。
|
|
453
|
+
- 无参 `/acp`(和 `/dcp`)现在显示压缩状态报告(同 `/acp stats`),对齐 pi-acp;`/acp help` 显示命令列表。
|
|
454
|
+
- 新增 `/acp export`:将 active 压缩块导出为 markdown(`--output <path>`、`--tier t1,t2,t3`、`--stdout`、`--append`)。
|
|
455
|
+
|
|
456
|
+
文件:`lib/hooks.ts`、`lib/commands/export.ts`、`AGENTS.md`、`README.md`、`README.zh-CN.md`。测试:`tests/hooks-permission.test.ts` 更新。
|
|
457
|
+
|
|
458
|
+
关闭:#285、#265。
|
|
459
|
+
|
|
460
|
+
**安装**:`opencode plugin opencode-acp@latest --global`
|
|
461
|
+
|
|
450
462
|
### v1.14.20 — v1.14.19 之后的修复批次(modelContextLimit 生命周期、inactive block 解压缩、日志)
|
|
451
463
|
|
|
452
464
|
**问题**:五类 v1.14.19 之后发现的问题:(1)`modelContextLimit` 在模型切换后失效(#312)——同一次 LLM 请求内,messages hook 先于 system hook 刷新窗口值,而目录未命中时(新实例 + 水合失败),上一个模型的窗口值会一直残留,导致所有按百分比计算的阈值误触发(#315);(2)`decompress` 对独立的 inactive block 报 `Block not found`,且 inactive block 在 `acp_status` 里完全不可见(#193);(3)预防式 `acknowledgeRisk: true`(从非质量门禁错误里带过来的)直接报错 `no quality gate rejection is pending`(#301);(4)debug nudge 通知用 `sendIgnoredMessage` 触发幽灵回合(#311),debug 推荐过滤日志每回合刷屏,ERROR/WARN 在 debug 关闭时不进日常日志(#311、#278、#279)。
|
package/dist/index.js
CHANGED
|
@@ -2900,14 +2900,14 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
2900
2900
|
block.directMessageIds = [...newlyCompressedMessageIds];
|
|
2901
2901
|
block.directToolIds = [...newlyCompressedToolIds];
|
|
2902
2902
|
block.compressedTokens = compressedTokens;
|
|
2903
|
-
let
|
|
2903
|
+
let effectiveTokens2 = compressedTokens;
|
|
2904
2904
|
for (const consumedBlockId of consumed) {
|
|
2905
2905
|
const cb = messagesState.blocksById.get(consumedBlockId);
|
|
2906
2906
|
if (cb && (cb.tier ?? 1) === targetTierForConsumption) {
|
|
2907
|
-
|
|
2907
|
+
effectiveTokens2 += cb.effectiveCompressedTokens ?? cb.compressedTokens;
|
|
2908
2908
|
}
|
|
2909
2909
|
}
|
|
2910
|
-
block.effectiveCompressedTokens =
|
|
2910
|
+
block.effectiveCompressedTokens = effectiveTokens2;
|
|
2911
2911
|
state.stats.pruneTokenCounter += compressedTokens;
|
|
2912
2912
|
state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
|
|
2913
2913
|
state.stats.pruneTokenCounter = 0;
|
|
@@ -8347,9 +8347,9 @@ function createDecompressTool(factoryCtx) {
|
|
|
8347
8347
|
}
|
|
8348
8348
|
const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
|
|
8349
8349
|
const lines2 = blockMessages.map(extractMessageText2);
|
|
8350
|
-
const { writeFile:
|
|
8350
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
8351
8351
|
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : targets[0]?.blocks[0]?.summary ?? "(no content available)";
|
|
8352
|
-
await
|
|
8352
|
+
await writeFile4(targetPath, fileContent, "utf-8");
|
|
8353
8353
|
const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
|
|
8354
8354
|
return `Block(s) ${displayIds2} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${targetPath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
|
|
8355
8355
|
}
|
|
@@ -9025,7 +9025,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
9025
9025
|
import { join as join3 } from "path";
|
|
9026
9026
|
import { existsSync as existsSync3 } from "fs";
|
|
9027
9027
|
import { homedir as homedir3 } from "os";
|
|
9028
|
-
var LOG_VERSION = true ? "1.14.
|
|
9028
|
+
var LOG_VERSION = true ? "1.14.21-pr.317.34" : "dev";
|
|
9029
9029
|
var Logger = class {
|
|
9030
9030
|
logDir;
|
|
9031
9031
|
enabled;
|
|
@@ -10089,6 +10089,308 @@ ${report}`;
|
|
|
10089
10089
|
);
|
|
10090
10090
|
}
|
|
10091
10091
|
|
|
10092
|
+
// lib/commands/export.ts
|
|
10093
|
+
import * as fs2 from "fs/promises";
|
|
10094
|
+
import { existsSync as existsSync5 } from "fs";
|
|
10095
|
+
import { dirname as dirname3, isAbsolute, join as join5, resolve } from "path";
|
|
10096
|
+
var ALL_TIERS = [1, 2, 3];
|
|
10097
|
+
var TIER_NAMES = {
|
|
10098
|
+
1: "Tier 1 \u2014 Capture",
|
|
10099
|
+
2: "Tier 2 \u2014 Distilled",
|
|
10100
|
+
3: "Tier 3 \u2014 Condensed"
|
|
10101
|
+
};
|
|
10102
|
+
function parseExportArgs(rawArgs) {
|
|
10103
|
+
const options = {
|
|
10104
|
+
outputPath: "",
|
|
10105
|
+
// "" sentinel => default path resolved later
|
|
10106
|
+
tiers: /* @__PURE__ */ new Set(),
|
|
10107
|
+
includeMetadata: true,
|
|
10108
|
+
append: false
|
|
10109
|
+
};
|
|
10110
|
+
const tokens = tokenize2(rawArgs);
|
|
10111
|
+
let i = 0;
|
|
10112
|
+
while (i < tokens.length) {
|
|
10113
|
+
const tok = tokens[i];
|
|
10114
|
+
const [flag, inlineValue] = splitFlag(tok);
|
|
10115
|
+
switch (flag) {
|
|
10116
|
+
case "--output":
|
|
10117
|
+
case "-o": {
|
|
10118
|
+
const value = inlineValue ?? tokens[i + 1];
|
|
10119
|
+
if (value === void 0) {
|
|
10120
|
+
throw new Error("--output requires a path (use `-` for stdout)");
|
|
10121
|
+
}
|
|
10122
|
+
options.outputPath = value;
|
|
10123
|
+
i += inlineValue !== void 0 ? 1 : 2;
|
|
10124
|
+
break;
|
|
10125
|
+
}
|
|
10126
|
+
case "--tier":
|
|
10127
|
+
case "-t": {
|
|
10128
|
+
const value = inlineValue ?? tokens[i + 1];
|
|
10129
|
+
if (value === void 0) {
|
|
10130
|
+
throw new Error("--tier requires a value (e.g. t2,t3 or all)");
|
|
10131
|
+
}
|
|
10132
|
+
options.tiers = parseTiers(value);
|
|
10133
|
+
i += inlineValue !== void 0 ? 1 : 2;
|
|
10134
|
+
break;
|
|
10135
|
+
}
|
|
10136
|
+
case "--no-metadata":
|
|
10137
|
+
if (inlineValue !== void 0) {
|
|
10138
|
+
throw new Error("--no-metadata does not take a value");
|
|
10139
|
+
}
|
|
10140
|
+
options.includeMetadata = false;
|
|
10141
|
+
i += 1;
|
|
10142
|
+
break;
|
|
10143
|
+
case "--metadata":
|
|
10144
|
+
if (inlineValue !== void 0) {
|
|
10145
|
+
throw new Error("--metadata does not take a value");
|
|
10146
|
+
}
|
|
10147
|
+
options.includeMetadata = true;
|
|
10148
|
+
i += 1;
|
|
10149
|
+
break;
|
|
10150
|
+
case "--append":
|
|
10151
|
+
if (inlineValue !== void 0) {
|
|
10152
|
+
throw new Error("--append does not take a value");
|
|
10153
|
+
}
|
|
10154
|
+
options.append = true;
|
|
10155
|
+
i += 1;
|
|
10156
|
+
break;
|
|
10157
|
+
case "--stdout":
|
|
10158
|
+
if (inlineValue !== void 0) {
|
|
10159
|
+
throw new Error("--stdout does not take a value");
|
|
10160
|
+
}
|
|
10161
|
+
options.outputPath = "-";
|
|
10162
|
+
i += 1;
|
|
10163
|
+
break;
|
|
10164
|
+
default:
|
|
10165
|
+
throw new Error(
|
|
10166
|
+
`Unknown flag: ${tok}. Supported: --output, --tier, --no-metadata, --append, --stdout.`
|
|
10167
|
+
);
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
return options;
|
|
10171
|
+
}
|
|
10172
|
+
function tokenize2(input) {
|
|
10173
|
+
const tokens = [];
|
|
10174
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
10175
|
+
let m;
|
|
10176
|
+
while ((m = re.exec(input)) !== null) {
|
|
10177
|
+
tokens.push(m[1] ?? m[2] ?? m[3]);
|
|
10178
|
+
}
|
|
10179
|
+
return tokens;
|
|
10180
|
+
}
|
|
10181
|
+
function splitFlag(tok) {
|
|
10182
|
+
const eq = tok.indexOf("=");
|
|
10183
|
+
if (eq === -1) return [tok, void 0];
|
|
10184
|
+
return [tok.slice(0, eq), tok.slice(eq + 1)];
|
|
10185
|
+
}
|
|
10186
|
+
function parseTiers(raw) {
|
|
10187
|
+
const lower = raw.trim().toLowerCase();
|
|
10188
|
+
if (lower === "all") return new Set(ALL_TIERS);
|
|
10189
|
+
const parts = lower.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
10190
|
+
const result = /* @__PURE__ */ new Set();
|
|
10191
|
+
for (const part of parts) {
|
|
10192
|
+
const digits = part.replace(/^t/i, "");
|
|
10193
|
+
const n = Number(digits);
|
|
10194
|
+
if (!Number.isInteger(n) || n < 1 || n > 3) {
|
|
10195
|
+
throw new Error(
|
|
10196
|
+
`Invalid tier "${part}". Expected t1, t2, t3, a combination (t2,t3), or "all".`
|
|
10197
|
+
);
|
|
10198
|
+
}
|
|
10199
|
+
result.add(n);
|
|
10200
|
+
}
|
|
10201
|
+
if (result.size === 0) {
|
|
10202
|
+
throw new Error(`Invalid tier "${raw}". Expected t1, t2, t3, or "all".`);
|
|
10203
|
+
}
|
|
10204
|
+
return result;
|
|
10205
|
+
}
|
|
10206
|
+
function resolveDefaultOutputPath(sessionId, cwd) {
|
|
10207
|
+
const short = (sessionId || "session").slice(0, 8);
|
|
10208
|
+
return join5(cwd, ".opencode", `acp-export-${short}.md`);
|
|
10209
|
+
}
|
|
10210
|
+
function formatTokens2(n) {
|
|
10211
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
10212
|
+
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
10213
|
+
}
|
|
10214
|
+
function effectiveTokens(block) {
|
|
10215
|
+
return block.effectiveCompressedTokens ?? block.compressedTokens ?? 0;
|
|
10216
|
+
}
|
|
10217
|
+
function collectActiveBlocks(state) {
|
|
10218
|
+
const msgState = state.prune.messages;
|
|
10219
|
+
const blocks = [];
|
|
10220
|
+
for (const id of msgState.activeBlockIds) {
|
|
10221
|
+
const block = msgState.blocksById.get(id);
|
|
10222
|
+
if (block && block.active) {
|
|
10223
|
+
blocks.push(block);
|
|
10224
|
+
}
|
|
10225
|
+
}
|
|
10226
|
+
return blocks;
|
|
10227
|
+
}
|
|
10228
|
+
function filterByTier(blocks, tiers) {
|
|
10229
|
+
if (tiers.size === 0) return blocks;
|
|
10230
|
+
return blocks.filter((b) => tiers.has(b.tier ?? 1));
|
|
10231
|
+
}
|
|
10232
|
+
function tokenSpans(blocks) {
|
|
10233
|
+
return {
|
|
10234
|
+
effective: blocks.reduce((s, b) => s + effectiveTokens(b), 0),
|
|
10235
|
+
summary: blocks.reduce((s, b) => s + (b.summaryTokens || 0), 0)
|
|
10236
|
+
};
|
|
10237
|
+
}
|
|
10238
|
+
function tierCounts(blocks) {
|
|
10239
|
+
const counts = {};
|
|
10240
|
+
for (const b of blocks) {
|
|
10241
|
+
const t = b.tier ?? 1;
|
|
10242
|
+
counts[t] = (counts[t] || 0) + 1;
|
|
10243
|
+
}
|
|
10244
|
+
return ALL_TIERS.filter((t) => counts[t]).map((t) => `T${t}: ${counts[t]}`).join(", ");
|
|
10245
|
+
}
|
|
10246
|
+
function renderExportMarkdown(params) {
|
|
10247
|
+
const { sessionId, generatedAt, blocks, tiers, includeMetadata } = params;
|
|
10248
|
+
const out = [];
|
|
10249
|
+
const tierFilterLabel = tiers.size === 0 ? "all" : ALL_TIERS.filter((t) => tiers.has(t)).map((t) => `T${t}`).join(", ");
|
|
10250
|
+
out.push("# ACP Session Export");
|
|
10251
|
+
out.push("");
|
|
10252
|
+
out.push(`- **Session**: \`${(sessionId || "unknown").slice(0, 16)}\``);
|
|
10253
|
+
out.push(`- **Generated**: ${generatedAt.toISOString()}`);
|
|
10254
|
+
out.push(`- **Blocks exported**: ${blocks.length}`);
|
|
10255
|
+
const breakdown = tierCounts(blocks);
|
|
10256
|
+
if (breakdown) {
|
|
10257
|
+
out.push(` - ${breakdown}`);
|
|
10258
|
+
}
|
|
10259
|
+
out.push(`- **Tiers**: ${tierFilterLabel}`);
|
|
10260
|
+
const span = tokenSpans(blocks);
|
|
10261
|
+
if (blocks.length > 0) {
|
|
10262
|
+
out.push(
|
|
10263
|
+
`- **Coverage**: ${formatTokens2(span.effective)} original \u2192 ${formatTokens2(span.summary)} summary`
|
|
10264
|
+
);
|
|
10265
|
+
}
|
|
10266
|
+
out.push("");
|
|
10267
|
+
out.push("---");
|
|
10268
|
+
out.push("");
|
|
10269
|
+
if (blocks.length === 0) {
|
|
10270
|
+
out.push("_No active compression blocks match the selected tiers._");
|
|
10271
|
+
out.push("");
|
|
10272
|
+
out.push(
|
|
10273
|
+
"Tip: run `/acp export --tier all` to include every active tier, or trigger a compression first."
|
|
10274
|
+
);
|
|
10275
|
+
return out.join("\n");
|
|
10276
|
+
}
|
|
10277
|
+
for (const tier of [3, 2, 1]) {
|
|
10278
|
+
const tierBlocks = blocks.filter((b) => (b.tier ?? 1) === tier).sort((a, b) => a.blockId - b.blockId);
|
|
10279
|
+
if (tierBlocks.length === 0) continue;
|
|
10280
|
+
out.push(`## ${TIER_NAMES[tier]}`);
|
|
10281
|
+
out.push("");
|
|
10282
|
+
for (const block of tierBlocks) {
|
|
10283
|
+
const topic = block.topic || block.batchTopic || "(no topic)";
|
|
10284
|
+
out.push(`### b${block.blockId} \u2014 ${topic}`);
|
|
10285
|
+
out.push("");
|
|
10286
|
+
if (includeMetadata) {
|
|
10287
|
+
const mode = block.mode ?? "range";
|
|
10288
|
+
const msgCount = block.effectiveMessageIds?.length ?? 0;
|
|
10289
|
+
const created = new Date(block.createdAt).toISOString();
|
|
10290
|
+
const age = block.survivedCount ?? 0;
|
|
10291
|
+
out.push(`- **Tier**: T${tier} \xB7 **Mode**: ${mode}`);
|
|
10292
|
+
out.push(`- **Messages**: ${msgCount} (effective)`);
|
|
10293
|
+
out.push(
|
|
10294
|
+
`- **Tokens**: ${formatTokens2(effectiveTokens(block))} \u2192 ${formatTokens2(block.summaryTokens || 0)} summary`
|
|
10295
|
+
);
|
|
10296
|
+
out.push(`- **Created**: ${created}`);
|
|
10297
|
+
out.push(`- **Age**: survived ${age} transform${age === 1 ? "" : "s"}`);
|
|
10298
|
+
out.push("");
|
|
10299
|
+
}
|
|
10300
|
+
const summary = (block.summary || "").trim() || "_(empty summary)_";
|
|
10301
|
+
out.push(summary);
|
|
10302
|
+
out.push("");
|
|
10303
|
+
out.push("---");
|
|
10304
|
+
out.push("");
|
|
10305
|
+
}
|
|
10306
|
+
}
|
|
10307
|
+
return out.join("\n");
|
|
10308
|
+
}
|
|
10309
|
+
async function handleExportCommand(ctx, args) {
|
|
10310
|
+
const { client, state, logger, sessionId } = ctx;
|
|
10311
|
+
let options;
|
|
10312
|
+
try {
|
|
10313
|
+
options = parseExportArgs(args);
|
|
10314
|
+
} catch (err) {
|
|
10315
|
+
const msg = err?.message ?? String(err);
|
|
10316
|
+
logger.warn("export: argument parse failed", { error: msg, args });
|
|
10317
|
+
await sendExportNotice(client, sessionId, ctx, `[ACP Export] ${msg}`);
|
|
10318
|
+
return;
|
|
10319
|
+
}
|
|
10320
|
+
const tiers = options.tiers;
|
|
10321
|
+
const allActive = collectActiveBlocks(state);
|
|
10322
|
+
const blocks = filterByTier(allActive, tiers);
|
|
10323
|
+
const generatedAt = /* @__PURE__ */ new Date();
|
|
10324
|
+
const markdown = renderExportMarkdown({
|
|
10325
|
+
sessionId,
|
|
10326
|
+
generatedAt,
|
|
10327
|
+
blocks,
|
|
10328
|
+
tiers,
|
|
10329
|
+
includeMetadata: options.includeMetadata
|
|
10330
|
+
});
|
|
10331
|
+
if (options.outputPath === "-") {
|
|
10332
|
+
await sendExportNotice(client, sessionId, ctx, markdown);
|
|
10333
|
+
return;
|
|
10334
|
+
}
|
|
10335
|
+
const cwd = ctx.workingDirectory || process.cwd();
|
|
10336
|
+
const targetPath = options.outputPath !== "" ? isAbsolute(options.outputPath) ? options.outputPath : resolve(cwd, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd);
|
|
10337
|
+
try {
|
|
10338
|
+
const dir = dirname3(targetPath);
|
|
10339
|
+
if (!existsSync5(dir)) {
|
|
10340
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
10341
|
+
}
|
|
10342
|
+
const flag = options.append ? "a" : "w";
|
|
10343
|
+
if (options.append && existsSync5(targetPath)) {
|
|
10344
|
+
await fs2.appendFile(targetPath, `
|
|
10345
|
+
|
|
10346
|
+
---
|
|
10347
|
+
|
|
10348
|
+
${markdown}`, "utf-8");
|
|
10349
|
+
} else {
|
|
10350
|
+
await fs2.writeFile(targetPath, markdown, { flag, encoding: "utf-8" });
|
|
10351
|
+
}
|
|
10352
|
+
} catch (err) {
|
|
10353
|
+
const msg = err?.message ?? String(err);
|
|
10354
|
+
logger.warn("export: file write failed", { path: targetPath, error: msg });
|
|
10355
|
+
await sendExportNotice(
|
|
10356
|
+
client,
|
|
10357
|
+
sessionId,
|
|
10358
|
+
ctx,
|
|
10359
|
+
`[ACP Export] Failed to write \`${targetPath}\`: ${msg}`
|
|
10360
|
+
);
|
|
10361
|
+
return;
|
|
10362
|
+
}
|
|
10363
|
+
logger.info("export: wrote markdown", {
|
|
10364
|
+
path: targetPath,
|
|
10365
|
+
blocks: blocks.length,
|
|
10366
|
+
tiers: tiers.size ? [...tiers].sort().join(",") : "all"
|
|
10367
|
+
});
|
|
10368
|
+
const summary = formatExportSummary(targetPath, blocks, allActive, generatedAt);
|
|
10369
|
+
await sendExportNotice(client, sessionId, ctx, summary);
|
|
10370
|
+
}
|
|
10371
|
+
function formatExportSummary(path, exported, allActive, generatedAt) {
|
|
10372
|
+
const lines = [];
|
|
10373
|
+
lines.push("[ACP Export]");
|
|
10374
|
+
lines.push(`Wrote ${exported.length} block${exported.length === 1 ? "" : "s"} to:`);
|
|
10375
|
+
lines.push(` ${path}`);
|
|
10376
|
+
if (exported.length === 0) {
|
|
10377
|
+
lines.push("");
|
|
10378
|
+
lines.push(
|
|
10379
|
+
`No matching blocks (of ${allActive.length} active). Try \`/acp export --tier all\`.`
|
|
10380
|
+
);
|
|
10381
|
+
} else {
|
|
10382
|
+
lines.push("");
|
|
10383
|
+
lines.push(`Generated ${generatedAt.toISOString()}.`);
|
|
10384
|
+
lines.push(
|
|
10385
|
+
"Review, edit, and commit as a devlog / AGENTS.md supplement \u2014 export is user-driven, never auto-injected."
|
|
10386
|
+
);
|
|
10387
|
+
}
|
|
10388
|
+
return lines.join("\n");
|
|
10389
|
+
}
|
|
10390
|
+
async function sendExportNotice(client, sessionId, ctx, text) {
|
|
10391
|
+
await sendIgnoredMessage(client, sessionId, text, {}, ctx.logger);
|
|
10392
|
+
}
|
|
10393
|
+
|
|
10092
10394
|
// lib/messages/filter/registry.ts
|
|
10093
10395
|
var registry3 = /* @__PURE__ */ new Map();
|
|
10094
10396
|
function registerMessageFilter(filter) {
|
|
@@ -10744,6 +11046,20 @@ ${text}`);
|
|
|
10744
11046
|
}
|
|
10745
11047
|
};
|
|
10746
11048
|
}
|
|
11049
|
+
function buildHelpText() {
|
|
11050
|
+
return [
|
|
11051
|
+
"[ACP] Available commands:",
|
|
11052
|
+
"",
|
|
11053
|
+
" /acp Show compression status (same as /acp stats)",
|
|
11054
|
+
" /acp context Token usage breakdown (system, user, assistant, tools)",
|
|
11055
|
+
" /acp stats Compression status: blocks, context usage, ranges",
|
|
11056
|
+
" /acp export Export active compression blocks to markdown",
|
|
11057
|
+
" Options: --output <path>, --tier t1,t2,t3, --stdout, --append",
|
|
11058
|
+
" /acp help Show this help",
|
|
11059
|
+
"",
|
|
11060
|
+
"Also accepts /dcp for backward compatibility."
|
|
11061
|
+
].join("\n");
|
|
11062
|
+
}
|
|
10747
11063
|
function createCommandExecuteHandler(client, registry4, logger, config, workingDirectory, hostPermissions) {
|
|
10748
11064
|
return async (input, output) => {
|
|
10749
11065
|
if (!config.commands.enabled) {
|
|
@@ -10761,23 +11077,35 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
10761
11077
|
config
|
|
10762
11078
|
);
|
|
10763
11079
|
syncCompressPermissionState(state, config, hostPermissions, messages);
|
|
10764
|
-
const effectivePermission = compressPermission(state, config);
|
|
10765
|
-
if (effectivePermission === "deny") {
|
|
10766
|
-
return;
|
|
10767
|
-
}
|
|
10768
11080
|
const commandCtx = {
|
|
10769
11081
|
client,
|
|
10770
11082
|
state,
|
|
10771
11083
|
config,
|
|
10772
11084
|
logger,
|
|
10773
11085
|
sessionId: input.sessionID,
|
|
10774
|
-
messages
|
|
11086
|
+
messages,
|
|
11087
|
+
workingDirectory
|
|
10775
11088
|
};
|
|
10776
11089
|
const sub = input.arguments?.trim().toLowerCase();
|
|
10777
|
-
if (sub === "stats" || sub === "status") {
|
|
11090
|
+
if (sub === "stats" || sub === "status" || sub === "") {
|
|
10778
11091
|
await handleStatsCommand(commandCtx);
|
|
10779
11092
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
10780
11093
|
}
|
|
11094
|
+
if (sub === "export" || sub.startsWith("export ")) {
|
|
11095
|
+
const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
|
|
11096
|
+
await handleExportCommand(commandCtx, exportArgs);
|
|
11097
|
+
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11098
|
+
}
|
|
11099
|
+
if (sub === "help") {
|
|
11100
|
+
await sendIgnoredMessage(
|
|
11101
|
+
client,
|
|
11102
|
+
input.sessionID,
|
|
11103
|
+
buildHelpText(),
|
|
11104
|
+
{},
|
|
11105
|
+
logger
|
|
11106
|
+
);
|
|
11107
|
+
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11108
|
+
}
|
|
10781
11109
|
await handleContextCommand(commandCtx);
|
|
10782
11110
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
10783
11111
|
}
|
|
@@ -10887,7 +11215,7 @@ function configureClientAuth(client) {
|
|
|
10887
11215
|
|
|
10888
11216
|
// lib/update.ts
|
|
10889
11217
|
import { readFile as readFile2, rm } from "fs/promises";
|
|
10890
|
-
import { basename, dirname as
|
|
11218
|
+
import { basename, dirname as dirname4, join as join6 } from "path";
|
|
10891
11219
|
import { fileURLToPath } from "url";
|
|
10892
11220
|
var PACKAGE_NAME = "opencode-acp";
|
|
10893
11221
|
function startAutoUpdate(ctx, enabled) {
|
|
@@ -10912,7 +11240,7 @@ function startAutoUpdate(ctx, enabled) {
|
|
|
10912
11240
|
async function checkAutoUpdate(signal) {
|
|
10913
11241
|
const packageDir = await findPackageDir(PACKAGE_NAME);
|
|
10914
11242
|
if (!packageDir) return { updated: false };
|
|
10915
|
-
const pkg = await readPackageJson(
|
|
11243
|
+
const pkg = await readPackageJson(join6(packageDir, "package.json"));
|
|
10916
11244
|
if (!pkg?.name || !pkg.version) return { updated: false };
|
|
10917
11245
|
const latest = await fetchLatestVersion(pkg.name, signal);
|
|
10918
11246
|
if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false };
|
|
@@ -10932,21 +11260,21 @@ async function checkAutoUpdate(signal) {
|
|
|
10932
11260
|
return { updated: true, name: pkg.name, current: pkg.version, latest };
|
|
10933
11261
|
}
|
|
10934
11262
|
async function findPackageDir(name) {
|
|
10935
|
-
let dir =
|
|
11263
|
+
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
10936
11264
|
for (; ; ) {
|
|
10937
|
-
const pkg = await readPackageJson(
|
|
11265
|
+
const pkg = await readPackageJson(join6(dir, "package.json"));
|
|
10938
11266
|
if (pkg?.name === name) return dir;
|
|
10939
|
-
const parent =
|
|
11267
|
+
const parent = dirname4(dir);
|
|
10940
11268
|
if (parent === dir) return void 0;
|
|
10941
11269
|
dir = parent;
|
|
10942
11270
|
}
|
|
10943
11271
|
}
|
|
10944
11272
|
async function updateRemoveDir(packageDir, name) {
|
|
10945
|
-
const packageParent =
|
|
10946
|
-
const nodeModulesDir = basename(packageParent).startsWith("@") ?
|
|
11273
|
+
const packageParent = dirname4(packageDir);
|
|
11274
|
+
const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
|
|
10947
11275
|
if (basename(nodeModulesDir) !== "node_modules") return void 0;
|
|
10948
|
-
const wrapperDir =
|
|
10949
|
-
const wrapperPkg = await readPackageJson(
|
|
11276
|
+
const wrapperDir = dirname4(nodeModulesDir);
|
|
11277
|
+
const wrapperPkg = await readPackageJson(join6(wrapperDir, "package.json"));
|
|
10950
11278
|
const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
|
|
10951
11279
|
if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
|
|
10952
11280
|
return wrapperDir;
|
|
@@ -10954,7 +11282,7 @@ async function updateRemoveDir(packageDir, name) {
|
|
|
10954
11282
|
function wrapperSpec(wrapperDir, name) {
|
|
10955
11283
|
if (name.startsWith("@")) {
|
|
10956
11284
|
const [scope, pkg] = name.split("/");
|
|
10957
|
-
if (!scope || !pkg || basename(
|
|
11285
|
+
if (!scope || !pkg || basename(dirname4(wrapperDir)) !== scope) return void 0;
|
|
10958
11286
|
const prefix2 = `${pkg}@`;
|
|
10959
11287
|
const base2 = basename(wrapperDir);
|
|
10960
11288
|
return base2.startsWith(prefix2) ? base2.slice(prefix2.length) : void 0;
|