opencode-acp 1.14.20 → 1.14.21-pr.320.35

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/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.320.35" : "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;