acryl-cli-linux-x64 0.1.31 → 0.1.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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/runtime/lib/bin.js +831 -220
  3. package/runtime/lib/index.js +831 -220
  4. package/runtime/lib/types/buildInfo.generated.d.ts +4 -0
  5. package/runtime/lib/types/tui/acrylMark.d.ts +14 -0
  6. package/runtime/lib/types/tui/actions.d.ts +8 -6
  7. package/runtime/lib/types/tui/auth-guidance.d.ts +0 -2
  8. package/runtime/lib/types/tui/login/LoginOverlay.d.ts +46 -11
  9. package/runtime/lib/types/tui/login/types.d.ts +5 -1
  10. package/runtime/lib/types/tui/modelProfile/ModelProfileOverlay.d.ts +55 -3
  11. package/runtime/lib/types/tui/modelProfile/types.d.ts +23 -0
  12. package/runtime/lib/types/tui/store.d.ts +10 -0
  13. package/runtime/node_modules/.modules.yaml +2 -2
  14. package/runtime/node_modules/.package-map.json +1 -1
  15. package/runtime/node_modules/@earendil-works/pi-ai/dist/auth/oauth/oauth-page.js +1 -1
  16. package/runtime/node_modules/acryl-control/package.json +1 -1
  17. package/runtime/node_modules/acryl-harness-runtime/lib/index.mjs +129 -2
  18. package/runtime/node_modules/acryl-harness-runtime/lib/types/acryl-home.d.ts +35 -0
  19. package/runtime/node_modules/acryl-harness-runtime/lib/types/index.d.ts +2 -0
  20. package/runtime/node_modules/acryl-harness-runtime/lib/types/session-bridge.d.ts +13 -0
  21. package/runtime/node_modules/acryl-harness-runtime/lib/types/session-log-exporter.d.ts +31 -0
  22. package/runtime/node_modules/acryl-harness-runtime/package.json +1 -1
  23. package/runtime/node_modules/acryl-harness-runtime/src/acryl-home.ts +50 -0
  24. package/runtime/node_modules/acryl-harness-runtime/src/index.ts +16 -0
  25. package/runtime/node_modules/acryl-harness-runtime/src/session-bridge.ts +24 -1
  26. package/runtime/node_modules/acryl-harness-runtime/src/session-log-exporter.ts +69 -0
  27. package/runtime/package.json +3 -1
  28. package/runtime/receipt.json +2 -2
@@ -1,4 +1,4 @@
1
- const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>`;
1
+ const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><g fill="#fff"><rect x="76.0" y="418.0" width="10.5" height="10.5"/><rect x="88.0" y="394.0" width="10.5" height="10.5"/><rect x="88.0" y="406.0" width="10.5" height="10.5"/><rect x="88.0" y="418.0" width="10.5" height="10.5"/><rect x="100.0" y="382.0" width="10.5" height="10.5"/><rect x="100.0" y="394.0" width="10.5" height="10.5"/><rect x="112.0" y="370.0" width="10.5" height="10.5"/><rect x="112.0" y="382.0" width="10.5" height="10.5"/><rect x="124.0" y="358.0" width="10.5" height="10.5"/><rect x="124.0" y="370.0" width="10.5" height="10.5"/><rect x="136.0" y="346.0" width="10.5" height="10.5"/><rect x="136.0" y="358.0" width="10.5" height="10.5"/><rect x="148.0" y="334.0" width="10.5" height="10.5"/><rect x="148.0" y="346.0" width="10.5" height="10.5"/><rect x="160.0" y="334.0" width="10.5" height="10.5"/><rect x="160.0" y="346.0" width="10.5" height="10.5"/><rect x="160.0" y="358.0" width="10.5" height="10.5"/><rect x="172.0" y="358.0" width="10.5" height="10.5"/><rect x="184.0" y="370.0" width="10.5" height="10.5"/><rect x="196.0" y="370.0" width="10.5" height="10.5"/><rect x="196.0" y="382.0" width="10.5" height="10.5"/><rect x="196.0" y="394.0" width="10.5" height="10.5"/><rect x="208.0" y="370.0" width="10.5" height="10.5"/><rect x="208.0" y="406.0" width="10.5" height="10.5"/><rect x="220.0" y="370.0" width="10.5" height="10.5"/><rect x="220.0" y="418.0" width="10.5" height="10.5"/><rect x="232.0" y="370.0" width="10.5" height="10.5"/><rect x="244.0" y="370.0" width="10.5" height="10.5"/><rect x="256.0" y="358.0" width="10.5" height="10.5"/><rect x="256.0" y="370.0" width="10.5" height="10.5"/><rect x="256.0" y="382.0" width="10.5" height="10.5"/><rect x="268.0" y="346.0" width="10.5" height="10.5"/><rect x="268.0" y="382.0" width="10.5" height="10.5"/><rect x="268.0" y="394.0" width="10.5" height="10.5"/><rect x="280.0" y="334.0" width="10.5" height="10.5"/><rect x="280.0" y="394.0" width="10.5" height="10.5"/><rect x="280.0" y="406.0" width="10.5" height="10.5"/><rect x="280.0" y="418.0" width="10.5" height="10.5"/><rect x="292.0" y="310.0" width="10.5" height="10.5"/><rect x="292.0" y="418.0" width="10.5" height="10.5"/><rect x="292.0" y="430.0" width="10.5" height="10.5"/><rect x="304.0" y="298.0" width="10.5" height="10.5"/><rect x="304.0" y="430.0" width="10.5" height="10.5"/><rect x="304.0" y="442.0" width="10.5" height="10.5"/><rect x="316.0" y="286.0" width="10.5" height="10.5"/><rect x="316.0" y="442.0" width="10.5" height="10.5"/><rect x="328.0" y="274.0" width="10.5" height="10.5"/><rect x="328.0" y="442.0" width="10.5" height="10.5"/><rect x="340.0" y="274.0" width="10.5" height="10.5"/><rect x="340.0" y="442.0" width="10.5" height="10.5"/><rect x="352.0" y="274.0" width="10.5" height="10.5"/><rect x="352.0" y="310.0" width="10.5" height="10.5"/><rect x="352.0" y="442.0" width="10.5" height="10.5"/><rect x="364.0" y="274.0" width="10.5" height="10.5"/><rect x="364.0" y="310.0" width="10.5" height="10.5"/><rect x="364.0" y="442.0" width="10.5" height="10.5"/><rect x="376.0" y="274.0" width="10.5" height="10.5"/><rect x="376.0" y="310.0" width="10.5" height="10.5"/><rect x="376.0" y="442.0" width="10.5" height="10.5"/><rect x="388.0" y="274.0" width="10.5" height="10.5"/><rect x="388.0" y="310.0" width="10.5" height="10.5"/><rect x="388.0" y="358.0" width="10.5" height="10.5"/><rect x="388.0" y="442.0" width="10.5" height="10.5"/><rect x="400.0" y="274.0" width="10.5" height="10.5"/><rect x="400.0" y="310.0" width="10.5" height="10.5"/><rect x="400.0" y="358.0" width="10.5" height="10.5"/><rect x="400.0" y="370.0" width="10.5" height="10.5"/><rect x="400.0" y="442.0" width="10.5" height="10.5"/><rect x="412.0" y="274.0" width="10.5" height="10.5"/><rect x="412.0" y="310.0" width="10.5" height="10.5"/><rect x="412.0" y="358.0" width="10.5" height="10.5"/><rect x="412.0" y="382.0" width="10.5" height="10.5"/><rect x="412.0" y="394.0" width="10.5" height="10.5"/><rect x="412.0" y="442.0" width="10.5" height="10.5"/><rect x="424.0" y="310.0" width="10.5" height="10.5"/><rect x="424.0" y="358.0" width="10.5" height="10.5"/><rect x="424.0" y="406.0" width="10.5" height="10.5"/><rect x="424.0" y="442.0" width="10.5" height="10.5"/><rect x="436.0" y="310.0" width="10.5" height="10.5"/><rect x="436.0" y="358.0" width="10.5" height="10.5"/><rect x="436.0" y="406.0" width="10.5" height="10.5"/><rect x="448.0" y="310.0" width="10.5" height="10.5"/><rect x="448.0" y="322.0" width="10.5" height="10.5"/><rect x="448.0" y="346.0" width="10.5" height="10.5"/><rect x="448.0" y="358.0" width="10.5" height="10.5"/><rect x="448.0" y="430.0" width="10.5" height="10.5"/><rect x="460.0" y="334.0" width="10.5" height="10.5"/><rect x="460.0" y="442.0" width="10.5" height="10.5"/><rect x="472.0" y="442.0" width="10.5" height="10.5"/><rect x="484.0" y="454.0" width="10.5" height="10.5"/><rect x="484.0" y="466.0" width="10.5" height="10.5"/><rect x="496.0" y="478.0" width="10.5" height="10.5"/><rect x="508.0" y="478.0" width="10.5" height="10.5"/><rect x="508.0" y="490.0" width="10.5" height="10.5"/><rect x="508.0" y="502.0" width="10.5" height="10.5"/><rect x="508.0" y="514.0" width="10.5" height="10.5"/><rect x="520.0" y="478.0" width="10.5" height="10.5"/><rect x="532.0" y="454.0" width="10.5" height="10.5"/><rect x="532.0" y="466.0" width="10.5" height="10.5"/><rect x="544.0" y="442.0" width="10.5" height="10.5"/><rect x="556.0" y="442.0" width="10.5" height="10.5"/><rect x="568.0" y="430.0" width="10.5" height="10.5"/><rect x="580.0" y="418.0" width="10.5" height="10.5"/><rect x="580.0" y="430.0" width="10.5" height="10.5"/><rect x="592.0" y="406.0" width="10.5" height="10.5"/><rect x="592.0" y="418.0" width="10.5" height="10.5"/><rect x="592.0" y="430.0" width="10.5" height="10.5"/><rect x="604.0" y="394.0" width="10.5" height="10.5"/><rect x="604.0" y="406.0" width="10.5" height="10.5"/><rect x="604.0" y="430.0" width="10.5" height="10.5"/><rect x="616.0" y="382.0" width="10.5" height="10.5"/><rect x="616.0" y="430.0" width="10.5" height="10.5"/><rect x="628.0" y="370.0" width="10.5" height="10.5"/><rect x="628.0" y="430.0" width="10.5" height="10.5"/><rect x="640.0" y="430.0" width="10.5" height="10.5"/><rect x="652.0" y="430.0" width="10.5" height="10.5"/><rect x="664.0" y="430.0" width="10.5" height="10.5"/><rect x="676.0" y="430.0" width="10.5" height="10.5"/><rect x="688.0" y="430.0" width="10.5" height="10.5"/><rect x="700.0" y="430.0" width="10.5" height="10.5"/><rect x="712.0" y="430.0" width="10.5" height="10.5"/></g></svg>`;
2
2
  function escapeHtml(value) {
3
3
  return value
4
4
  .replaceAll("&", "&amp;")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acryl-control",
3
- "version": "0.1.31",
3
+ "version": "0.1.35",
4
4
  "private": false,
5
5
  "description": "Host-neutral Cordis control plane for ACRYL",
6
6
  "license": "MIT",
@@ -1,9 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
- import { existsSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
5
6
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
6
7
  import { SessionId } from "@deepseek-ai/dsh-session";
8
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
+ import { homedir } from "node:os";
7
10
  import { provideCmdline } from "@deepseek-ai/dsh-cmdline";
8
11
  import { DEFAULT_PROFILE_BUNDLES, boot, composeEntries, healProfilesModuleFallback, initProfile, loadProfile, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
9
12
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -114,6 +117,8 @@ function status(agent) {
114
117
  */
115
118
  function createAcrylSessionBridge(ctx, options) {
116
119
  const handles = /* @__PURE__ */ new Map();
120
+ const modelSelections = /* @__PURE__ */ new Map();
121
+ const modelSelectionDisposers = /* @__PURE__ */ new Map();
117
122
  const subscribers = /* @__PURE__ */ new Map();
118
123
  const eventListeners = /* @__PURE__ */ new Map();
119
124
  let disposed = false;
@@ -175,6 +180,12 @@ function createAcrylSessionBridge(ctx, options) {
175
180
  }
176
181
  });
177
182
  handles.set(handle.agent.id, handle);
183
+ const ref = {
184
+ current: void 0,
185
+ assembled: void 0
186
+ };
187
+ modelSelectionDisposers.set(handle.agent.id, installModelSelection(handle.agent.ctx, ref));
188
+ modelSelections.set(handle.agent.id, ref);
178
189
  return handle.agent.id;
179
190
  },
180
191
  snapshot,
@@ -234,6 +245,15 @@ function createAcrylSessionBridge(ctx, options) {
234
245
  }));
235
246
  await accepted;
236
247
  },
248
+ async selectModel(input) {
249
+ agentFor(input.sessionId);
250
+ const ref = modelSelections.get(input.sessionId);
251
+ if (ref === void 0) throw new Error(`ACRYL session ${input.sessionId} has no installed model selection`);
252
+ ref.current = {
253
+ provider: input.provider,
254
+ model: input.model
255
+ };
256
+ },
237
257
  async cancel(sessionId) {
238
258
  agentFor(sessionId).cancel({ kind: "user" });
239
259
  },
@@ -243,6 +263,9 @@ function createAcrylSessionBridge(ctx, options) {
243
263
  offSessionEvent();
244
264
  subscribers.clear();
245
265
  eventListeners.clear();
266
+ for (const dispose of modelSelectionDisposers.values()) dispose();
267
+ modelSelectionDisposers.clear();
268
+ modelSelections.clear();
246
269
  const activeHandles = [...handles.values()];
247
270
  handles.clear();
248
271
  const sessions = ctx.get("sessions");
@@ -259,6 +282,106 @@ function createAcrylSessionBridge(ctx, options) {
259
282
  });
260
283
  }
261
284
  //#endregion
285
+ //#region src/session-log-exporter.ts
286
+ /**
287
+ * A durable, cross-surface debug log for ACRYL sessions.
288
+ *
289
+ * Cordis's own `ctx.logger` already carries every structured log record any
290
+ * plugin emits, but nothing in this stack persisted it anywhere — a failure
291
+ * like a vendored provider's OAuth error only ever reached the terminal
292
+ * transcript, gone the moment the pane scrolled or the process exited. This
293
+ * registers one JSONL file exporter on `ctx.logger` per surface (CLI/TUI,
294
+ * web, and — once wired there too — the desktop GUI), so `error`/`warn`
295
+ * records (and everything, when `ACRYL_LOG_LEVEL=debug`) survive the session
296
+ * and can be read back after the fact instead of re-derived from a
297
+ * screenshot.
298
+ *
299
+ * @module acryl-harness-runtime/session-log-exporter
300
+ */
301
+ const LOGGER_LEVEL_WARN = 2;
302
+ const LOGGER_LEVEL_DEBUG = 3;
303
+ /**
304
+ * Register the file exporter for this process's lifetime. `ctx.logger.exporter()`
305
+ * already ties its own disposal to the fiber that registers it (see
306
+ * `LoggerService.exporter` in `@deepseek-ai/cordis`), so this needs no extra
307
+ * `ctx.effect()` wrapper of its own.
308
+ * @param ctx - the plugin context whose `ctx.logger` gains the exporter.
309
+ * @param options - which surface/home this log file belongs to.
310
+ */
311
+ function installSessionLogExporter(ctx, options) {
312
+ const logDir = join(resolveDshHome(options.dshHome), "logs");
313
+ try {
314
+ mkdirSync(logDir, {
315
+ recursive: true,
316
+ mode: 448
317
+ });
318
+ } catch {
319
+ return;
320
+ }
321
+ const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
322
+ const logFile = join(logDir, `acryl-${options.surface}-${day}.jsonl`);
323
+ const threshold = process.env.ACRYL_LOG_LEVEL === "debug" ? LOGGER_LEVEL_DEBUG : LOGGER_LEVEL_WARN;
324
+ ctx.logger.exporter({ export(message) {
325
+ if (message.level > threshold) return;
326
+ try {
327
+ appendFileSync(logFile, `${JSON.stringify({
328
+ ts: new Date(message.ts).toISOString(),
329
+ type: message.type,
330
+ name: message.name,
331
+ args: message.args.map((arg) => arg instanceof Error ? {
332
+ message: arg.message,
333
+ stack: arg.stack,
334
+ name: arg.name
335
+ } : arg)
336
+ })}\n`, { mode: 384 });
337
+ } catch {}
338
+ } });
339
+ }
340
+ //#endregion
341
+ //#region src/acryl-home.ts
342
+ /**
343
+ * ACRYL's own data root, nested one level above the DSH engine home it
344
+ * composes.
345
+ *
346
+ * The stock DSH Desktop app (from dshdesktop.com) uses plain `~/.dsh`, and so
347
+ * did every ACRYL surface until now — sharing that root made the two
348
+ * genuinely different products silently share credentials/settings/sessions,
349
+ * which is both confusing and useless for comparing ACRYL against a stock
350
+ * DSH Desktop install side by side. ACRYL now owns `~/.acryl` and nests each
351
+ * engine's artifacts under it by engine name:
352
+ *
353
+ * ```txt
354
+ * ~/.acryl/ ACRYL's own root — everything not engine-specific
355
+ * ~/.acryl/.dsh/ the DSH engine home (what DSH_HOME resolves to)
356
+ * ~/.acryl/.pi/ reserved for a future pi.dev engine
357
+ * ```
358
+ *
359
+ * `$DSH_HOME`, if a caller has already set it explicitly, still wins — this
360
+ * only changes the *default* the harness's own `resolveDshHome()` falls
361
+ * back to when nothing overrides it.
362
+ *
363
+ * @module acryl-harness-runtime/acryl-home
364
+ */
365
+ /** ACRYL's own root directory name under the OS home. */
366
+ const ACRYL_HOME_DIR_NAME = ".acryl";
367
+ /** The DSH engine's directory name, nested under ACRYL's root. */
368
+ const ACRYL_DSH_ENGINE_DIR_NAME = ".dsh";
369
+ /** ACRYL's own root — `~/.acryl` unless `$ACRYL_HOME` overrides it. */
370
+ function resolveAcrylHome(env = process.env) {
371
+ const overridden = env.ACRYL_HOME?.trim();
372
+ return overridden && overridden !== "" ? overridden : join(homedir(), ACRYL_HOME_DIR_NAME);
373
+ }
374
+ /**
375
+ * The DSH engine home ACRYL boots against: `$DSH_HOME` if a caller already
376
+ * set it (highest precedence, same as `resolveDshHome()` itself), otherwise
377
+ * `<acrylHome>/.dsh` instead of the harness's own bare `~/.dsh` default.
378
+ */
379
+ function resolveAcrylDshHome(env = process.env) {
380
+ const overridden = env.DSH_HOME?.trim();
381
+ if (overridden && overridden !== "") return resolveDshHome(void 0, env);
382
+ return resolveDshHome(join(resolveAcrylHome(env), ACRYL_DSH_ENGINE_DIR_NAME), env);
383
+ }
384
+ //#endregion
262
385
  //#region src/plugin-acryl-workspace-status.ts
263
386
  /** Canonical tool name exposed to the model. */
264
387
  const TOOL_NAME = "acryl_workspace_status";
@@ -330,6 +453,7 @@ const profileRoot = "[]\n";
330
453
  /** Boot one normal pinned-Harness ACRYL profile in a single Cordis root. */
331
454
  async function bootAcrylHarnessProfile(options) {
332
455
  if (options.profile.trim() === "") throw new Error("ACRYL Harness profile must not be empty");
456
+ process.env.DSH_HOME = resolveAcrylDshHome();
333
457
  initProfile(resolveProfileDir(options.profile), DEFAULT_PROFILE_BUNDLES);
334
458
  healProfilesModuleFallback(dshInstallAnchor);
335
459
  const profile = loadProfile("acryl", options.profile, dshInstallAnchor);
@@ -343,6 +467,7 @@ async function bootAcrylHarnessProfile(options) {
343
467
  if (composeEntries([patches]).find((entry) => entry.id === "hmr")?.disabled !== true && !process.execArgv.includes("--expose-internals")) throw new Error("ACRYL profile enables Cordis HMR and must be launched with Node --expose-internals");
344
468
  const ctx = await boot("acryl", rootConfig, patches, options.prepare);
345
469
  if (ctx.tools) installAcrylWorkspaceStatusTool(ctx);
470
+ installSessionLogExporter(ctx, { surface: "tui" });
346
471
  let disposed = false;
347
472
  return Object.freeze({
348
473
  ctx,
@@ -363,6 +488,7 @@ async function bootAcrylHarnessProfile(options) {
363
488
  */
364
489
  async function bootAcrylWebProfile(options = {}) {
365
490
  const profileName = "web";
491
+ process.env.DSH_HOME = resolveAcrylDshHome();
366
492
  initProfile(resolveProfileDir(profileName), DEFAULT_PROFILE_BUNDLES);
367
493
  healProfilesModuleFallback(dshInstallAnchor);
368
494
  const profile = loadProfile("web", profileName, dshInstallAnchor);
@@ -384,6 +510,7 @@ async function bootAcrylWebProfile(options = {}) {
384
510
  return options.prepare?.(hostCtx);
385
511
  });
386
512
  if (ctx.tools) installAcrylWorkspaceStatusTool(ctx);
513
+ installSessionLogExporter(ctx, { surface: "web" });
387
514
  const startup = ctx.get("webStartup");
388
515
  const url = `http://${startup?.host ?? "127.0.0.1"}:${startup?.port ?? 3080}`;
389
516
  let disposed = false;
@@ -399,6 +526,6 @@ async function bootAcrylWebProfile(options = {}) {
399
526
  });
400
527
  }
401
528
  //#endregion
402
- export { ACRYL_CODING_CAPABILITIES, bootAcrylHarnessProfile, bootAcrylWebProfile, createAcrylCodingCapabilityPatches, createAcrylSessionBridge };
529
+ export { ACRYL_CODING_CAPABILITIES, ACRYL_DSH_ENGINE_DIR_NAME, ACRYL_HOME_DIR_NAME, bootAcrylHarnessProfile, bootAcrylWebProfile, createAcrylCodingCapabilityPatches, createAcrylSessionBridge, installSessionLogExporter, resolveAcrylDshHome, resolveAcrylHome };
403
530
 
404
531
  //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ACRYL's own data root, nested one level above the DSH engine home it
3
+ * composes.
4
+ *
5
+ * The stock DSH Desktop app (from dshdesktop.com) uses plain `~/.dsh`, and so
6
+ * did every ACRYL surface until now — sharing that root made the two
7
+ * genuinely different products silently share credentials/settings/sessions,
8
+ * which is both confusing and useless for comparing ACRYL against a stock
9
+ * DSH Desktop install side by side. ACRYL now owns `~/.acryl` and nests each
10
+ * engine's artifacts under it by engine name:
11
+ *
12
+ * ```txt
13
+ * ~/.acryl/ ACRYL's own root — everything not engine-specific
14
+ * ~/.acryl/.dsh/ the DSH engine home (what DSH_HOME resolves to)
15
+ * ~/.acryl/.pi/ reserved for a future pi.dev engine
16
+ * ```
17
+ *
18
+ * `$DSH_HOME`, if a caller has already set it explicitly, still wins — this
19
+ * only changes the *default* the harness's own `resolveDshHome()` falls
20
+ * back to when nothing overrides it.
21
+ *
22
+ * @module acryl-harness-runtime/acryl-home
23
+ */
24
+ /** ACRYL's own root directory name under the OS home. */
25
+ export declare const ACRYL_HOME_DIR_NAME = ".acryl";
26
+ /** The DSH engine's directory name, nested under ACRYL's root. */
27
+ export declare const ACRYL_DSH_ENGINE_DIR_NAME = ".dsh";
28
+ /** ACRYL's own root — `~/.acryl` unless `$ACRYL_HOME` overrides it. */
29
+ export declare function resolveAcrylHome(env?: Record<string, string | undefined>): string;
30
+ /**
31
+ * The DSH engine home ACRYL boots against: `$DSH_HOME` if a caller already
32
+ * set it (highest precedence, same as `resolveDshHome()` itself), otherwise
33
+ * `<acrylHome>/.dsh` instead of the harness's own bare `~/.dsh` default.
34
+ */
35
+ export declare function resolveAcrylDshHome(env?: Record<string, string | undefined>): string;
@@ -1,6 +1,8 @@
1
1
  export { ACRYL_CODING_CAPABILITIES, createAcrylCodingCapabilityPatches, type AcrylCodingCapability, type AcrylSurface, } from './coding-capabilities.ts';
2
2
  export type { DurableSessionMessage, DurableSessionMessagePort, DurableSessionMessageReceipt, } from './durable-message.ts';
3
3
  export { createAcrylSessionBridge, type AcrylSessionBridge, type AcrylSessionBridgeOptions, } from './session-bridge.ts';
4
+ export { installSessionLogExporter, type InstallSessionLogExporterOptions, } from './session-log-exporter.ts';
5
+ export { ACRYL_DSH_ENGINE_DIR_NAME, ACRYL_HOME_DIR_NAME, resolveAcrylDshHome, resolveAcrylHome, } from './acryl-home.ts';
4
6
  import type { Context } from '@deepseek-ai/cordis';
5
7
  export interface BootAcrylHarnessProfileOptions {
6
8
  readonly profile: string;
@@ -22,6 +22,19 @@ export interface AcrylSessionBridge {
22
22
  readonly sessionId: string;
23
23
  readonly text: string;
24
24
  }): Promise<void>;
25
+ /**
26
+ * Switch an already-open session's live model. `agentOptions.provider`/`model`
27
+ * are a one-time construction input to `ctx.agents.create` — not a live
28
+ * setting — so this is the only thing that changes what a running session
29
+ * sends its next request to (`ModelSelectionRef` installed on the agent's
30
+ * own scoped context via `installModelSelection`, per-step prompt assembly
31
+ * reads it fresh).
32
+ */
33
+ selectModel(input: {
34
+ readonly sessionId: string;
35
+ readonly provider: string;
36
+ readonly model: string;
37
+ }): Promise<void>;
25
38
  cancel(sessionId: string): Promise<void>;
26
39
  dispose(): Promise<void>;
27
40
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A durable, cross-surface debug log for ACRYL sessions.
3
+ *
4
+ * Cordis's own `ctx.logger` already carries every structured log record any
5
+ * plugin emits, but nothing in this stack persisted it anywhere — a failure
6
+ * like a vendored provider's OAuth error only ever reached the terminal
7
+ * transcript, gone the moment the pane scrolled or the process exited. This
8
+ * registers one JSONL file exporter on `ctx.logger` per surface (CLI/TUI,
9
+ * web, and — once wired there too — the desktop GUI), so `error`/`warn`
10
+ * records (and everything, when `ACRYL_LOG_LEVEL=debug`) survive the session
11
+ * and can be read back after the fact instead of re-derived from a
12
+ * screenshot.
13
+ *
14
+ * @module acryl-harness-runtime/session-log-exporter
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ export interface InstallSessionLogExporterOptions {
18
+ /** Which ACRYL surface this process is (`tui`, `web`, `desktop`) — becomes part of the log filename. */
19
+ readonly surface: string;
20
+ /** Harness home override; defaults to `$DSH_HOME`/`~/.dsh` exactly like credentials/settings resolve it. */
21
+ readonly dshHome?: string;
22
+ }
23
+ /**
24
+ * Register the file exporter for this process's lifetime. `ctx.logger.exporter()`
25
+ * already ties its own disposal to the fiber that registers it (see
26
+ * `LoggerService.exporter` in `@deepseek-ai/cordis`), so this needs no extra
27
+ * `ctx.effect()` wrapper of its own.
28
+ * @param ctx - the plugin context whose `ctx.logger` gains the exporter.
29
+ * @param options - which surface/home this log file belongs to.
30
+ */
31
+ export declare function installSessionLogExporter(ctx: Context, options: InstallSessionLogExporterOptions): void;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acryl-harness-runtime",
3
- "version": "0.1.31",
3
+ "version": "0.1.35",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ACRYL's own data root, nested one level above the DSH engine home it
3
+ * composes.
4
+ *
5
+ * The stock DSH Desktop app (from dshdesktop.com) uses plain `~/.dsh`, and so
6
+ * did every ACRYL surface until now — sharing that root made the two
7
+ * genuinely different products silently share credentials/settings/sessions,
8
+ * which is both confusing and useless for comparing ACRYL against a stock
9
+ * DSH Desktop install side by side. ACRYL now owns `~/.acryl` and nests each
10
+ * engine's artifacts under it by engine name:
11
+ *
12
+ * ```txt
13
+ * ~/.acryl/ ACRYL's own root — everything not engine-specific
14
+ * ~/.acryl/.dsh/ the DSH engine home (what DSH_HOME resolves to)
15
+ * ~/.acryl/.pi/ reserved for a future pi.dev engine
16
+ * ```
17
+ *
18
+ * `$DSH_HOME`, if a caller has already set it explicitly, still wins — this
19
+ * only changes the *default* the harness's own `resolveDshHome()` falls
20
+ * back to when nothing overrides it.
21
+ *
22
+ * @module acryl-harness-runtime/acryl-home
23
+ */
24
+
25
+ import { homedir } from 'node:os'
26
+ import { join } from 'node:path'
27
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
28
+
29
+ /** ACRYL's own root directory name under the OS home. */
30
+ export const ACRYL_HOME_DIR_NAME = '.acryl'
31
+
32
+ /** The DSH engine's directory name, nested under ACRYL's root. */
33
+ export const ACRYL_DSH_ENGINE_DIR_NAME = '.dsh'
34
+
35
+ /** ACRYL's own root — `~/.acryl` unless `$ACRYL_HOME` overrides it. */
36
+ export function resolveAcrylHome(env: Record<string, string | undefined> = process.env): string {
37
+ const overridden = env.ACRYL_HOME?.trim()
38
+ return overridden && overridden !== '' ? overridden : join(homedir(), ACRYL_HOME_DIR_NAME)
39
+ }
40
+
41
+ /**
42
+ * The DSH engine home ACRYL boots against: `$DSH_HOME` if a caller already
43
+ * set it (highest precedence, same as `resolveDshHome()` itself), otherwise
44
+ * `<acrylHome>/.dsh` instead of the harness's own bare `~/.dsh` default.
45
+ */
46
+ export function resolveAcrylDshHome(env: Record<string, string | undefined> = process.env): string {
47
+ const overridden = env.DSH_HOME?.trim()
48
+ if (overridden && overridden !== '') return resolveDshHome(undefined, env)
49
+ return resolveDshHome(join(resolveAcrylHome(env), ACRYL_DSH_ENGINE_DIR_NAME), env)
50
+ }
@@ -14,6 +14,16 @@ export {
14
14
  type AcrylSessionBridge,
15
15
  type AcrylSessionBridgeOptions,
16
16
  } from './session-bridge.ts'
17
+ export {
18
+ installSessionLogExporter,
19
+ type InstallSessionLogExporterOptions,
20
+ } from './session-log-exporter.ts'
21
+ export {
22
+ ACRYL_DSH_ENGINE_DIR_NAME,
23
+ ACRYL_HOME_DIR_NAME,
24
+ resolveAcrylDshHome,
25
+ resolveAcrylHome,
26
+ } from './acryl-home.ts'
17
27
 
18
28
  import { writeFileSync } from 'node:fs'
19
29
  import { createRequire } from 'node:module'
@@ -30,8 +40,10 @@ import {
30
40
  resolveProfileDir,
31
41
  } from '@deepseek-ai/dsh-app-boot'
32
42
 
43
+ import { resolveAcrylDshHome } from './acryl-home.ts'
33
44
  import { createAcrylCodingCapabilityPatches } from './coding-capabilities.ts'
34
45
  import { installAcrylWorkspaceStatusTool } from './plugin-acryl-workspace-status.ts'
46
+ import { installSessionLogExporter } from './session-log-exporter.ts'
35
47
 
36
48
  const require = createRequire(import.meta.url)
37
49
  const dshInstallAnchor = require.resolve('@deepseek-ai/dsh/package.json')
@@ -53,6 +65,7 @@ export async function bootAcrylHarnessProfile(
53
65
  options: BootAcrylHarnessProfileOptions,
54
66
  ): Promise<AcrylHarnessRuntime> {
55
67
  if (options.profile.trim() === '') throw new Error('ACRYL Harness profile must not be empty')
68
+ process.env.DSH_HOME = resolveAcrylDshHome()
56
69
  const profileDirectory = resolveProfileDir(options.profile)
57
70
  initProfile(profileDirectory, DEFAULT_PROFILE_BUNDLES)
58
71
  healProfilesModuleFallback(dshInstallAnchor)
@@ -72,6 +85,7 @@ export async function bootAcrylHarnessProfile(
72
85
  }
73
86
  const ctx = await boot('acryl', rootConfig, patches, options.prepare)
74
87
  if ((ctx as { tools?: unknown }).tools) installAcrylWorkspaceStatusTool(ctx)
88
+ installSessionLogExporter(ctx, { surface: 'tui' })
75
89
  let disposed = false
76
90
  return Object.freeze({
77
91
  ctx,
@@ -110,6 +124,7 @@ export async function bootAcrylWebProfile(
110
124
  options: BootAcrylWebProfileOptions = {},
111
125
  ): Promise<AcrylWebRuntime> {
112
126
  const profileName = 'web'
127
+ process.env.DSH_HOME = resolveAcrylDshHome()
113
128
  const profileDirectory = resolveProfileDir(profileName)
114
129
  initProfile(profileDirectory, DEFAULT_PROFILE_BUNDLES)
115
130
  healProfilesModuleFallback(dshInstallAnchor)
@@ -127,6 +142,7 @@ export async function bootAcrylWebProfile(
127
142
  return options.prepare?.(hostCtx)
128
143
  })
129
144
  if ((ctx as { tools?: unknown }).tools) installAcrylWorkspaceStatusTool(ctx)
145
+ installSessionLogExporter(ctx, { surface: 'web' })
130
146
  const startup = ctx.get('webStartup') as { host?: string; port?: number } | undefined
131
147
  const host = startup?.host ?? '127.0.0.1'
132
148
  const port = startup?.port ?? 3080
@@ -1,5 +1,5 @@
1
1
  import type { Context } from '@deepseek-ai/cordis'
2
- import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
2
+ import { installModelSelection, type Agent, type AgentHandle, type ModelSelectionRef } from '@deepseek-ai/dsh-agent'
3
3
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
4
4
  import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
5
5
  import type {} from '@deepseek-ai/dsh-agent-default-model'
@@ -39,6 +39,15 @@ export interface AcrylSessionBridge {
39
39
  listener: (event: SessionEvent) => void,
40
40
  ): Promise<AcrylSessionEventSubscription>
41
41
  submitPrompt(input: { readonly sessionId: string; readonly text: string }): Promise<void>
42
+ /**
43
+ * Switch an already-open session's live model. `agentOptions.provider`/`model`
44
+ * are a one-time construction input to `ctx.agents.create` — not a live
45
+ * setting — so this is the only thing that changes what a running session
46
+ * sends its next request to (`ModelSelectionRef` installed on the agent's
47
+ * own scoped context via `installModelSelection`, per-step prompt assembly
48
+ * reads it fresh).
49
+ */
50
+ selectModel(input: { readonly sessionId: string; readonly provider: string; readonly model: string }): Promise<void>
42
51
  cancel(sessionId: string): Promise<void>
43
52
  dispose(): Promise<void>
44
53
  }
@@ -101,6 +110,8 @@ export function createAcrylSessionBridge(
101
110
  options: AcrylSessionBridgeOptions,
102
111
  ): AcrylSessionBridge {
103
112
  const handles = new Map<string, AgentHandle>()
113
+ const modelSelections = new Map<string, ModelSelectionRef>()
114
+ const modelSelectionDisposers = new Map<string, () => void>()
104
115
  const subscribers = new Map<string, Set<(snapshot: AcrylSessionSnapshot) => void>>()
105
116
  const eventListeners = new Map<string, Set<(event: SessionEvent) => void>>()
106
117
  let disposed = false
@@ -170,6 +181,9 @@ export function createAcrylSessionBridge(
170
181
  agentOptions: { provider: selection.provider, model: selection.model },
171
182
  })
172
183
  handles.set(handle.agent.id, handle)
184
+ const ref: ModelSelectionRef = { current: undefined, assembled: undefined }
185
+ modelSelectionDisposers.set(handle.agent.id, installModelSelection(handle.agent.ctx, ref))
186
+ modelSelections.set(handle.agent.id, ref)
173
187
  return handle.agent.id
174
188
  },
175
189
  snapshot,
@@ -235,6 +249,12 @@ export function createAcrylSessionBridge(
235
249
  }))
236
250
  await accepted
237
251
  },
252
+ async selectModel(input: { readonly sessionId: string; readonly provider: string; readonly model: string }): Promise<void> {
253
+ agentFor(input.sessionId)
254
+ const ref = modelSelections.get(input.sessionId)
255
+ if (ref === undefined) throw new Error(`ACRYL session ${input.sessionId} has no installed model selection`)
256
+ ref.current = { provider: input.provider, model: input.model }
257
+ },
238
258
  async cancel(sessionId: string): Promise<void> {
239
259
  agentFor(sessionId).cancel({ kind: 'user' })
240
260
  },
@@ -244,6 +264,9 @@ export function createAcrylSessionBridge(
244
264
  offSessionEvent()
245
265
  subscribers.clear()
246
266
  eventListeners.clear()
267
+ for (const dispose of modelSelectionDisposers.values()) dispose()
268
+ modelSelectionDisposers.clear()
269
+ modelSelections.clear()
247
270
  // Durable continuity: idle the turn, checkpoint the session log, then
248
271
  // release the native handle. Mirror of Tomo's shutdown sequence, owned
249
272
  // here so every surface gets the same durability guarantee.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A durable, cross-surface debug log for ACRYL sessions.
3
+ *
4
+ * Cordis's own `ctx.logger` already carries every structured log record any
5
+ * plugin emits, but nothing in this stack persisted it anywhere — a failure
6
+ * like a vendored provider's OAuth error only ever reached the terminal
7
+ * transcript, gone the moment the pane scrolled or the process exited. This
8
+ * registers one JSONL file exporter on `ctx.logger` per surface (CLI/TUI,
9
+ * web, and — once wired there too — the desktop GUI), so `error`/`warn`
10
+ * records (and everything, when `ACRYL_LOG_LEVEL=debug`) survive the session
11
+ * and can be read back after the fact instead of re-derived from a
12
+ * screenshot.
13
+ *
14
+ * @module acryl-harness-runtime/session-log-exporter
15
+ */
16
+
17
+ import { appendFileSync, mkdirSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import type { Context } from '@deepseek-ai/cordis'
20
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
21
+
22
+ export interface InstallSessionLogExporterOptions {
23
+ /** Which ACRYL surface this process is (`tui`, `web`, `desktop`) — becomes part of the log filename. */
24
+ readonly surface: string
25
+ /** Harness home override; defaults to `$DSH_HOME`/`~/.dsh` exactly like credentials/settings resolve it. */
26
+ readonly dshHome?: string
27
+ }
28
+
29
+ const LOGGER_LEVEL_WARN = 2
30
+ const LOGGER_LEVEL_DEBUG = 3
31
+
32
+ /**
33
+ * Register the file exporter for this process's lifetime. `ctx.logger.exporter()`
34
+ * already ties its own disposal to the fiber that registers it (see
35
+ * `LoggerService.exporter` in `@deepseek-ai/cordis`), so this needs no extra
36
+ * `ctx.effect()` wrapper of its own.
37
+ * @param ctx - the plugin context whose `ctx.logger` gains the exporter.
38
+ * @param options - which surface/home this log file belongs to.
39
+ */
40
+ export function installSessionLogExporter(ctx: Context, options: InstallSessionLogExporterOptions): void {
41
+ const dshHome = resolveDshHome(options.dshHome)
42
+ const logDir = join(dshHome, 'logs')
43
+ try {
44
+ mkdirSync(logDir, { recursive: true, mode: 0o700 })
45
+ } catch {
46
+ return // best-effort diagnostics only; a session must never fail to start over this
47
+ }
48
+ const day = new Date().toISOString().slice(0, 10)
49
+ const logFile = join(logDir, `acryl-${options.surface}-${day}.jsonl`)
50
+ const threshold = process.env.ACRYL_LOG_LEVEL === 'debug' ? LOGGER_LEVEL_DEBUG : LOGGER_LEVEL_WARN
51
+
52
+ ctx.logger.exporter({
53
+ export(message) {
54
+ if (message.level > threshold) return
55
+ try {
56
+ appendFileSync(logFile, `${JSON.stringify({
57
+ ts: new Date(message.ts).toISOString(),
58
+ type: message.type,
59
+ name: message.name,
60
+ args: message.args.map(arg => (arg instanceof Error
61
+ ? { message: arg.message, stack: arg.stack, name: arg.name }
62
+ : arg)),
63
+ })}\n`, { mode: 0o600 })
64
+ } catch {
65
+ // best-effort: a write failure here must never take a session down
66
+ }
67
+ },
68
+ })
69
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acryl-tui",
3
- "version": "0.1.31",
3
+ "version": "0.1.35",
4
4
  "private": true,
5
5
  "description": "Canonical ACRYL pi-tui coding-agent terminal client",
6
6
  "license": "MIT",
@@ -25,7 +25,9 @@
25
25
  "node": ">=22"
26
26
  },
27
27
  "scripts": {
28
+ "prebuild": "node scripts/generate-build-info.mjs",
28
29
  "build": "tsdown && tsc -p tsconfig.json --emitDeclarationOnly",
30
+ "pretypecheck": "node scripts/generate-build-info.mjs",
29
31
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
30
32
  "test": "vitest run",
31
33
  "tui": "node lib/bin.js tui",
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "surface": "cli",
4
4
  "target": "linux-x64",
5
- "version": "0.1.31",
5
+ "version": "0.1.35",
6
6
  "packageName": "acryl-cli-linux-x64",
7
- "payloadSha256": "28662eabe7bc951c7b2dcc1f038eaffedaa340ee1a8ff8ad721fea5f95d501c9"
7
+ "payloadSha256": "0e4da558f00aca2aa26c52a10677258683194e04e009d906f8d1d534ca688d53"
8
8
  }