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 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` | Shows available ACP commands |
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` | Cumulative pruning statistics across all sessions |
241
- | `/acp manual [on\|off]` | Toggle manual mode. When on, the AI will not autonomously use context management tools |
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` | 显示可用的 ACP 命令 |
193
+ | `/acp` | 显示压缩状态(同 `/acp stats`);`/acp help` 查看命令列表 |
194
194
  | `/acp context` | 按类别(system、user、assistant、tools 等)显示 token 用量明细,以及通过剪枝节省的量 |
195
- | `/acp stats` | 跨所有会话的累计剪枝统计 |
196
- | `/acp manual [on\|off]` | 切换手动模式。开启后,AI 不会自动使用上下文管理工具 |
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 effectiveTokens = compressedTokens;
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
- effectiveTokens += cb.effectiveCompressedTokens ?? cb.compressedTokens;
2907
+ effectiveTokens2 += cb.effectiveCompressedTokens ?? cb.compressedTokens;
2908
2908
  }
2909
2909
  }
2910
- block.effectiveCompressedTokens = effectiveTokens;
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: writeFile3 } = await import("fs/promises");
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 writeFile3(targetPath, fileContent, "utf-8");
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.20" : "dev";
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 dirname3, join as join5 } from "path";
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(join5(packageDir, "package.json"));
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 = dirname3(fileURLToPath(import.meta.url));
11263
+ let dir = dirname4(fileURLToPath(import.meta.url));
10936
11264
  for (; ; ) {
10937
- const pkg = await readPackageJson(join5(dir, "package.json"));
11265
+ const pkg = await readPackageJson(join6(dir, "package.json"));
10938
11266
  if (pkg?.name === name) return dir;
10939
- const parent = dirname3(dir);
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 = dirname3(packageDir);
10946
- const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname3(packageParent) : packageParent;
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 = dirname3(nodeModulesDir);
10949
- const wrapperPkg = await readPackageJson(join5(wrapperDir, "package.json"));
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(dirname3(wrapperDir)) !== scope) return void 0;
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;