@slock-ai/daemon 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import {
4
4
  } from "./chunk-JG7ONJZ6.js";
5
5
 
6
6
  // src/core.ts
7
- import path12 from "path";
8
- import os5 from "os";
7
+ import path13 from "path";
8
+ import os6 from "os";
9
9
  import { createRequire } from "module";
10
10
  import { accessSync } from "fs";
11
11
  import { fileURLToPath } from "url";
@@ -545,8 +545,57 @@ var RUNTIMES = [
545
545
  { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
546
546
  { id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
547
547
  { id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
548
- { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true }
548
+ { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
549
+ { id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
549
550
  ];
551
+ var RUNTIME_MODELS = {
552
+ claude: [
553
+ { id: "sonnet", label: "Sonnet" },
554
+ { id: "opus", label: "Opus" },
555
+ { id: "haiku", label: "Haiku" }
556
+ ],
557
+ codex: [
558
+ { id: "gpt-5.5", label: "GPT-5.5" },
559
+ { id: "gpt-5.4", label: "GPT-5.4" },
560
+ { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
561
+ { id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
562
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
563
+ { id: "gpt-5.2", label: "GPT-5.2" },
564
+ { id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
565
+ { id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
566
+ { id: "gpt-5-codex", label: "GPT-5 Codex" },
567
+ { id: "gpt-5", label: "GPT-5" }
568
+ ],
569
+ copilot: [
570
+ { id: "gpt-5.4", label: "GPT-5.4" },
571
+ { id: "gpt-5.2", label: "GPT-5.2" },
572
+ { id: "claude-4-sonnet", label: "Claude 4 Sonnet" },
573
+ { id: "claude-4.5-sonnet", label: "Claude 4.5 Sonnet" }
574
+ ],
575
+ cursor: [
576
+ { id: "composer-2-fast", label: "Composer 2 Fast" },
577
+ { id: "composer-2", label: "Composer 2" },
578
+ { id: "auto", label: "Auto" }
579
+ ],
580
+ gemini: [
581
+ { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
582
+ { id: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)" },
583
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
584
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }
585
+ ],
586
+ opencode: [
587
+ { id: "opencode/gpt-5-nano", label: "GPT-5 Nano (OpenCode)" },
588
+ { id: "opencode/big-pickle", label: "Big Pickle (OpenCode)" },
589
+ { id: "opencode/hy3-preview-free", label: "HY3 Preview Free (OpenCode)" },
590
+ { id: "opencode/minimax-m2.5-free", label: "MiniMax M2.5 Free (OpenCode)" },
591
+ { id: "opencode/nemotron-3-super-free", label: "Nemotron 3 Super Free (OpenCode)" }
592
+ ],
593
+ // Kimi CLI resolves model keys from each user's local config, so the safest
594
+ // built-in option is to defer to whatever default model the CLI already uses.
595
+ kimi: [
596
+ { id: "default", label: "Configured Default" }
597
+ ]
598
+ };
550
599
  var PLAN_CONFIG = {
551
600
  free: {
552
601
  displayName: "Hobby",
@@ -584,8 +633,8 @@ var DISPLAY_PLAN_CONFIG = {
584
633
  // src/agentProcessManager.ts
585
634
  import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
586
635
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
587
- import path10 from "path";
588
- import os3 from "os";
636
+ import path11 from "path";
637
+ import os4 from "os";
589
638
 
590
639
  // src/drivers/claude.ts
591
640
  import { spawn } from "child_process";
@@ -631,6 +680,9 @@ function buildPrompt(config, variant, opts) {
631
680
  const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
632
681
  const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
633
682
  const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
683
+ const scheduleReminderCmd = isCli ? "`slock reminder schedule`" : `\`${t("schedule_reminder")}\``;
684
+ const listRemindersCmd = isCli ? "`slock reminder list`" : `\`${t("list_reminders")}\``;
685
+ const cancelReminderCmd = isCli ? "`slock reminder cancel`" : `\`${t("cancel_reminder")}\``;
634
686
  const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
635
687
  const criticalRules = isCli ? [
636
688
  "- Always communicate through `slock` CLI commands. This is your only output channel.",
@@ -681,15 +733,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
681
733
  14. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Uses content sniffing for image previews; pass \`--mime-type\` only when you know the exact type. Returns an attachment ID to pass to \`slock message send\`.
682
734
  15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
683
735
  16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
684
- 17. **\`slock profile update\`** \u2014 Update your own profile. Currently this only supports \`--avatar-file\`.
736
+ 17. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--display-name <name>\`, and \`--description <text>\`. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
685
737
  18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
686
738
  19. **\`slock reminder list\`** \u2014 List your reminders.
687
739
  20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
688
740
 
689
- When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
690
- Do not use runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders; use \`slock reminder schedule\` so reminders stay anchored, observable, and cancelable in Slock.
691
- For agent-created reminders, first resolve the anchor message from the current conversation and pass its \`msgId\` explicitly. If you cannot resolve a message id, do not create the reminder.
692
-
693
741
  The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints JSON to stderr:
694
742
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
695
743
 
@@ -712,7 +760,15 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
712
760
  10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
713
761
  11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
714
762
  12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
715
- 13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
763
+ 13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
764
+ 14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
765
+ 15. **${listRemindersCmd}** \u2014 List your reminders.
766
+ 16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
767
+ const reminderSection = `### Reminders
768
+
769
+ Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
770
+ Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay anchored, observable, and cancelable in Slock.
771
+ Create agent reminders only after resolving the anchor message from the current conversation and passing its msgId explicitly; if no anchor can be resolved, consider posting a status update in the relevant thread so the intent is visible, then revisit when context is available.`;
716
772
  const sendingMessagesSection = isCli ? `### Sending messages
717
773
 
718
774
  - **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
@@ -782,6 +838,11 @@ To jump directly to a specific hit with nearby context, use \`slock message read
782
838
  Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
783
839
 
784
840
  To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
841
+ const historicalReferenceSection = isCli ? `### Historical references
842
+
843
+ When a user refers to prior Slock discussion and the relevant context is not already available, first use \`slock message search\` and \`slock message read\` to find the original thread, decision, or owner before answering. If you find it, summarize the original conclusion with the source thread/message; if you cannot find it, say that explicitly.` : `### Historical references
844
+
845
+ When a user refers to prior Slock discussion and the relevant context is not already available, first use \`${t("search_messages")}\` and ${readCmd} to find the original thread, decision, or owner before answering. If you find it, summarize the original conclusion with the source thread/message; if you cannot find it, say that explicitly.`;
785
846
  const tasksSection = isCli ? `### Tasks
786
847
 
787
848
  When someone sends a message that asks you to do something \u2014 fix a bug, write code, review a PR, deploy, investigate an issue \u2014 that is work. Claim it before you start.
@@ -931,6 +992,8 @@ Header fields:
931
992
 
932
993
  ${sendingMessagesSection}
933
994
 
995
+ ${reminderSection}
996
+
934
997
  ${threadsSection}
935
998
 
936
999
  ${discoverySection}
@@ -939,6 +1002,8 @@ ${channelAwarenessSection}
939
1002
 
940
1003
  ${readingHistorySection}
941
1004
 
1005
+ ${historicalReferenceSection}
1006
+
942
1007
  ${tasksSection}
943
1008
 
944
1009
  ### Splitting tasks for parallel execution
@@ -996,7 +1061,7 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
996
1061
 
997
1062
  ## Workspace & Memory
998
1063
 
999
- Your working directory (cwd) is your **persistent workspace**. Everything you write here survives across sessions.
1064
+ Your working directory (cwd) is your **persistent, agent-owned workspace**; files you create here survive across sessions. Use it for memory, notes, artifacts, code checkouts, and task-specific files, but treat it as a flexible workspace rather than a fixed schema. Keep **MEMORY.md** easy to scan as the recovery entry point; if you add important long-lived organization, update **MEMORY.md** or a note index so future sessions can find it. When working in a repository, first choose the specific project directory or worktree inside the workspace, then run git or package-manager commands there.
1000
1065
 
1001
1066
  ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1002
1067
 
@@ -2240,6 +2305,17 @@ var CursorDriver = class {
2240
2305
  import { spawn as spawn5 } from "child_process";
2241
2306
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2242
2307
  import path7 from "path";
2308
+ function buildGeminiSpawnEnv(ctx) {
2309
+ return {
2310
+ ...process.env,
2311
+ FORCE_COLOR: "0",
2312
+ NO_COLOR: "1",
2313
+ // Gemini CLI's trusted-workspace gate breaks our managed headless flow
2314
+ // unless we explicitly trust the daemon-owned agent workspace.
2315
+ GEMINI_CLI_TRUST_WORKSPACE: "true",
2316
+ ...ctx.config.envVars || {}
2317
+ };
2318
+ }
2243
2319
  var GeminiDriver = class {
2244
2320
  id = "gemini";
2245
2321
  supportsStdinNotification = false;
@@ -2279,7 +2355,7 @@ var GeminiDriver = class {
2279
2355
  if (ctx.config.sessionId) {
2280
2356
  args.push("--resume", ctx.config.sessionId);
2281
2357
  }
2282
- const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...ctx.config.envVars || {} };
2358
+ const spawnEnv = buildGeminiSpawnEnv(ctx);
2283
2359
  const proc = spawn5("gemini", args, {
2284
2360
  cwd: ctx.workingDirectory,
2285
2361
  stdio: ["pipe", "pipe", "pipe"],
@@ -2581,6 +2657,279 @@ function detectKimiModels(home = os2.homedir()) {
2581
2657
  return { models, default: defaultModel };
2582
2658
  }
2583
2659
 
2660
+ // src/drivers/opencode.ts
2661
+ import { spawn as spawn7 } from "child_process";
2662
+ import { readFileSync as readFileSync3 } from "fs";
2663
+ import os3 from "os";
2664
+ import path9 from "path";
2665
+ var CHAT_MCP_SERVER_NAME = "chat";
2666
+ var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
2667
+ var SLOCK_AGENT_NAME = "slock";
2668
+ var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
2669
+ var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
2670
+ var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
2671
+ function buildChatBridgeCommand(ctx) {
2672
+ const isTsSource = ctx.chatBridgePath.endsWith(".ts");
2673
+ return [
2674
+ isTsSource ? "npx" : "node",
2675
+ ...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
2676
+ "--agent-id",
2677
+ ctx.agentId,
2678
+ "--server-url",
2679
+ ctx.config.serverUrl,
2680
+ "--auth-token",
2681
+ ctx.config.authToken || ctx.daemonApiKey,
2682
+ "--runtime",
2683
+ "opencode",
2684
+ ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
2685
+ "--runtime-actions-only"
2686
+ ];
2687
+ }
2688
+ function parseOpenCodeConfigContent(raw) {
2689
+ if (!raw) return {};
2690
+ try {
2691
+ const parsed = JSON.parse(raw);
2692
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2693
+ return parsed;
2694
+ }
2695
+ } catch {
2696
+ }
2697
+ return {};
2698
+ }
2699
+ function parseUserOpenCodeConfig(ctx) {
2700
+ const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
2701
+ return parseOpenCodeConfigContent(raw);
2702
+ }
2703
+ function readLocalOpenCodeConfig(home = os3.homedir()) {
2704
+ const configPath = path9.join(home, ".config", "opencode", "opencode.json");
2705
+ try {
2706
+ return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
2707
+ } catch {
2708
+ }
2709
+ return {};
2710
+ }
2711
+ function recordField(value) {
2712
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2713
+ }
2714
+ function parseSemver(version) {
2715
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
2716
+ if (!match) return null;
2717
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
2718
+ }
2719
+ function isSupportedOpenCodeVersion(version) {
2720
+ if (!version) return true;
2721
+ const actual = parseSemver(version);
2722
+ const minimum = parseSemver(MIN_SUPPORTED_OPENCODE_VERSION);
2723
+ if (!actual || !minimum) return true;
2724
+ for (let i = 0; i < 3; i += 1) {
2725
+ if (actual[i] > minimum[i]) return true;
2726
+ if (actual[i] < minimum[i]) return false;
2727
+ }
2728
+ return true;
2729
+ }
2730
+ function unsupportedOpenCodeVersionMessage(version) {
2731
+ if (!version || isSupportedOpenCodeVersion(version)) return null;
2732
+ return `OpenCode CLI ${version} is unsupported; requires OpenCode >= ${MIN_SUPPORTED_OPENCODE_VERSION}. Upgrade opencode before starting this runtime.`;
2733
+ }
2734
+ function mergeOpenCodeConfigs(localConfig, envConfig) {
2735
+ return {
2736
+ ...localConfig,
2737
+ ...envConfig,
2738
+ provider: {
2739
+ ...recordField(localConfig.provider),
2740
+ ...recordField(envConfig.provider)
2741
+ },
2742
+ agent: {
2743
+ ...recordField(localConfig.agent),
2744
+ ...recordField(envConfig.agent)
2745
+ },
2746
+ mcp: {
2747
+ ...recordField(localConfig.mcp),
2748
+ ...recordField(envConfig.mcp)
2749
+ }
2750
+ };
2751
+ }
2752
+ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
2753
+ const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
2754
+ const userAgents = recordField(userConfig.agent);
2755
+ const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
2756
+ return {
2757
+ ...userConfig,
2758
+ $schema: "https://opencode.ai/config.json",
2759
+ agent: {
2760
+ ...userAgents,
2761
+ [SLOCK_AGENT_NAME]: {
2762
+ ...userSlockAgent,
2763
+ description: "Slock agent runtime",
2764
+ prompt: ctx.standingPrompt
2765
+ }
2766
+ },
2767
+ mcp: {
2768
+ ...recordField(userConfig.mcp),
2769
+ [CHAT_MCP_SERVER_NAME]: {
2770
+ type: "local",
2771
+ command: buildChatBridgeCommand(ctx),
2772
+ enabled: true
2773
+ }
2774
+ }
2775
+ };
2776
+ }
2777
+ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
2778
+ const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
2779
+ const config = buildOpenCodeConfig(ctx, home);
2780
+ const env = {
2781
+ ...slock.spawnEnv,
2782
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(config)
2783
+ };
2784
+ const args = [
2785
+ "run",
2786
+ "--format",
2787
+ "json",
2788
+ "--dangerously-skip-permissions",
2789
+ "--pure",
2790
+ "--dir",
2791
+ ctx.workingDirectory
2792
+ ];
2793
+ if (ctx.config.model && ctx.config.model !== "default") {
2794
+ args.push("--model", ctx.config.model);
2795
+ }
2796
+ args.push("--agent", SLOCK_AGENT_NAME);
2797
+ if (ctx.config.sessionId) {
2798
+ args.push("--session", ctx.config.sessionId);
2799
+ }
2800
+ const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT : ctx.prompt;
2801
+ args.push("--", turnPrompt);
2802
+ return { args, env, config };
2803
+ }
2804
+ function detectOpenCodeModels(home = os3.homedir()) {
2805
+ const models = [...RUNTIME_MODELS.opencode || []];
2806
+ const providers = recordField(readLocalOpenCodeConfig(home).provider);
2807
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
2808
+ const providerModels = recordField(recordField(providerConfig).models);
2809
+ for (const [modelId, modelConfig] of Object.entries(providerModels)) {
2810
+ const fullId = `${providerId}/${modelId}`;
2811
+ if (models.some((model2) => model2.id === fullId)) continue;
2812
+ const model = recordField(modelConfig);
2813
+ const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
2814
+ models.push({ id: fullId, label: name });
2815
+ }
2816
+ }
2817
+ return models.length > 0 ? { models } : null;
2818
+ }
2819
+ function isSystemFirstMessageTask(message) {
2820
+ return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
2821
+ }
2822
+ function buildOpenCodeSystemPrompt(config) {
2823
+ return buildCliTransportSystemPrompt(config, {
2824
+ toolPrefix: CHAT_MCP_TOOL_PREFIX,
2825
+ extraCriticalRules: [
2826
+ "- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `chat_runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2827
+ ],
2828
+ postStartupNotes: [
2829
+ "**OpenCode runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
2830
+ ],
2831
+ includeStdinNotificationSection: false,
2832
+ messageNotificationStyle: "poll"
2833
+ });
2834
+ }
2835
+ var OpenCodeDriver = class {
2836
+ id = "opencode";
2837
+ supportsStdinNotification = false;
2838
+ mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
2839
+ busyDeliveryMode = "none";
2840
+ terminateProcessOnTurnEnd = true;
2841
+ deferSpawnUntilMessage = true;
2842
+ usesSlockCliForCommunication = true;
2843
+ shouldDeferWakeMessage(message) {
2844
+ return isSystemFirstMessageTask(message);
2845
+ }
2846
+ sessionId = null;
2847
+ sessionAnnounced = false;
2848
+ probe() {
2849
+ if (!resolveCommandOnPath("opencode")) return { available: false };
2850
+ const version = readCommandVersion("opencode") || void 0;
2851
+ const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
2852
+ if (unsupportedMessage) {
2853
+ return {
2854
+ available: false,
2855
+ version: `${version} (requires >= ${MIN_SUPPORTED_OPENCODE_VERSION})`
2856
+ };
2857
+ }
2858
+ return { available: true, version };
2859
+ }
2860
+ async detectModels() {
2861
+ return detectOpenCodeModels();
2862
+ }
2863
+ spawn(ctx) {
2864
+ this.sessionId = ctx.config.sessionId || null;
2865
+ this.sessionAnnounced = false;
2866
+ const unsupportedMessage = unsupportedOpenCodeVersionMessage(readCommandVersion("opencode"));
2867
+ if (unsupportedMessage) {
2868
+ throw new Error(unsupportedMessage);
2869
+ }
2870
+ const launch = buildOpenCodeLaunchOptions(ctx);
2871
+ const proc = spawn7("opencode", launch.args, {
2872
+ cwd: ctx.workingDirectory,
2873
+ stdio: ["pipe", "pipe", "pipe"],
2874
+ env: launch.env,
2875
+ shell: process.platform === "win32"
2876
+ });
2877
+ proc.stdin?.end();
2878
+ return { process: proc };
2879
+ }
2880
+ parseLine(line) {
2881
+ let event;
2882
+ try {
2883
+ event = JSON.parse(line);
2884
+ } catch {
2885
+ return [];
2886
+ }
2887
+ const events = [];
2888
+ if (event.sessionID && event.sessionID !== this.sessionId) {
2889
+ this.sessionId = event.sessionID;
2890
+ }
2891
+ if (!this.sessionAnnounced && this.sessionId) {
2892
+ events.push({ kind: "session_init", sessionId: this.sessionId });
2893
+ this.sessionAnnounced = true;
2894
+ }
2895
+ switch (event.type) {
2896
+ case "step_start":
2897
+ events.push({ kind: "thinking", text: "" });
2898
+ break;
2899
+ case "text":
2900
+ if (typeof event.part?.text === "string" && event.part.text.length > 0) {
2901
+ events.push({ kind: "text", text: event.part.text });
2902
+ }
2903
+ break;
2904
+ case "tool_use":
2905
+ events.push({
2906
+ kind: "tool_call",
2907
+ name: event.part?.tool || "unknown_tool",
2908
+ input: event.part?.state?.input
2909
+ });
2910
+ break;
2911
+ case "step_finish":
2912
+ if (event.part?.reason !== "tool-calls") {
2913
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
2914
+ }
2915
+ break;
2916
+ case "error": {
2917
+ const message = event.error?.data?.message || event.error?.message || (event.error?.name ? `${event.error.name} (no message)` : null) || "Unknown OpenCode error";
2918
+ events.push({ kind: "error", message });
2919
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
2920
+ break;
2921
+ }
2922
+ }
2923
+ return events;
2924
+ }
2925
+ encodeStdinMessage(_text, _sessionId, _opts) {
2926
+ return null;
2927
+ }
2928
+ buildSystemPrompt(config, _agentId) {
2929
+ return buildOpenCodeSystemPrompt(config);
2930
+ }
2931
+ };
2932
+
2584
2933
  // src/drivers/index.ts
2585
2934
  var driverFactories = {
2586
2935
  claude: () => new ClaudeDriver(),
@@ -2588,7 +2937,8 @@ var driverFactories = {
2588
2937
  copilot: () => new CopilotDriver(),
2589
2938
  cursor: () => new CursorDriver(),
2590
2939
  gemini: () => new GeminiDriver(),
2591
- kimi: () => new KimiDriver()
2940
+ kimi: () => new KimiDriver(),
2941
+ opencode: () => new OpenCodeDriver()
2592
2942
  };
2593
2943
  function getDriver(runtimeId) {
2594
2944
  const createDriver = driverFactories[runtimeId];
@@ -2601,7 +2951,7 @@ function getDriver(runtimeId) {
2601
2951
 
2602
2952
  // src/workspaces.ts
2603
2953
  import { readdir, rm, stat } from "fs/promises";
2604
- import path9 from "path";
2954
+ import path10 from "path";
2605
2955
  function isValidWorkspaceDirectoryName(directoryName) {
2606
2956
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
2607
2957
  }
@@ -2609,7 +2959,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
2609
2959
  if (!isValidWorkspaceDirectoryName(directoryName)) {
2610
2960
  return null;
2611
2961
  }
2612
- return path9.join(dataDir, directoryName);
2962
+ return path10.join(dataDir, directoryName);
2613
2963
  }
2614
2964
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
2615
2965
  return {
@@ -2658,7 +3008,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
2658
3008
  return summary;
2659
3009
  }
2660
3010
  const childSummaries = await Promise.all(
2661
- entries.map((entry) => summarizeWorkspaceEntry(path9.join(dirPath, entry.name), entry))
3011
+ entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
2662
3012
  );
2663
3013
  for (const childSummary of childSummaries) {
2664
3014
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -2677,7 +3027,7 @@ async function scanWorkspaceDirectories(dataDir) {
2677
3027
  if (!entry.isDirectory()) {
2678
3028
  return null;
2679
3029
  }
2680
- const dirPath = path9.join(dataDir, entry.name);
3030
+ const dirPath = path10.join(dataDir, entry.name);
2681
3031
  try {
2682
3032
  const summary = await summarizeWorkspaceDirectory(dirPath);
2683
3033
  return {
@@ -2709,7 +3059,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
2709
3059
  }
2710
3060
 
2711
3061
  // src/agentProcessManager.ts
2712
- var DATA_DIR = path10.join(os3.homedir(), ".slock", "agents");
3062
+ var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
2713
3063
  function toLocalTime(iso) {
2714
3064
  const d = new Date(iso);
2715
3065
  if (isNaN(d.getTime())) return iso;
@@ -2750,12 +3100,12 @@ function findSessionJsonl(root, predicate) {
2750
3100
  for (const entry of entries) {
2751
3101
  if (++visited > maxEntries) return null;
2752
3102
  if (!entry.isFile() || !predicate(entry.name)) continue;
2753
- return path10.join(dir, entry.name);
3103
+ return path11.join(dir, entry.name);
2754
3104
  }
2755
3105
  for (const entry of entries) {
2756
3106
  if (++visited > maxEntries) return null;
2757
3107
  if (!entry.isDirectory()) continue;
2758
- const found = visit(path10.join(dir, entry.name), depth - 1);
3108
+ const found = visit(path11.join(dir, entry.name), depth - 1);
2759
3109
  if (found) return found;
2760
3110
  }
2761
3111
  return null;
@@ -2768,9 +3118,9 @@ function safeSessionFilename(value) {
2768
3118
  }
2769
3119
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
2770
3120
  try {
2771
- const dir = path10.join(fallbackDir, ".slock", "runtime-sessions");
3121
+ const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
2772
3122
  mkdirSync4(dir, { recursive: true });
2773
- const filePath = path10.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
3123
+ const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
2774
3124
  writeFileSync7(filePath, JSON.stringify({
2775
3125
  type: "runtime_session_handoff",
2776
3126
  runtime,
@@ -2789,8 +3139,8 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
2789
3139
  return null;
2790
3140
  }
2791
3141
  }
2792
- function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), fallbackDir) {
2793
- const directPath = path10.isAbsolute(sessionId) ? sessionId : null;
3142
+ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
3143
+ const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
2794
3144
  if (directPath) {
2795
3145
  try {
2796
3146
  if (statSync(directPath).isFile()) {
@@ -2799,7 +3149,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), f
2799
3149
  } catch {
2800
3150
  }
2801
3151
  }
2802
- const resolvedPath = runtime === "claude" ? findSessionJsonl(path10.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path10.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
3152
+ const resolvedPath = runtime === "claude" ? findSessionJsonl(path11.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path11.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
2803
3153
  if (!resolvedPath && fallbackDir) {
2804
3154
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
2805
3155
  if (fallback) return fallback;
@@ -2830,12 +3180,38 @@ function formatThreadContextMessage(message) {
2830
3180
  const senderType = formatVisibleActorType(message.sender_type);
2831
3181
  return `- [msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}`;
2832
3182
  }
2833
- function formatIncomingMessage(message) {
3183
+ function mcpToolName(driver, name) {
3184
+ return `${driver?.mcpToolPrefix ?? ""}${name}`;
3185
+ }
3186
+ function communicationCommand(driver, name) {
3187
+ if (!driver?.usesSlockCliForCommunication) {
3188
+ return mcpToolName(driver, name);
3189
+ }
3190
+ switch (name) {
3191
+ case "send_message":
3192
+ return "slock message send";
3193
+ case "read_history":
3194
+ return "slock message read";
3195
+ case "check_messages":
3196
+ return "slock message check";
3197
+ case "claim_tasks":
3198
+ return "slock task claim";
3199
+ default:
3200
+ return `slock ${name.replace(/_/g, " ")}`;
3201
+ }
3202
+ }
3203
+ function dynamicReplyInstruction(driver) {
3204
+ return driver?.usesSlockCliForCommunication ? "reply using `slock message send --target <exact target>`" : `reply with ${communicationCommand(driver, "send_message")}`;
3205
+ }
3206
+ function dynamicClaimInstruction(driver) {
3207
+ return driver?.usesSlockCliForCommunication ? "claim the relevant task with `slock task claim`" : `claim the relevant task with ${communicationCommand(driver, "claim_tasks")}`;
3208
+ }
3209
+ function formatIncomingMessage(message, driver) {
2834
3210
  const threadJoinPrefix = message.thread_join_context ? [
2835
3211
  `[System: You were added to a new thread via @mention. Read this context before replying.]`,
2836
3212
  `parent: ${message.thread_join_context.parent_target}`,
2837
3213
  `thread: ${message.thread_join_context.thread_target}`,
2838
- `suggested next step: read_history(channel="${message.thread_join_context.suggested_read_history_target}")`,
3214
+ `suggested next step: ${driver?.usesSlockCliForCommunication ? `slock message read --channel "${message.thread_join_context.suggested_read_history_target}"` : `${communicationCommand(driver, "read_history")}(channel="${message.thread_join_context.suggested_read_history_target}")`}`,
2839
3215
  "",
2840
3216
  "Parent message:",
2841
3217
  formatThreadContextMessage(message.thread_join_context.parent_message),
@@ -3369,9 +3745,20 @@ function classifyTerminalFailure(ap) {
3369
3745
  return null;
3370
3746
  }
3371
3747
  function isMissingResumeSession(ap) {
3372
- if (ap.driver.id !== "claude") return false;
3373
3748
  if (!ap.sessionId) return false;
3374
- return /No conversation found with session ID/i.test(ap.lastRuntimeError || "");
3749
+ const candidates = [
3750
+ ap.lastRuntimeError,
3751
+ ...ap.recentStderr
3752
+ ].filter((value) => !!value);
3753
+ if (ap.driver.id === "claude") {
3754
+ return candidates.some((text) => /No conversation found with session ID/i.test(text));
3755
+ }
3756
+ if (ap.driver.id === "opencode") {
3757
+ return candidates.some(
3758
+ (text) => /Session not found/i.test(text) && text.includes(ap.sessionId)
3759
+ );
3760
+ }
3761
+ return false;
3375
3762
  }
3376
3763
  function getMessageDeliveryText(driver) {
3377
3764
  return driver.supportsStdinNotification ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
@@ -3416,7 +3803,7 @@ var AgentProcessManager = class _AgentProcessManager {
3416
3803
  this.daemonApiKey = daemonApiKey;
3417
3804
  this.serverUrl = opts.serverUrl;
3418
3805
  this.dataDir = opts.dataDir || DATA_DIR;
3419
- this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os3.homedir();
3806
+ this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
3420
3807
  this.driverResolver = opts.driverResolver || getDriver;
3421
3808
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
3422
3809
  this.tracer = opts.tracer ?? noopTracer;
@@ -3433,26 +3820,26 @@ var AgentProcessManager = class _AgentProcessManager {
3433
3820
  this.agentsStarting.add(agentId);
3434
3821
  try {
3435
3822
  const driver = this.driverResolver(config.runtime || "claude");
3436
- const agentDataDir = path10.join(this.dataDir, agentId);
3823
+ const agentDataDir = path11.join(this.dataDir, agentId);
3437
3824
  await mkdir(agentDataDir, { recursive: true });
3438
3825
  const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
3439
- const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
3826
+ const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
3440
3827
  try {
3441
3828
  await access(memoryMdPath);
3442
3829
  } catch {
3443
3830
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
3444
3831
  await writeFile(memoryMdPath, initialMemoryMd);
3445
3832
  }
3446
- const notesDir = path10.join(agentDataDir, "notes");
3833
+ const notesDir = path11.join(agentDataDir, "notes");
3447
3834
  await mkdir(notesDir, { recursive: true });
3448
3835
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
3449
3836
  const seedFiles = buildOnboardingSeedFiles();
3450
3837
  for (const { relativePath, content } of seedFiles) {
3451
- const fullPath = path10.join(agentDataDir, relativePath);
3838
+ const fullPath = path11.join(agentDataDir, relativePath);
3452
3839
  try {
3453
3840
  await access(fullPath);
3454
3841
  } catch {
3455
- await mkdir(path10.dirname(fullPath), { recursive: true });
3842
+ await mkdir(path11.dirname(fullPath), { recursive: true });
3456
3843
  await writeFile(fullPath, content);
3457
3844
  }
3458
3845
  }
@@ -3470,7 +3857,7 @@ var AgentProcessManager = class _AgentProcessManager {
3470
3857
  const channelLabel = formatChannelLabel(wakeMessage);
3471
3858
  prompt = runtimeProfileControlPrompt ?? `New message received:
3472
3859
 
3473
- ${formatIncomingMessage(wakeMessage)}`;
3860
+ ${formatIncomingMessage(wakeMessage, driver)}`;
3474
3861
  if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
3475
3862
  const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
3476
3863
  if (otherUnread.length > 0) {
@@ -3483,13 +3870,13 @@ You also have unread messages in other channels:`;
3483
3870
  }
3484
3871
  prompt += `
3485
3872
 
3486
- Use read_history to catch up, or respond to the message above first.`;
3873
+ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to the message above first.`;
3487
3874
  }
3488
3875
  }
3489
3876
  if (!runtimeProfileControlPrompt) {
3490
3877
  prompt += `
3491
3878
 
3492
- Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
3879
+ Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
3493
3880
 
3494
3881
  IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver)}`;
3495
3882
  prompt += getBusyDeliveryNote(driver);
@@ -3502,7 +3889,7 @@ IMPORTANT: If the message requires multi-step work (e.g. research, code changes,
3502
3889
  }
3503
3890
  prompt += `
3504
3891
 
3505
- Use read_history to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call check_messages in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: reply with send_message and claim the relevant task before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver)}`;
3892
+ Use ${communicationCommand(driver, "read_history")} to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call ${communicationCommand(driver, "check_messages")} in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: ${dynamicReplyInstruction(driver)} and ${dynamicClaimInstruction(driver)} before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver)}`;
3506
3893
  } else if (isResume) {
3507
3894
  prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
3508
3895
  prompt += getBusyDeliveryNote(driver);
@@ -3510,6 +3897,24 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3510
3897
  prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
3511
3898
  }
3512
3899
  const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
3900
+ const canDeferEmptyStart = driver.deferSpawnUntilMessage === true && !wakeMessage && !runtimeConfig.runtimeProfileControl && (!unreadSummary || Object.keys(unreadSummary).length === 0);
3901
+ if (canDeferEmptyStart) {
3902
+ const pendingMessages = this.startingInboxes.get(agentId) || [];
3903
+ this.startingInboxes.delete(agentId);
3904
+ this.agentsStarting.delete(agentId);
3905
+ this.idleAgentConfigs.set(agentId, {
3906
+ config: effectiveConfig,
3907
+ sessionId: effectiveConfig.sessionId || null,
3908
+ launchId: launchId || null
3909
+ });
3910
+ this.sendAgentStatus(agentId, "active", launchId || null);
3911
+ this.broadcastActivity(agentId, "online", "Process idle");
3912
+ logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
3913
+ for (const message of pendingMessages) {
3914
+ this.deliverMessage(agentId, message);
3915
+ }
3916
+ return;
3917
+ }
3513
3918
  const { process: proc } = driver.spawn({
3514
3919
  agentId,
3515
3920
  config: effectiveConfig,
@@ -3528,6 +3933,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3528
3933
  config: runtimeConfig,
3529
3934
  sessionId: runtimeConfig.sessionId || null,
3530
3935
  launchId: launchId || null,
3936
+ startupWakeMessage: wakeMessage,
3937
+ startupUnreadSummary: unreadSummary,
3938
+ startupResumePrompt: resumePrompt,
3531
3939
  isIdle: false,
3532
3940
  notificationTimer: null,
3533
3941
  pendingNotificationCount: 0,
@@ -3545,6 +3953,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3545
3953
  spawnError: null,
3546
3954
  exitCode: null,
3547
3955
  exitSignal: null,
3956
+ expectedTerminationReason: null,
3548
3957
  pendingTrajectory: null,
3549
3958
  gatedSteering: createGatedSteeringState()
3550
3959
  };
@@ -3614,20 +4023,60 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3614
4023
  }
3615
4024
  const finalCode = ap.exitCode ?? code;
3616
4025
  const finalSignal = ap.exitSignal ?? signal;
3617
- const terminalFailureDetail = classifyTerminalFailure(ap);
3618
- this.endRuntimeTrace(ap, finalCode === 0 ? "ok" : "error", {
3619
- outcome: finalCode === 0 ? "process-exit" : "process-crash",
4026
+ const expectedTermination = Boolean(ap.expectedTerminationReason);
4027
+ const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
4028
+ const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
4029
+ const missingResumeSession = isMissingResumeSession(ap);
4030
+ const summary = summarizeCrash(finalCode, finalSignal);
4031
+ this.endRuntimeTrace(ap, processEndedCleanly ? "ok" : "error", {
4032
+ outcome: processEndedCleanly ? "process-exit" : "process-crash",
4033
+ expectedTerminationReason: ap.expectedTerminationReason || void 0,
3620
4034
  exitCode: finalCode,
3621
4035
  exitSignal: finalSignal
3622
4036
  });
3623
- if (finalCode === 0) {
4037
+ if (processEndedCleanly) {
3624
4038
  this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
3625
4039
  } else {
3626
4040
  this.clearCompactionWatchdog(ap);
3627
4041
  }
3628
4042
  this.agents.delete(agentId);
3629
- if (finalCode === 0) {
3630
- const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
4043
+ if (missingResumeSession) {
4044
+ const staleSessionId = ap.sessionId;
4045
+ const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : "Claude";
4046
+ const restartConfig = { ...ap.config, sessionId: null };
4047
+ logger.warn(
4048
+ `[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} is unavailable locally; falling back to cold start`
4049
+ );
4050
+ this.broadcastActivity(
4051
+ agentId,
4052
+ "working",
4053
+ `Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`,
4054
+ [{ kind: "text", text: `Stored ${runtimeLabel} session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
4055
+ );
4056
+ this.startAgent(
4057
+ agentId,
4058
+ restartConfig,
4059
+ ap.startupWakeMessage,
4060
+ ap.startupUnreadSummary,
4061
+ ap.startupResumePrompt,
4062
+ ap.launchId || void 0
4063
+ ).catch((err) => {
4064
+ logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
4065
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
4066
+ this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
4067
+ });
4068
+ return;
4069
+ }
4070
+ if (processEndedCleanly) {
4071
+ let queuedWakeMessage;
4072
+ if (!ap.driver.supportsStdinNotification) {
4073
+ while (ap.inbox.length > 0) {
4074
+ const candidate = ap.inbox.shift();
4075
+ if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
4076
+ queuedWakeMessage = candidate;
4077
+ break;
4078
+ }
4079
+ }
3631
4080
  const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
3632
4081
  if (queuedWakeMessage) {
3633
4082
  logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
@@ -3662,26 +4111,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3662
4111
  } else {
3663
4112
  this.idleAgentConfigs.delete(agentId);
3664
4113
  const reason = formatCrashReason(finalCode, finalSignal, ap);
3665
- const summary = summarizeCrash(finalCode, finalSignal);
3666
- if (isMissingResumeSession(ap)) {
3667
- const staleSessionId = ap.sessionId;
3668
- const restartConfig = { ...ap.config, sessionId: null };
3669
- logger.warn(
3670
- `[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
3671
- );
3672
- this.broadcastActivity(
3673
- agentId,
3674
- "working",
3675
- "Stored Claude session missing; cold-starting a new session\u2026",
3676
- [{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
3677
- );
3678
- this.startAgent(agentId, restartConfig, void 0, void 0, void 0, ap.launchId || void 0).catch((err) => {
3679
- logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
3680
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
3681
- this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
3682
- });
3683
- return;
3684
- }
3685
4114
  logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
3686
4115
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
3687
4116
  if (terminalFailureDetail) {
@@ -3804,6 +4233,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3804
4233
  }
3805
4234
  const cached = this.idleAgentConfigs.get(agentId);
3806
4235
  if (cached) {
4236
+ const driver = this.driverResolver(cached.config.runtime || "claude");
4237
+ if (this.shouldDeferWakeMessage(agentId, driver, message)) {
4238
+ return;
4239
+ }
3807
4240
  logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
3808
4241
  this.idleAgentConfigs.delete(agentId);
3809
4242
  this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
@@ -3812,6 +4245,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3812
4245
  }
3813
4246
  return;
3814
4247
  }
4248
+ if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
4249
+ return;
4250
+ }
3815
4251
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
3816
4252
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
3817
4253
  nextMessages.push(message);
@@ -3840,7 +4276,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3840
4276
  }
3841
4277
  }
3842
4278
  async resetWorkspace(agentId) {
3843
- const agentDataDir = path10.join(this.dataDir, agentId);
4279
+ const agentDataDir = path11.join(this.dataDir, agentId);
3844
4280
  try {
3845
4281
  await rm2(agentDataDir, { recursive: true, force: true });
3846
4282
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -3856,6 +4292,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3856
4292
  getRunningAgentIds() {
3857
4293
  return [...this.agents.keys()];
3858
4294
  }
4295
+ shouldDeferWakeMessage(agentId, driver, message) {
4296
+ if (!driver.shouldDeferWakeMessage?.(message)) return false;
4297
+ logger.info(`[Agent ${agentId}] Deferred non-concrete wake message for ${driver.id}`);
4298
+ return true;
4299
+ }
3859
4300
  getAgentSessionId(agentId) {
3860
4301
  return this.agents.get(agentId)?.sessionId ?? null;
3861
4302
  }
@@ -3870,7 +4311,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
3870
4311
  return result;
3871
4312
  }
3872
4313
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
3873
- const workspacePath = path10.join(this.dataDir, agentId);
4314
+ const workspacePath = path11.join(this.dataDir, agentId);
3874
4315
  return {
3875
4316
  agentId,
3876
4317
  launchId,
@@ -4019,7 +4460,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4019
4460
  }
4020
4461
  // Workspace file browsing
4021
4462
  async getFileTree(agentId, dirPath) {
4022
- const agentDir = path10.join(this.dataDir, agentId);
4463
+ const agentDir = path11.join(this.dataDir, agentId);
4023
4464
  try {
4024
4465
  await stat2(agentDir);
4025
4466
  } catch {
@@ -4027,8 +4468,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4027
4468
  }
4028
4469
  let targetDir = agentDir;
4029
4470
  if (dirPath) {
4030
- const resolved = path10.resolve(agentDir, dirPath);
4031
- if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
4471
+ const resolved = path11.resolve(agentDir, dirPath);
4472
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
4032
4473
  return [];
4033
4474
  }
4034
4475
  targetDir = resolved;
@@ -4036,9 +4477,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4036
4477
  return this.listDirectoryChildren(targetDir, agentDir);
4037
4478
  }
4038
4479
  async readFile(agentId, filePath) {
4039
- const agentDir = path10.join(this.dataDir, agentId);
4040
- const resolved = path10.resolve(agentDir, filePath);
4041
- if (!resolved.startsWith(agentDir + path10.sep) && resolved !== agentDir) {
4480
+ const agentDir = path11.join(this.dataDir, agentId);
4481
+ const resolved = path11.resolve(agentDir, filePath);
4482
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
4042
4483
  throw new Error("Access denied");
4043
4484
  }
4044
4485
  const info = await stat2(resolved);
@@ -4062,7 +4503,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4062
4503
  ".sh",
4063
4504
  ".py"
4064
4505
  ]);
4065
- const ext = path10.extname(resolved).toLowerCase();
4506
+ const ext = path11.extname(resolved).toLowerCase();
4066
4507
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
4067
4508
  return { content: null, binary: true };
4068
4509
  }
@@ -4088,14 +4529,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4088
4529
  async listSkills(agentId, runtimeHint) {
4089
4530
  const agent = this.agents.get(agentId);
4090
4531
  const runtime = runtimeHint || agent?.config.runtime || "claude";
4091
- const home = os3.homedir();
4092
- const workspaceDir = path10.join(this.dataDir, agentId);
4532
+ const home = os4.homedir();
4533
+ const workspaceDir = path11.join(this.dataDir, agentId);
4093
4534
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
4094
4535
  const globalResults = await Promise.all(
4095
- paths.global.map((p) => this.scanSkillsDir(path10.join(home, p)))
4536
+ paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
4096
4537
  );
4097
4538
  const workspaceResults = await Promise.all(
4098
- paths.workspace.map((p) => this.scanSkillsDir(path10.join(workspaceDir, p)))
4539
+ paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
4099
4540
  );
4100
4541
  const dedup = (skills) => {
4101
4542
  const seen = /* @__PURE__ */ new Set();
@@ -4124,7 +4565,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4124
4565
  const skills = [];
4125
4566
  for (const entry of entries) {
4126
4567
  if (entry.isDirectory() || entry.isSymbolicLink()) {
4127
- const skillMd = path10.join(dir, entry.name, "SKILL.md");
4568
+ const skillMd = path11.join(dir, entry.name, "SKILL.md");
4128
4569
  try {
4129
4570
  const content = await readFile(skillMd, "utf-8");
4130
4571
  const skill = this.parseSkillMd(entry.name, content);
@@ -4135,7 +4576,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4135
4576
  } else if (entry.name.endsWith(".md")) {
4136
4577
  const cmdName = entry.name.replace(/\.md$/, "");
4137
4578
  try {
4138
- const content = await readFile(path10.join(dir, entry.name), "utf-8");
4579
+ const content = await readFile(path11.join(dir, entry.name), "utf-8");
4139
4580
  const skill = this.parseSkillMd(cmdName, content);
4140
4581
  skill.sourcePath = dir;
4141
4582
  skills.push(skill);
@@ -4350,7 +4791,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4350
4791
  logger.info(
4351
4792
  `[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
4352
4793
  );
4353
- this.broadcastActivity(agentId, "working", "Message received");
4794
+ if (reason === "turn_end") {
4795
+ this.broadcastActivity(agentId, "working", "Message received");
4796
+ }
4354
4797
  if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
4355
4798
  return true;
4356
4799
  }
@@ -4527,6 +4970,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4527
4970
  this.broadcastActivity(agentId, "online", "Idle");
4528
4971
  }
4529
4972
  this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
4973
+ if (ap.driver.terminateProcessOnTurnEnd) {
4974
+ ap.expectedTerminationReason = "turn_end";
4975
+ logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
4976
+ try {
4977
+ ap.process.kill("SIGTERM");
4978
+ } catch (err) {
4979
+ const reason = err instanceof Error ? err.message : String(err);
4980
+ logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after turn_end: ${reason}`);
4981
+ }
4982
+ }
4530
4983
  }
4531
4984
  if (event.sessionId) {
4532
4985
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
@@ -4588,7 +5041,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4588
5041
  if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
4589
5042
  const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
4590
5043
  console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
4591
- this.broadcastActivity(agentId, "working", "Message received");
4592
5044
  if (this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy")) {
4593
5045
  return;
4594
5046
  }
@@ -4607,11 +5059,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
4607
5059
  if (messages.length === 0) return true;
4608
5060
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
4609
5061
 
4610
- ${formatIncomingMessage(messages[0])}
5062
+ ${formatIncomingMessage(messages[0], ap.driver)}
4611
5063
 
4612
5064
  Respond as appropriate. Complete all your work before stopping.` : `New messages received:
4613
5065
 
4614
- ${messages.map((message) => formatIncomingMessage(message)).join("\n")}
5066
+ ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
4615
5067
 
4616
5068
  Respond as appropriate. Complete all your work before stopping.`);
4617
5069
  const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
@@ -4649,8 +5101,8 @@ Respond as appropriate. Complete all your work before stopping.`);
4649
5101
  const nodes = [];
4650
5102
  for (const entry of entries) {
4651
5103
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
4652
- const fullPath = path10.join(dir, entry.name);
4653
- const relativePath = path10.relative(rootDir, fullPath);
5104
+ const fullPath = path11.join(dir, entry.name);
5105
+ const relativePath = path11.relative(rootDir, fullPath);
4654
5106
  let info;
4655
5107
  try {
4656
5108
  info = await stat2(fullPath);
@@ -4881,10 +5333,10 @@ var ReminderCache = class {
4881
5333
 
4882
5334
  // src/machineLock.ts
4883
5335
  import { createHash, randomUUID as randomUUID2 } from "crypto";
4884
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
4885
- import os4 from "os";
4886
- import path11 from "path";
4887
- var DEFAULT_MACHINE_STATE_ROOT = path11.join(os4.homedir(), ".slock", "machines");
5336
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
5337
+ import os5 from "os";
5338
+ import path12 from "path";
5339
+ var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
4888
5340
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
4889
5341
  var DaemonMachineLockConflictError = class extends Error {
4890
5342
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -4903,11 +5355,11 @@ function getDaemonMachineLockId(apiKey) {
4903
5355
  return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
4904
5356
  }
4905
5357
  function ownerPath(lockDir) {
4906
- return path11.join(lockDir, "owner.json");
5358
+ return path12.join(lockDir, "owner.json");
4907
5359
  }
4908
5360
  function readOwner(lockDir) {
4909
5361
  try {
4910
- return JSON.parse(readFileSync3(ownerPath(lockDir), "utf8"));
5362
+ return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
4911
5363
  } catch {
4912
5364
  return null;
4913
5365
  }
@@ -4933,8 +5385,8 @@ function acquireDaemonMachineLock(options) {
4933
5385
  const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
4934
5386
  const fingerprint = apiKeyFingerprint(options.apiKey);
4935
5387
  const lockId = getDaemonMachineLockId(options.apiKey);
4936
- const machineDir = path11.join(rootDir, lockId);
4937
- const lockDir = path11.join(machineDir, "daemon.lock");
5388
+ const machineDir = path12.join(rootDir, lockId);
5389
+ const lockDir = path12.join(machineDir, "daemon.lock");
4938
5390
  const token = randomUUID2();
4939
5391
  mkdirSync5(machineDir, { recursive: true });
4940
5392
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -4943,7 +5395,7 @@ function acquireDaemonMachineLock(options) {
4943
5395
  const owner = {
4944
5396
  pid: process.pid,
4945
5397
  token,
4946
- hostname: os4.hostname(),
5398
+ hostname: os5.hostname(),
4947
5399
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4948
5400
  serverUrl: options.serverUrl,
4949
5401
  apiKeyFingerprint: fingerprint.slice(0, 16)
@@ -5006,23 +5458,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
5006
5458
  }
5007
5459
  }
5008
5460
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
5009
- const dirname = path12.dirname(fileURLToPath(moduleUrl));
5010
- const jsPath = path12.resolve(dirname, "chat-bridge.js");
5461
+ const dirname = path13.dirname(fileURLToPath(moduleUrl));
5462
+ const jsPath = path13.resolve(dirname, "chat-bridge.js");
5011
5463
  try {
5012
5464
  accessSync(jsPath);
5013
5465
  return jsPath;
5014
5466
  } catch {
5015
- return path12.resolve(dirname, "chat-bridge.ts");
5467
+ return path13.resolve(dirname, "chat-bridge.ts");
5016
5468
  }
5017
5469
  }
5018
5470
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
5019
- const thisDir = path12.dirname(fileURLToPath(moduleUrl));
5020
- const bundledDistPath = path12.resolve(thisDir, "cli", "index.js");
5471
+ const thisDir = path13.dirname(fileURLToPath(moduleUrl));
5472
+ const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
5021
5473
  try {
5022
5474
  accessSync(bundledDistPath);
5023
5475
  return bundledDistPath;
5024
5476
  } catch {
5025
- const workspaceDistPath = path12.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5477
+ const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
5026
5478
  accessSync(workspaceDistPath);
5027
5479
  return workspaceDistPath;
5028
5480
  }
@@ -5031,9 +5483,14 @@ function detectRuntimes() {
5031
5483
  const ids = [];
5032
5484
  const versions = {};
5033
5485
  for (const runtime of RUNTIMES) {
5486
+ const driver = getDriver(runtime.id);
5034
5487
  try {
5035
- const probe = getDriver(runtime.id).probe?.();
5036
- if (probe?.available) {
5488
+ if (driver.probe) {
5489
+ const probe = driver.probe();
5490
+ if (!probe.available) {
5491
+ if (probe.version) versions[runtime.id] = probe.version;
5492
+ continue;
5493
+ }
5037
5494
  ids.push(runtime.id);
5038
5495
  if (probe.version) versions[runtime.id] = probe.version;
5039
5496
  continue;
@@ -5134,7 +5591,7 @@ var DaemonCore = class {
5134
5591
  }
5135
5592
  resolveMachineStateRoot() {
5136
5593
  if (this.options.machineStateDir) return this.options.machineStateDir;
5137
- if (this.options.dataDir) return path12.join(path12.dirname(this.options.dataDir), "machines");
5594
+ if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
5138
5595
  return DEFAULT_MACHINE_STATE_ROOT;
5139
5596
  }
5140
5597
  start() {
@@ -5325,8 +5782,8 @@ var DaemonCore = class {
5325
5782
  capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
5326
5783
  runtimes,
5327
5784
  runningAgents: this.agentManager.getRunningAgentIds(),
5328
- hostname: this.options.hostname ?? os5.hostname(),
5329
- os: this.options.osDescription ?? `${os5.platform()} ${os5.arch()}`,
5785
+ hostname: this.options.hostname ?? os6.hostname(),
5786
+ os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
5330
5787
  daemonVersion: this.daemonVersion
5331
5788
  });
5332
5789
  for (const agentId of this.agentManager.getRunningAgentIds()) {
package/dist/cli/index.js CHANGED
@@ -1397,7 +1397,8 @@ var RUNTIMES = [
1397
1397
  { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
1398
1398
  { id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
1399
1399
  { id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
1400
- { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true }
1400
+ { id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
1401
+ { id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
1401
1402
  ];
1402
1403
  function getRuntimeDisplayName(id) {
1403
1404
  return RUNTIMES.find((r) => r.id === id)?.displayName ?? id;
@@ -1552,6 +1553,8 @@ var FILENAME_MIME_MAP2 = {
1552
1553
  ".gif": "image/gif",
1553
1554
  ".webp": "image/webp"
1554
1555
  };
1556
+ var MAX_PROFILE_DESCRIPTION_LENGTH = 3e3;
1557
+ var MAX_PROFILE_DISPLAY_NAME_LENGTH = 80;
1555
1558
  function inferImageMimeType(filename, buffer) {
1556
1559
  const lowerFilename = filename.toLowerCase();
1557
1560
  if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
@@ -1596,7 +1599,7 @@ function readAvatarFile(avatarFile) {
1596
1599
  return { filename, buffer, mimeType };
1597
1600
  }
1598
1601
  function registerProfileUpdateCommand(parent) {
1599
- parent.command("update").description("Update your own profile").requiredOption("--avatar-file <path>", "Path to a local image file to use as your avatar").option("--json", "Emit machine-readable JSON").action(async (opts) => {
1602
+ parent.command("update").description("Update your own profile").option("--avatar-file <path>", "Path to a local image file to use as your avatar").option("--display-name <name>", "Set your display name (non-empty)").option("--description <text>", "Set your profile description (non-empty)").option("--json", "Emit machine-readable JSON").action(async (opts) => {
1600
1603
  let ctx;
1601
1604
  try {
1602
1605
  ctx = loadAgentContext();
@@ -1604,28 +1607,75 @@ function registerProfileUpdateCommand(parent) {
1604
1607
  if (err instanceof AgentBootstrapError) fail(err.code, err.message);
1605
1608
  throw err;
1606
1609
  }
1607
- if (!opts.avatarFile) {
1608
- fail("INVALID_ARG", "--avatar-file is required");
1610
+ const hasAvatar = opts.avatarFile !== void 0;
1611
+ const hasDisplayName = opts.displayName !== void 0;
1612
+ const hasDescription = opts.description !== void 0;
1613
+ if (!hasAvatar && !hasDisplayName && !hasDescription) {
1614
+ fail("INVALID_ARG", "Provide at least one of --avatar-file, --display-name, or --description");
1615
+ }
1616
+ let trimmedDisplayName;
1617
+ if (hasDisplayName) {
1618
+ trimmedDisplayName = opts.displayName.trim();
1619
+ if (trimmedDisplayName.length === 0) {
1620
+ fail("INVALID_ARG", "--display-name must not be empty");
1621
+ }
1622
+ if (trimmedDisplayName.length > MAX_PROFILE_DISPLAY_NAME_LENGTH) {
1623
+ fail("INVALID_ARG", `--display-name must be at most ${MAX_PROFILE_DISPLAY_NAME_LENGTH} characters`);
1624
+ }
1625
+ }
1626
+ if (hasDescription) {
1627
+ if (opts.description.length === 0) {
1628
+ fail("INVALID_ARG", "--description must not be empty");
1629
+ }
1630
+ if (opts.description.length > MAX_PROFILE_DESCRIPTION_LENGTH) {
1631
+ fail("INVALID_ARG", `--description must be at most ${MAX_PROFILE_DESCRIPTION_LENGTH} characters`);
1632
+ }
1609
1633
  }
1610
- const avatar = readAvatarFile(opts.avatarFile);
1611
- const form = new FormData();
1612
- const avatarBytes = Uint8Array.from(avatar.buffer);
1613
- form.append("avatar", new Blob([avatarBytes], { type: avatar.mimeType }), avatar.filename);
1614
1634
  const client = new ApiClient(ctx);
1615
- const res = await client.requestMultipart(
1616
- "POST",
1617
- `/internal/agent/${encodeURIComponent(ctx.agentId)}/profile/avatar`,
1618
- form
1619
- );
1620
- if (!res.ok || !res.data) {
1621
- const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
1622
- fail(code, res.error ?? `HTTP ${res.status}`);
1635
+ let latestProfile = null;
1636
+ if (hasDisplayName || hasDescription) {
1637
+ const body = {};
1638
+ if (hasDisplayName) {
1639
+ body.displayName = trimmedDisplayName;
1640
+ }
1641
+ if (hasDescription) {
1642
+ body.description = opts.description;
1643
+ }
1644
+ const res = await client.request(
1645
+ "POST",
1646
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/profile`,
1647
+ body
1648
+ );
1649
+ if (!res.ok || !res.data) {
1650
+ const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
1651
+ fail(code, res.error ?? `HTTP ${res.status}`);
1652
+ }
1653
+ latestProfile = res.data;
1654
+ }
1655
+ if (hasAvatar) {
1656
+ const avatar = readAvatarFile(opts.avatarFile);
1657
+ const form = new FormData();
1658
+ const avatarBytes = Uint8Array.from(avatar.buffer);
1659
+ form.append("avatar", new Blob([avatarBytes], { type: avatar.mimeType }), avatar.filename);
1660
+ const res = await client.requestMultipart(
1661
+ "POST",
1662
+ `/internal/agent/${encodeURIComponent(ctx.agentId)}/profile/avatar`,
1663
+ form
1664
+ );
1665
+ if (!res.ok || !res.data) {
1666
+ const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
1667
+ fail(code, res.error ?? `HTTP ${res.status}`);
1668
+ }
1669
+ latestProfile = res.data;
1670
+ }
1671
+ if (!latestProfile) {
1672
+ fail("PROFILE_UPDATE_FAILED", "No profile returned from server");
1623
1673
  }
1624
1674
  if (opts.json) {
1625
- emit({ ok: true, data: res.data });
1675
+ emit({ ok: true, data: latestProfile });
1626
1676
  return;
1627
1677
  }
1628
- process.stdout.write(`${formatProfile(res.data)}
1678
+ process.stdout.write(`${formatProfile(latestProfile)}
1629
1679
  `);
1630
1680
  });
1631
1681
  }
package/dist/core.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSlockCliPath,
10
10
  resolveWorkspaceDirectoryPath,
11
11
  scanWorkspaceDirectories
12
- } from "./chunk-RNEIFBXW.js";
12
+ } from "./chunk-37O7EHYE.js";
13
13
  import {
14
14
  subscribeDaemonLogs
15
15
  } from "./chunk-JG7ONJZ6.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DAEMON_CLI_USAGE,
4
4
  DaemonCore,
5
5
  parseDaemonCliArgs
6
- } from "./chunk-RNEIFBXW.js";
6
+ } from "./chunk-37O7EHYE.js";
7
7
  import "./chunk-JG7ONJZ6.js";
8
8
 
9
9
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"