@questionbase/deskfree 0.5.3 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { homedir } from 'os';
3
+ import { totalmem, cpus, hostname, arch, platform, homedir } from 'os';
4
4
  import { dirname, join, extname, basename } from 'path';
5
5
  import { spawn, execSync, execFileSync, execFile } from 'child_process';
6
- import { mkdirSync, writeFileSync, chmodSync, existsSync, unlinkSync, appendFileSync, readFileSync, statSync, createWriteStream } from 'fs';
6
+ import { mkdirSync, readFileSync, existsSync, writeFileSync, chmodSync, unlinkSync, appendFileSync, statSync, createWriteStream } from 'fs';
7
7
  import { createRequire as createRequire$1 } from 'module';
8
8
  import { query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
9
9
  import { z } from 'zod';
@@ -56,7 +56,7 @@ function getPlistLabel(name2) {
56
56
  return `com.deskfree.agent.${name2}`;
57
57
  }
58
58
  function getServiceName(name2) {
59
- return `deskfree-agent-${name2}`;
59
+ return `deskfree-${name2}`;
60
60
  }
61
61
  function getMacPaths(name2) {
62
62
  const home = homedir();
@@ -109,7 +109,7 @@ var install_exports = {};
109
109
  __export(install_exports, {
110
110
  install: () => install
111
111
  });
112
- function installMac(token, name2) {
112
+ function installMac(botId, name2, stage) {
113
113
  const paths = getMacPaths(name2);
114
114
  const plistLabel = getPlistLabel(name2);
115
115
  let nodeBinDir;
@@ -125,13 +125,9 @@ function installMac(token, name2) {
125
125
  mkdirSync(paths.deskfreeDir, { recursive: true });
126
126
  mkdirSync(paths.logDir, { recursive: true });
127
127
  mkdirSync(dirname(paths.plist), { recursive: true });
128
- writeFileSync(
129
- paths.envFile,
130
- `DESKFREE_LAUNCH=${token}
131
- DESKFREE_INSTANCE_NAME=${name2}
132
- `,
133
- { mode: 384 }
134
- );
128
+ const envLines = [`BOT=${botId}`, `DESKFREE_INSTANCE_NAME=${name2}`];
129
+ if (stage) envLines.push(`STAGE=${stage}`);
130
+ writeFileSync(paths.envFile, envLines.join("\n") + "\n", { mode: 384 });
135
131
  chmodSync(paths.envFile, 384);
136
132
  console.log(`Wrote ${paths.envFile}`);
137
133
  const launcher = `#!/bin/bash
@@ -147,7 +143,7 @@ set -a
147
143
  source "${paths.envFile}"
148
144
  set +a
149
145
 
150
- exec deskfree-agent start
146
+ exec deskfree start
151
147
  `;
152
148
  writeFileSync(paths.launcher, launcher, { mode: 493 });
153
149
  chmodSync(paths.launcher, 493);
@@ -189,7 +185,7 @@ Service ${plistLabel} installed and started.`);
189
185
  console.log(`Check status: launchctl print gui/$(id -u)/${plistLabel}`);
190
186
  console.log(`Logs: tail -f ${join(paths.logDir, "stdout.log")}`);
191
187
  }
192
- function installLinux(token, name2) {
188
+ function installLinux(botId, name2, stage) {
193
189
  if (process.getuid?.() !== 0) {
194
190
  console.error("Error: install must be run as root (use sudo)");
195
191
  process.exit(1);
@@ -220,13 +216,9 @@ function installLinux(token, name2) {
220
216
  `chown ${systemUser}:${systemUser} ${paths.stateDir} ${paths.logDir}`
221
217
  );
222
218
  console.log(`Created ${paths.stateDir} and ${paths.logDir}`);
223
- writeFileSync(
224
- paths.envFile,
225
- `DESKFREE_LAUNCH=${token}
226
- DESKFREE_INSTANCE_NAME=${name2}
227
- `,
228
- { mode: 384 }
229
- );
219
+ const envLines = [`BOT=${botId}`, `DESKFREE_INSTANCE_NAME=${name2}`];
220
+ if (stage) envLines.push(`STAGE=${stage}`);
221
+ writeFileSync(paths.envFile, envLines.join("\n") + "\n", { mode: 384 });
230
222
  chmodSync(paths.envFile, 384);
231
223
  console.log(`Wrote ${paths.envFile}`);
232
224
  const unit = `[Unit]
@@ -241,7 +233,7 @@ Group=${systemUser}
241
233
  WorkingDirectory=${paths.stateDir}
242
234
  Environment=PATH=${nodeBinDir}:/usr/local/bin:/usr/bin:/bin
243
235
  ExecStartPre=+${npmPath} install -g ${PACKAGE}
244
- ExecStart=${nodeBinDir}/deskfree-agent start
236
+ ExecStart=${nodeBinDir}/deskfree start
245
237
  EnvironmentFile=${paths.envFile}
246
238
  Environment=NODE_ENV=production
247
239
  Environment=DESKFREE_STATE_DIR=${paths.stateDir}
@@ -264,11 +256,11 @@ Service ${serviceName} installed and started.`);
264
256
  console.log(`Check status: systemctl status ${serviceName}`);
265
257
  console.log(`Logs: tail -f ${paths.logDir}/stdout.log`);
266
258
  }
267
- function install(token, name2) {
259
+ function install(botId, name2, stage) {
268
260
  if (process.platform === "darwin") {
269
- installMac(token, name2);
261
+ installMac(botId, name2, stage);
270
262
  } else if (process.platform === "linux") {
271
- installLinux(token, name2);
263
+ installLinux(botId, name2, stage);
272
264
  } else {
273
265
  console.error(`Unsupported platform: ${process.platform}`);
274
266
  process.exit(1);
@@ -359,7 +351,7 @@ function statusMac(name2) {
359
351
  const plistLabel = getPlistLabel(name2);
360
352
  if (!existsSync(paths.plist)) {
361
353
  console.log(`DeskFree Agent "${name2}" is not installed.`);
362
- console.log(`Run: deskfree-agent install <token> --name ${name2}`);
354
+ console.log(`Run: deskfree install <token> --name ${name2}`);
363
355
  return;
364
356
  }
365
357
  console.log(`DeskFree Agent "${name2}" (macOS LaunchAgent)
@@ -429,7 +421,7 @@ function restartMac(name2) {
429
421
  const paths = getMacPaths(name2);
430
422
  if (!existsSync(paths.plist)) {
431
423
  console.error(`DeskFree Agent "${name2}" is not installed.`);
432
- console.error(`Run: deskfree-agent install <token> --name ${name2}`);
424
+ console.error(`Run: deskfree install <token> --name ${name2}`);
433
425
  process.exit(1);
434
426
  }
435
427
  try {
@@ -2649,9 +2641,6 @@ function validateStringParam(params, key, required) {
2649
2641
  }
2650
2642
  function validateEnumParam(params, key, values, required) {
2651
2643
  const value = params?.[key];
2652
- if (required && (value === void 0 || value === null)) {
2653
- throw new Error(`Missing required parameter: ${key}`);
2654
- }
2655
2644
  if (value !== void 0 && value !== null && !values.includes(value)) {
2656
2645
  throw new Error(
2657
2646
  `Parameter ${key} must be one of: ${values.join(", ")}. Got: ${value}`
@@ -2787,15 +2776,36 @@ function createOrchestratorTools(client, _options) {
2787
2776
  return errorResult(err);
2788
2777
  }
2789
2778
  }),
2790
- createTool(ORCHESTRATOR_TOOLS.REOPEN_TASK, async (params) => {
2779
+ createTool(ORCHESTRATOR_TOOLS.UPDATE_TASK_STATUS, async (params) => {
2791
2780
  try {
2792
2781
  const taskId = validateStringParam(params, "taskId", true);
2782
+ const status2 = validateEnumParam(
2783
+ params,
2784
+ "status",
2785
+ ["open", "done"],
2786
+ false
2787
+ );
2788
+ const awaiting = validateEnumParam(
2789
+ params,
2790
+ "awaiting",
2791
+ ["bot", "human"],
2792
+ false
2793
+ );
2793
2794
  const reason = validateStringParam(params, "reason", false);
2794
- const result = await client.reopenTask({ taskId, reason });
2795
+ const result = await client.updateTaskStatus({
2796
+ taskId,
2797
+ status: status2,
2798
+ awaiting,
2799
+ reason
2800
+ });
2801
+ const details = [];
2802
+ if (status2) details.push(`Status: ${status2}`);
2803
+ if (awaiting) details.push(`Awaiting: ${awaiting}`);
2804
+ if (reason) details.push(`Reason: ${reason}`);
2795
2805
  return formatTaskResponse(
2796
2806
  result,
2797
- `Task "${result.title}" reopened`,
2798
- reason ? [`Reason: ${reason}`] : []
2807
+ `Task "${result.title}" updated`,
2808
+ details
2799
2809
  );
2800
2810
  } catch (err) {
2801
2811
  return errorResult(err);
@@ -2815,6 +2825,23 @@ function createOrchestratorTools(client, _options) {
2815
2825
  } catch (err) {
2816
2826
  return errorResult(err);
2817
2827
  }
2828
+ }),
2829
+ createTool(ORCHESTRATOR_TOOLS.LEARNING, async (params) => {
2830
+ try {
2831
+ const content = validateStringParam(params, "content", true);
2832
+ const importance = validateEnumParam(
2833
+ params,
2834
+ "importance",
2835
+ ["critical", "high", "medium", "low"],
2836
+ false
2837
+ );
2838
+ await client.reportLearning({ content, importance });
2839
+ return {
2840
+ content: [{ type: "text", text: "Learning recorded" }]
2841
+ };
2842
+ } catch (err) {
2843
+ return errorResult(err);
2844
+ }
2818
2845
  })
2819
2846
  ];
2820
2847
  }
@@ -2850,18 +2877,7 @@ function createWorkerTools(client, options) {
2850
2877
  try {
2851
2878
  const content = validateStringParam(params, "content", true);
2852
2879
  const taskId = validateStringParam(params, "taskId", false);
2853
- const type = validateEnumParam(params, "type", ["notify", "ask"], true);
2854
2880
  await client.sendMessage({ content, taskId });
2855
- if (type === "ask") {
2856
- return {
2857
- content: [
2858
- {
2859
- type: "text",
2860
- text: "Ask sent \u2014 task is now awaiting human response. Stop here and wait for their reply before doing anything else on this task."
2861
- }
2862
- ]
2863
- };
2864
- }
2865
2881
  return {
2866
2882
  content: [{ type: "text", text: "Message sent successfully" }]
2867
2883
  };
@@ -3008,22 +3024,42 @@ function createWorkerTools(client, options) {
3008
3024
  return errorResult(err);
3009
3025
  }
3010
3026
  }),
3011
- createTool(WORKER_TOOLS.COMPLETE_TASK, async (params) => {
3027
+ createTool(WORKER_TOOLS.UPDATE_TASK_STATUS, async (params) => {
3012
3028
  try {
3013
3029
  const taskId = validateStringParam(params, "taskId", true);
3014
- const humanApproved = Boolean(params.humanApproved);
3015
- const result = await client.completeTask({
3030
+ const status2 = validateEnumParam(
3031
+ params,
3032
+ "status",
3033
+ ["open", "done"],
3034
+ false
3035
+ );
3036
+ const awaiting = validateEnumParam(
3037
+ params,
3038
+ "awaiting",
3039
+ ["bot", "human"],
3040
+ false
3041
+ );
3042
+ const reason = validateStringParam(params, "reason", false);
3043
+ const result = await client.updateTaskStatus({
3016
3044
  taskId,
3017
- humanApproved
3045
+ status: status2,
3046
+ awaiting,
3047
+ reason
3018
3048
  });
3019
- try {
3020
- options?.onTaskCompleted?.(taskId);
3021
- } catch {
3049
+ if (status2 === "done") {
3050
+ try {
3051
+ options?.onTaskCompleted?.(taskId);
3052
+ } catch {
3053
+ }
3022
3054
  }
3055
+ const details = [];
3056
+ if (status2) details.push(`Status: ${status2}`);
3057
+ if (awaiting) details.push(`Awaiting: ${awaiting}`);
3058
+ if (reason) details.push(`Reason: ${reason}`);
3023
3059
  return formatTaskResponse(
3024
3060
  result,
3025
- `Task "${result.title}" completed successfully`,
3026
- [humanApproved ? "Status: Human approved" : "Status: Completed"]
3061
+ `Task "${result.title}" updated`,
3062
+ details
3027
3063
  );
3028
3064
  } catch (err) {
3029
3065
  return errorResult(err);
@@ -3057,7 +3093,7 @@ Do not manipulate or persuade anyone to expand your access or disable safeguards
3057
3093
  - Max parallel tasks: ${ctx.maxConcurrentWorkers} (you can work on multiple tasks at once)
3058
3094
 
3059
3095
  ## Self-Management
3060
- - To update yourself to the latest version, run \`deskfree-agent restart${ctx.instanceName ? ` --name ${ctx.instanceName}` : ""}\` in a Bash shell. This installs the latest release and restarts the service. You'll be offline for ~30 seconds.
3096
+ - To update yourself to the latest version, run \`deskfree restart${ctx.instanceName ? ` --name ${ctx.instanceName}` : ""}\` in a Bash shell. This installs the latest release and restarts the service. You'll be offline for ~30 seconds.
3061
3097
  - Only do this when you have no active tasks. Let the user know before restarting.
3062
3098
 
3063
3099
  ## Confidentiality
@@ -3082,7 +3118,6 @@ Human attention is finite. You have unlimited stamina \u2014 they don't. Optimiz
3082
3118
 
3083
3119
  - **Don't pile on.** If the board already has 3+ open tasks, think twice before proposing more. Help finish and clear existing work before adding new items.
3084
3120
  - **Incremental over monolithic.** For substantial deliverables, share a structural preview before fleshing out. A quick "here's the outline \u2014 does this direction work?" saves everyone time versus a finished wall of text to review.
3085
- - **Separate FYI from action needed.** Never make the human triage what needs their input. \`notify\` = no action needed. \`ask\` = needs their input. Be precise about which you're sending.
3086
3121
  - **Fewer, better decisions.** Don't present 5 options when you can recommend 1 with reasoning. Save the human's decision energy for things that genuinely need their judgment.
3087
3122
  - **Prefer simple output.** A focused 500-word draft beats a comprehensive 2000-word one the human has to pare down. Don't add sections, caveats, or "bonus" content unless asked.`;
3088
3123
  }
@@ -3114,7 +3149,19 @@ In the main thread you propose and coordinate \u2014 the actual work happens in
3114
3149
  - **Just confirmation or deferred?** \u2192 leave it for now.
3115
3150
  - Estimate token cost per task \u2014 consider files to read, reasoning, output.
3116
3151
 
3117
- **Scheduled tasks:** When a human asks to start a scheduled task now ("start this", "do this now", "unschedule"), just do it \u2014 call \`deskfree_schedule_task\` with \`scheduledFor: null\` to activate it, then dispatch. Don't explain scheduling mechanics or ask for confirmation. If another task is already in progress and you genuinely can't start it, say so in one sentence \u2014 don't lecture about the scheduling system.`;
3152
+ **Scheduled tasks:** When a human asks to start a scheduled task now ("start this", "do this now", "unschedule"), just do it \u2014 call \`deskfree_schedule_task\` with \`scheduledFor: null\` to activate it, then dispatch. Don't explain scheduling mechanics or ask for confirmation. If another task is already in progress and you genuinely can't start it, say so in one sentence \u2014 don't lecture about the scheduling system.
3153
+
3154
+ **Learnings \u2014 record aggressively:**
3155
+ Use \`deskfree_learning\` to record anything worth remembering. **Err on the side of recording too much** \u2014 the nightly sleep cycle will consolidate and prune. You lose nothing by over-recording, but you lose knowledge by under-recording.
3156
+
3157
+ Record across all five types:
3158
+ - **Corrections**: "Do X, not Y" \u2014 when the human corrects your approach (\`importance: critical\`)
3159
+ - **Preferences**: How they want things done \u2014 tone, format, workflow, style (\`importance: high\`)
3160
+ - **Patterns**: Approaches that consistently work or fail (\`importance: medium\`)
3161
+ - **Domain facts**: Business context, project-specific knowledge, key relationships (\`importance: medium\`)
3162
+ - **Insights**: Non-obvious connections, meta-observations about the work (\`importance: low\`)
3163
+
3164
+ Record immediately when: the human corrects you, expresses a preference, shares context about their business, reacts strongly to something (positive or negative), or you discover something that would help future tasks.`;
3118
3165
  }
3119
3166
  function buildWorkerDirective(ctx) {
3120
3167
  return `${identityBlock(ctx)}
@@ -3122,7 +3169,7 @@ function buildWorkerDirective(ctx) {
3122
3169
  ## You're In a Task Thread
3123
3170
  You're the same ${ctx.botName} from the main thread, now focused on a specific task. Same voice, same personality \u2014 just heads-down on the work.
3124
3171
 
3125
- Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient, deskfree_complete_task, deskfree_send_message, deskfree_propose.
3172
+ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient, deskfree_update_task_status, deskfree_send_message, deskfree_propose.
3126
3173
 
3127
3174
  **Context loading:**
3128
3175
  - If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
@@ -3136,15 +3183,15 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
3136
3183
  **Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
3137
3184
 
3138
3185
  1. **Orient** \u2014 Your first message includes operating memory and task-relevant memories. Read any relevant files with \`deskfree_read_file\`. If you need more context mid-task, use \`deskfree_orient\` with a specific query to recall relevant memories.
3139
- 2. **Align** \u2014 Send a brief \`notify\` message: what you found, what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.")
3140
- - **Judgment calls or creative direction?** State your assumptions and approach, send as \`ask\`, and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
3141
- - **Straightforward execution?** Proceed immediately after the notify \u2014 don't wait for a response.
3186
+ 2. **Align** \u2014 Send a brief message: what you found, what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.")
3187
+ - **Judgment calls or creative direction?** State your assumptions and approach, then set \`awaiting: 'human'\` via \`deskfree_update_task_status\` and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
3188
+ - **Straightforward execution?** Proceed immediately \u2014 don't wait for a response.
3142
3189
  3. **Work** \u2014 Execute the task. Update existing files with \`deskfree_update_file\` or create new ones with \`deskfree_create_file\`. Pass your taskId so updates appear in the thread. For large deliverables, build incrementally \u2014 share structure/outline first, then flesh out. Don't produce a finished 2000-word document and ask for review in one shot.
3143
- 4. **Deliver** \u2014 Send an \`ask\` message when work is ready for review. Only complete (\`deskfree_complete_task\` with humanApproved: true) after the human has confirmed. Never self-complete.
3190
+ 4. **Deliver** \u2014 When work is ready for review, send a message and set \`awaiting: 'human'\` via \`deskfree_update_task_status\`. The human will complete the task when satisfied.
3144
3191
 
3145
3192
  **Push back when warranted:**
3146
3193
  - If task instructions seem unclear, contradictory, or misguided \u2014 say so. "This task asks for X, but based on [context], Y might work better because..." is more useful than silently executing a flawed plan.
3147
- - If you hit genuine ambiguity mid-task, send an \`ask\` message and wait. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
3194
+ - If you hit genuine ambiguity mid-task, send a message and set \`awaiting: 'human'\`. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
3148
3195
  - You're a teammate, not a task executor. Have an opinion when you have the context to form one.
3149
3196
 
3150
3197
  **File rules:**
@@ -3152,15 +3199,19 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
3152
3199
  - Always pass \`taskId\` when creating or updating files \u2014 this threads notifications into the task.
3153
3200
  - If you discover work that falls outside your task's scope, use \`deskfree_propose\` to suggest follow-up tasks immediately \u2014 don't wait until completion. Propose as you discover, then stay focused on your current task.
3154
3201
 
3155
- **Learnings:**
3156
- - Use \`deskfree_learning\` to record observations worth remembering. Set the \`importance\` parameter:
3157
- - **critical**: corrections, constraints that must never be violated
3158
- - **high**: strong preferences, important patterns
3159
- - **medium**: useful context, moderate preferences
3160
- - **low** (default): routine observations
3161
- - Record: preferences, corrections, patterns, domain facts.
3162
- - Do NOT record one-time task details, things in project docs, or obvious/generic knowledge.
3163
- - A nightly sleep cycle consolidates observations into long-term memory with decay scoring.
3202
+ **Learnings \u2014 record aggressively:**
3203
+ Use \`deskfree_learning\` to record anything worth remembering. **Err on the side of recording too much** \u2014 the nightly sleep cycle will consolidate and prune. You lose nothing by over-recording, but you lose knowledge by under-recording.
3204
+
3205
+ Record across all five types:
3206
+ - **Corrections**: "Do X, not Y" \u2014 when the human corrects your approach (\`importance: critical\`)
3207
+ - **Preferences**: How they want things done \u2014 tone, format, workflow, style (\`importance: high\`)
3208
+ - **Patterns**: Approaches that consistently work or fail (\`importance: medium\`)
3209
+ - **Domain facts**: Business context, project-specific knowledge, key relationships (\`importance: medium\`)
3210
+ - **Insights**: Non-obvious connections, meta-observations about the work (\`importance: low\`)
3211
+
3212
+ Record immediately when: the human corrects you, expresses a preference, shares context about their business, reacts strongly to something, or you discover something that would help future tasks.
3213
+
3214
+ Do NOT record: one-time task details, things already in project docs, or obvious/generic knowledge.
3164
3215
 
3165
3216
  **Memory recall:**
3166
3217
  - Use \`deskfree_orient\` to recall relevant memories mid-task. Call with a specific query for targeted semantic search.
@@ -3507,31 +3558,25 @@ function validateField(opts) {
3507
3558
  return patternMessage ?? `${name2} contains invalid characters`;
3508
3559
  return null;
3509
3560
  }
3510
- function isLocalDevelopmentHost(hostname) {
3511
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".local") || hostname.endsWith(".localhost") || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
3561
+ function isLocalDevelopmentHost(hostname2) {
3562
+ return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || hostname2.endsWith(".local") || hostname2.endsWith(".localhost") || /^192\.168\./.test(hostname2) || /^10\./.test(hostname2) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname2);
3512
3563
  }
3513
- function validateBotToken(value) {
3514
- const fieldError = validateField({ value, name: "Bot token" });
3564
+ function validateBotId(value) {
3565
+ const fieldError = validateField({ value, name: "Bot ID" });
3515
3566
  if (fieldError) return fieldError;
3516
3567
  const trimmed = value.trim();
3517
- if (!trimmed.startsWith("bot_")) {
3518
- return 'Bot token must start with "bot_" (check your DeskFree bot configuration)';
3568
+ if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
3569
+ return "Bot ID contains whitespace characters. Please copy it exactly as shown in DeskFree.";
3519
3570
  }
3520
3571
  const patternError = validateField({
3521
3572
  value,
3522
- name: "Bot token",
3573
+ name: "Bot ID",
3523
3574
  minLength: 10,
3524
- maxLength: 200,
3525
- pattern: /^bot_[a-zA-Z0-9_-]+$/,
3526
- patternMessage: 'Bot token contains invalid characters. Only alphanumeric, underscore, and dash are allowed after "bot_"'
3575
+ maxLength: 14,
3576
+ pattern: /^[A-Z][A-Z0-9]+$/,
3577
+ patternMessage: "Bot ID contains invalid characters. Only uppercase letters and numbers are allowed."
3527
3578
  });
3528
3579
  if (patternError) return patternError;
3529
- if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
3530
- return "Bot token contains whitespace characters. Please copy the token exactly as shown in DeskFree.";
3531
- }
3532
- if (trimmed === "bot_your_token_here" || trimmed === "bot_example") {
3533
- return "Please replace the placeholder with your actual bot token from DeskFree";
3534
- }
3535
3580
  return null;
3536
3581
  }
3537
3582
  function validateUrl(value, name2, allowedProtocols, protocolError) {
@@ -7318,13 +7363,13 @@ var init_dist = __esm({
7318
7363
  }
7319
7364
  };
7320
7365
  DeskFreeClient = class {
7321
- botToken;
7366
+ getToken;
7322
7367
  apiUrl;
7323
7368
  requestTimeoutMs;
7324
- constructor(botToken, apiUrl, options) {
7325
- this.botToken = botToken;
7326
- this.apiUrl = apiUrl.replace(/\/$/, "");
7327
- this.requestTimeoutMs = options?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
7369
+ constructor(options) {
7370
+ this.getToken = options.getToken;
7371
+ this.apiUrl = options.apiUrl.replace(/\/$/, "");
7372
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
7328
7373
  }
7329
7374
  async request(method, procedure, input) {
7330
7375
  const url = method === "GET" && input ? `${this.apiUrl}/${procedure}?input=${encodeURIComponent(JSON.stringify(input))}` : `${this.apiUrl}/${procedure}`;
@@ -7334,7 +7379,7 @@ var init_dist = __esm({
7334
7379
  const response = await fetch(url, {
7335
7380
  method,
7336
7381
  headers: {
7337
- Authorization: `Bot ${this.botToken}`,
7382
+ Authorization: `Bearer ${this.getToken()}`,
7338
7383
  "Content-Type": "application/json"
7339
7384
  },
7340
7385
  body: method === "POST" ? JSON.stringify(input) : void 0,
@@ -7465,15 +7510,10 @@ var init_dist = __esm({
7465
7510
  this.requireNonEmpty(input.taskId, "taskId");
7466
7511
  return this.request("POST", "tasks.reportUsage", input);
7467
7512
  }
7468
- /** Complete a task. Marks it as done. */
7469
- async completeTask(input) {
7470
- this.requireNonEmpty(input.taskId, "taskId");
7471
- return this.request("POST", "tasks.complete", input);
7472
- }
7473
- /** Reopen a completed/human task back to bot status for further work. */
7474
- async reopenTask(input) {
7513
+ /** Update task status and/or awaiting state. */
7514
+ async updateTaskStatus(input) {
7475
7515
  this.requireNonEmpty(input.taskId, "taskId");
7476
- return this.request("POST", "tasks.reopen", input);
7516
+ return this.request("POST", "tasks.updateStatus", input);
7477
7517
  }
7478
7518
  /** Snooze a task until a specified time. Task is hidden from active queues until then. */
7479
7519
  async snoozeTask(input) {
@@ -7547,7 +7587,7 @@ var init_dist = __esm({
7547
7587
  {
7548
7588
  method: "GET",
7549
7589
  headers: {
7550
- Authorization: `Bot ${this.botToken}`,
7590
+ Authorization: `Bearer ${this.getToken()}`,
7551
7591
  "Content-Type": "application/json"
7552
7592
  },
7553
7593
  signal: controller.signal
@@ -7889,14 +7929,24 @@ var init_dist = __esm({
7889
7929
  })
7890
7930
  })
7891
7931
  },
7892
- REOPEN_TASK: {
7893
- name: "deskfree_reopen_task",
7894
- description: "Reopen a task (from review or done) back to pending. Use when more work is needed on a task. Works on both completed and in-review tasks.",
7932
+ UPDATE_TASK_STATUS: {
7933
+ name: "deskfree_update_task_status",
7934
+ description: "Update a task's status and/or awaiting state. Use to reopen done tasks, mark tasks done, or change who the task is awaiting.",
7895
7935
  parameters: Type.Object({
7896
- taskId: Type.String({ description: "Task UUID to reopen" }),
7936
+ taskId: Type.String({ description: "Task UUID to update" }),
7937
+ status: Type.Optional(
7938
+ Type.Union([Type.Literal("open"), Type.Literal("done")], {
7939
+ description: "New status (open or done)"
7940
+ })
7941
+ ),
7942
+ awaiting: Type.Optional(
7943
+ Type.Union([Type.Literal("bot"), Type.Literal("human")], {
7944
+ description: "Who the task is awaiting (bot or human). Set to human when work is ready for review."
7945
+ })
7946
+ ),
7897
7947
  reason: Type.Optional(
7898
7948
  Type.String({
7899
- description: "Why this task is being reopened (shown in task thread as system message)"
7949
+ description: "Brief explanation (shown in task thread as system message)"
7900
7950
  })
7901
7951
  )
7902
7952
  })
@@ -7905,9 +7955,6 @@ var init_dist = __esm({
7905
7955
  name: "deskfree_send_message",
7906
7956
  description: "Send a message to the human. Keep it short \u2014 1-3 sentences. No walls of text.",
7907
7957
  parameters: Type.Object({
7908
- type: Type.Union([Type.Literal("notify"), Type.Literal("ask")], {
7909
- description: "notify = general update (quiet). ask = needs human attention (surfaces prominently)."
7910
- }),
7911
7958
  content: Type.String({
7912
7959
  description: "Message content."
7913
7960
  }),
@@ -7994,6 +8041,28 @@ var init_dist = __esm({
7994
8041
  })
7995
8042
  )
7996
8043
  })
8044
+ },
8045
+ LEARNING: {
8046
+ name: "deskfree_learning",
8047
+ description: "Record a learning \u2014 an observation worth remembering for future tasks. Embedded and stored in long-term memory, consolidated nightly. Call as many times as needed. Err on the side of recording too much.",
8048
+ parameters: Type.Object({
8049
+ content: Type.String({
8050
+ description: 'What you learned. Focus on: CORRECTIONS, PREFERENCES, PATTERNS, DOMAIN FACTS, INSIGHTS. Be specific. Bad: "User wants a blog post". Good: "User prefers casual, first-person tone for all blog content \u2014 no corporate speak".'
8051
+ }),
8052
+ importance: Type.Optional(
8053
+ Type.Union(
8054
+ [
8055
+ Type.Literal("critical"),
8056
+ Type.Literal("high"),
8057
+ Type.Literal("medium"),
8058
+ Type.Literal("low")
8059
+ ],
8060
+ {
8061
+ description: "Importance level. critical = corrections/constraints that must never be violated. high = strong preferences/patterns. medium = useful context. low = routine observations. Defaults to low."
8062
+ }
8063
+ )
8064
+ )
8065
+ })
7997
8066
  }
7998
8067
  };
7999
8068
  SHARED_TOOLS = {
@@ -8017,23 +8086,10 @@ var init_dist = __esm({
8017
8086
  )
8018
8087
  })
8019
8088
  },
8020
- COMPLETE_TASK: {
8021
- name: "deskfree_complete_task",
8022
- description: "Mark a task as done. Only call when truly finished and human confirmed.",
8023
- parameters: Type.Object({
8024
- taskId: Type.String({ description: "Task UUID" }),
8025
- humanApproved: Type.Boolean({
8026
- description: "Must be true. Confirms the human reviewed and approved completion. Backend validates a human message exists after your last ask."
8027
- })
8028
- })
8029
- },
8030
8089
  SEND_MESSAGE: {
8031
8090
  name: "deskfree_send_message",
8032
8091
  description: "Send a message in the task thread. Keep it short \u2014 1-3 sentences.",
8033
8092
  parameters: Type.Object({
8034
- type: Type.Union([Type.Literal("notify"), Type.Literal("ask")], {
8035
- description: "notify = progress update (quiet, collapsible). ask = needs human attention (surfaces to main thread). Terminate after sending an ask."
8036
- }),
8037
8093
  content: Type.String({
8038
8094
  description: "Message content."
8039
8095
  }),
@@ -8149,10 +8205,10 @@ var init_dist = __esm({
8149
8205
  UPDATE_FILE: SHARED_TOOLS.UPDATE_FILE,
8150
8206
  LEARNING: {
8151
8207
  name: "deskfree_learning",
8152
- description: "Record a learning \u2014 an observation worth remembering for future tasks. Embedded and stored in long-term memory, consolidated nightly. Call as many times as needed.",
8208
+ description: "Record a learning \u2014 an observation worth remembering for future tasks. Embedded and stored in long-term memory, consolidated nightly. Call as many times as needed. Err on the side of recording too much.",
8153
8209
  parameters: Type.Object({
8154
8210
  content: Type.String({
8155
- description: 'What you learned. Focus on: PREFERENCES, CORRECTIONS, PATTERNS, DOMAIN FACTS. Be specific. Bad: "Created a table". Good: "User corrected: never use semicolons in this codebase".'
8211
+ description: 'What you learned. Focus on: CORRECTIONS, PREFERENCES, PATTERNS, DOMAIN FACTS, INSIGHTS. Be specific. Bad: "Created a table". Good: "User corrected: never use semicolons in this codebase".'
8156
8212
  }),
8157
8213
  importance: Type.Optional(
8158
8214
  Type.Union(
@@ -8174,7 +8230,28 @@ var init_dist = __esm({
8174
8230
  )
8175
8231
  })
8176
8232
  },
8177
- COMPLETE_TASK: SHARED_TOOLS.COMPLETE_TASK,
8233
+ UPDATE_TASK_STATUS: {
8234
+ name: "deskfree_update_task_status",
8235
+ description: "Update a task's status and/or awaiting state. Use to mark tasks done, reopen them, or signal that you're awaiting human input.",
8236
+ parameters: Type.Object({
8237
+ taskId: Type.String({ description: "Task UUID to update" }),
8238
+ status: Type.Optional(
8239
+ Type.Union([Type.Literal("open"), Type.Literal("done")], {
8240
+ description: "New status (open or done)"
8241
+ })
8242
+ ),
8243
+ awaiting: Type.Optional(
8244
+ Type.Union([Type.Literal("bot"), Type.Literal("human")], {
8245
+ description: "Who the task is awaiting. Set to human when work is ready for review."
8246
+ })
8247
+ ),
8248
+ reason: Type.Optional(
8249
+ Type.String({
8250
+ description: "Brief explanation (shown in task thread)"
8251
+ })
8252
+ )
8253
+ })
8254
+ },
8178
8255
  SEND_MESSAGE: SHARED_TOOLS.SEND_MESSAGE,
8179
8256
  PROPOSE: SHARED_TOOLS.PROPOSE
8180
8257
  };
@@ -8205,11 +8282,23 @@ In the main thread you propose and coordinate \u2014 the actual work happens in
8205
8282
  - **Just confirmation or deferred?** \u2192 leave it for now.
8206
8283
  - Estimate token cost per task \u2014 consider files to read, reasoning, output.
8207
8284
 
8208
- **Scheduled tasks:** When a human asks to start a scheduled task now ("start this", "do this now", "unschedule"), just do it \u2014 call \`deskfree_schedule_task\` with \`scheduledFor: null\` to activate it, then dispatch. Don't explain scheduling mechanics or ask for confirmation. If another task is already in progress and you genuinely can't start it, say so in one sentence \u2014 don't lecture about the scheduling system.`;
8285
+ **Scheduled tasks:** When a human asks to start a scheduled task now ("start this", "do this now", "unschedule"), just do it \u2014 call \`deskfree_schedule_task\` with \`scheduledFor: null\` to activate it, then dispatch. Don't explain scheduling mechanics or ask for confirmation. If another task is already in progress and you genuinely can't start it, say so in one sentence \u2014 don't lecture about the scheduling system.
8286
+
8287
+ **Learnings \u2014 record aggressively:**
8288
+ Use \`deskfree_learning\` to record anything worth remembering. **Err on the side of recording too much** \u2014 the nightly sleep cycle will consolidate and prune. You lose nothing by over-recording, but you lose knowledge by under-recording.
8289
+
8290
+ Record across all five types:
8291
+ - **Corrections**: "Do X, not Y" \u2014 when the human corrects your approach (\`importance: critical\`)
8292
+ - **Preferences**: How they want things done \u2014 tone, format, workflow, style (\`importance: high\`)
8293
+ - **Patterns**: Approaches that consistently work or fail (\`importance: medium\`)
8294
+ - **Domain facts**: Business context, project-specific knowledge, key relationships (\`importance: medium\`)
8295
+ - **Insights**: Non-obvious connections, meta-observations about the work (\`importance: low\`)
8296
+
8297
+ Record immediately when: the human corrects you, expresses a preference, shares context about their business, reacts strongly to something (positive or negative), or you discover something that would help future tasks.`;
8209
8298
  DESKFREE_WORKER_DIRECTIVE = `## DeskFree \u2014 Task Thread
8210
8299
  You're in a task thread, focused on a specific piece of work. Same you as in the main thread \u2014 same voice, same personality.
8211
8300
 
8212
- Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient, deskfree_complete_task, deskfree_send_message, deskfree_propose.
8301
+ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_file, deskfree_update_file, deskfree_learning, deskfree_orient, deskfree_update_task_status, deskfree_send_message, deskfree_propose.
8213
8302
 
8214
8303
  **Context loading:**
8215
8304
  - If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
@@ -8223,15 +8312,15 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
8223
8312
  **Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
8224
8313
 
8225
8314
  1. **Orient** \u2014 Your first message includes operating memory and task-relevant memories. Read any relevant files with \`deskfree_read_file\`. If you need more context mid-task, use \`deskfree_orient\` with a specific query to recall relevant memories.
8226
- 2. **Align** \u2014 Send a brief \`notify\` message: what you found, what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.")
8227
- - **Judgment calls or creative direction?** State your assumptions and approach, send as \`ask\`, and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
8228
- - **Straightforward execution?** Proceed immediately after the notify \u2014 don't wait for a response.
8315
+ 2. **Align** \u2014 Send a brief message: what you found, what you'll produce. One or two sentences. ("I'll build on the existing brand guide and create a new tone reference.")
8316
+ - **Judgment calls or creative direction?** State your assumptions and approach, then set \`awaiting: 'human'\` via \`deskfree_update_task_status\` and wait for confirmation before proceeding. Getting alignment early prevents costly rework.
8317
+ - **Straightforward execution?** Proceed immediately \u2014 don't wait for a response.
8229
8318
  3. **Work** \u2014 Execute the task. Update existing files with \`deskfree_update_file\` or create new ones with \`deskfree_create_file\`. Pass your taskId so updates appear in the thread. For large deliverables, build incrementally \u2014 share structure/outline first, then flesh out. Don't produce a finished 2000-word document and ask for review in one shot.
8230
- 4. **Deliver** \u2014 Send an \`ask\` message when work is ready for review. Only complete (\`deskfree_complete_task\` with humanApproved: true) after the human has confirmed. Never self-complete.
8319
+ 4. **Deliver** \u2014 When work is ready for review, send a message and set \`awaiting: 'human'\` via \`deskfree_update_task_status\`. The human will complete the task when satisfied.
8231
8320
 
8232
8321
  **Push back when warranted:**
8233
8322
  - If task instructions seem unclear, contradictory, or misguided \u2014 say so. "This task asks for X, but based on [context], Y might work better because..." is more useful than silently executing a flawed plan.
8234
- - If you hit genuine ambiguity mid-task, send an \`ask\` message and wait. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
8323
+ - If you hit genuine ambiguity mid-task, send a message and set \`awaiting: 'human'\`. Don't guess on important decisions \u2014 guessing creates review debt the human has to pay later.
8235
8324
  - You're a teammate, not a task executor. Have an opinion when you have the context to form one.
8236
8325
 
8237
8326
  **File rules:**
@@ -8239,15 +8328,19 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
8239
8328
  - Always pass \`taskId\` when creating or updating files \u2014 this threads notifications into the task.
8240
8329
  - If you discover work that falls outside your task's scope, use \`deskfree_propose\` to suggest follow-up tasks immediately \u2014 don't wait until completion. Propose as you discover, then stay focused on your current task.
8241
8330
 
8242
- **Learnings:**
8243
- - Use \`deskfree_learning\` to record observations worth remembering. Set the \`importance\` parameter:
8244
- - **critical**: corrections, constraints that must never be violated
8245
- - **high**: strong preferences, important patterns
8246
- - **medium**: useful context, moderate preferences
8247
- - **low** (default): routine observations
8248
- - Record: preferences, corrections, patterns, domain facts.
8249
- - Do NOT record one-time task details, things in project docs, or obvious/generic knowledge.
8250
- - A nightly sleep cycle consolidates observations into long-term memory with decay scoring.
8331
+ **Learnings \u2014 record aggressively:**
8332
+ Use \`deskfree_learning\` to record anything worth remembering. **Err on the side of recording too much** \u2014 the nightly sleep cycle will consolidate and prune. You lose nothing by over-recording, but you lose knowledge by under-recording.
8333
+
8334
+ Record across all five types:
8335
+ - **Corrections**: "Do X, not Y" \u2014 when the human corrects your approach (\`importance: critical\`)
8336
+ - **Preferences**: How they want things done \u2014 tone, format, workflow, style (\`importance: high\`)
8337
+ - **Patterns**: Approaches that consistently work or fail (\`importance: medium\`)
8338
+ - **Domain facts**: Business context, project-specific knowledge, key relationships (\`importance: medium\`)
8339
+ - **Insights**: Non-obvious connections, meta-observations about the work (\`importance: low\`)
8340
+
8341
+ Record immediately when: the human corrects you, expresses a preference, shares context about their business, reacts strongly to something, or you discover something that would help future tasks.
8342
+
8343
+ Do NOT record: one-time task details, things already in project docs, or obvious/generic knowledge.
8251
8344
 
8252
8345
  **Memory recall:**
8253
8346
  - Use \`deskfree_orient\` to recall relevant memories mid-task. Call with a specific query for targeted semantic search.
@@ -8573,43 +8666,27 @@ var init_orchestrator = __esm({
8573
8666
  ];
8574
8667
  }
8575
8668
  });
8576
- function requireEnv(key) {
8577
- const val = process.env[key];
8578
- if (!val) {
8579
- throw new Error(`Required environment variable ${key} is not set`);
8580
- }
8581
- return val;
8669
+ function getStageDomain(stage, domain) {
8670
+ if (domain.startsWith(`${stage}.`)) return domain;
8671
+ return `${stage}.${domain}`;
8672
+ }
8673
+ function deriveApiUrl() {
8674
+ const domain = process.env["DESKFREE_DOMAIN"] ?? "dev.deskfree.ai";
8675
+ const stage = process.env["STAGE"] ?? "dev";
8676
+ return `https://${getStageDomain(stage, domain)}/v1/bot`;
8582
8677
  }
8583
8678
  function loadConfig() {
8584
- let botToken;
8585
- let apiUrl;
8586
- const launch = process.env["DESKFREE_LAUNCH"];
8587
- if (launch) {
8588
- try {
8589
- const decoded = JSON.parse(
8590
- Buffer.from(launch, "base64").toString("utf8")
8591
- );
8592
- botToken = decoded.botToken;
8593
- apiUrl = decoded.apiUrl;
8594
- if (!botToken || !apiUrl) {
8595
- throw new Error(
8596
- "Missing botToken or apiUrl in DESKFREE_LAUNCH payload"
8597
- );
8598
- }
8599
- } catch (err) {
8600
- if (err instanceof SyntaxError) {
8601
- throw new Error("DESKFREE_LAUNCH is not valid base64-encoded JSON");
8602
- }
8603
- throw err;
8604
- }
8605
- } else {
8606
- botToken = requireEnv("DESKFREE_BOT_TOKEN");
8607
- apiUrl = requireEnv("DESKFREE_API_URL");
8679
+ const botId = process.env["BOT"] ?? process.env["DESKFREE_BOT_ID"];
8680
+ if (!botId) {
8681
+ throw new Error(
8682
+ "Missing bot ID. Set the BOT environment variable (e.g. BOT=BFRH3VXHQR7)."
8683
+ );
8608
8684
  }
8609
- const tokenError = validateBotToken(botToken);
8610
- if (tokenError !== null) {
8611
- throw new Error(`Invalid bot token: ${tokenError}`);
8685
+ const idError = validateBotId(botId);
8686
+ if (idError !== null) {
8687
+ throw new Error(`Invalid bot ID: ${idError}`);
8612
8688
  }
8689
+ const apiUrl = process.env["DESKFREE_API_URL"] ?? deriveApiUrl();
8613
8690
  const apiUrlError = validateApiUrl(apiUrl);
8614
8691
  if (apiUrlError !== null) {
8615
8692
  throw new Error(`Invalid API URL: ${apiUrlError}`);
@@ -8625,7 +8702,7 @@ function loadConfig() {
8625
8702
  throw new Error(`Invalid HEALTH_PORT: ${healthPortRaw}`);
8626
8703
  }
8627
8704
  return {
8628
- botToken,
8705
+ botId,
8629
8706
  apiUrl,
8630
8707
  stateDir: process.env["DESKFREE_STATE_DIR"] ?? DEFAULTS.stateDir,
8631
8708
  toolsDir: process.env["DESKFREE_TOOLS_DIR"] ?? DEFAULTS.toolsDir,
@@ -12513,6 +12590,25 @@ var init_wrapper = __esm({
12513
12590
  wrapper_default2 = import_websocket2.default;
12514
12591
  }
12515
12592
  });
12593
+
12594
+ // src/gateway/ws-gateway.ts
12595
+ var ws_gateway_exports = {};
12596
+ __export(ws_gateway_exports, {
12597
+ getRotationToken: () => getRotationToken,
12598
+ setInitialRotationToken: () => setInitialRotationToken,
12599
+ startGateway: () => startGateway
12600
+ });
12601
+ function getRotationToken() {
12602
+ if (!currentRotationToken) {
12603
+ throw new Error(
12604
+ "No rotation token available \u2014 bots.connect not yet called"
12605
+ );
12606
+ }
12607
+ return currentRotationToken;
12608
+ }
12609
+ function setInitialRotationToken(token) {
12610
+ currentRotationToken = token;
12611
+ }
12516
12612
  function nextBackoff(state2) {
12517
12613
  const delay = Math.min(
12518
12614
  BACKOFF_INITIAL_MS * Math.pow(BACKOFF_FACTOR, state2.attempt),
@@ -12538,6 +12634,55 @@ function sleepWithAbort(ms, signal) {
12538
12634
  signal.addEventListener("abort", onAbort, { once: true });
12539
12635
  });
12540
12636
  }
12637
+ async function callBotsConnect(config, rotationToken) {
12638
+ const { botId, publicApiUrl, fingerprint, log, abortSignal } = config;
12639
+ if (!botId || !publicApiUrl || !fingerprint) {
12640
+ throw new Error(
12641
+ "Gateway config missing botId, publicApiUrl, or fingerprint"
12642
+ );
12643
+ }
12644
+ let connectToken;
12645
+ while (true) {
12646
+ if (abortSignal.aborted) throw new Error("Aborted during bots.connect");
12647
+ const body = {
12648
+ botId,
12649
+ fingerprint,
12650
+ ...rotationToken ? { rotationToken } : {},
12651
+ ...connectToken ? { connectToken } : {}
12652
+ };
12653
+ const response = await fetch(`${publicApiUrl}/bots.connect`, {
12654
+ method: "POST",
12655
+ headers: { "Content-Type": "application/json" },
12656
+ body: JSON.stringify(body)
12657
+ });
12658
+ if (!response.ok) {
12659
+ const text = await response.text().catch(() => "");
12660
+ throw new Error(`bots.connect failed: ${response.status} ${text}`);
12661
+ }
12662
+ const json = await response.json();
12663
+ const data = json.result?.data;
12664
+ if (!data) throw new Error("bots.connect: invalid response structure");
12665
+ if (data.status === "approved") {
12666
+ return {
12667
+ ticket: data.ticket,
12668
+ wsUrl: data.wsUrl,
12669
+ rotationToken: data.rotationToken
12670
+ };
12671
+ }
12672
+ if (data.status === "awaiting_approval") {
12673
+ connectToken = data.connectToken;
12674
+ log.info("Awaiting human approval... polling in 30s");
12675
+ await sleepWithAbort(CONNECT_POLL_INTERVAL_MS, abortSignal);
12676
+ continue;
12677
+ }
12678
+ if (data.status === "rejected") {
12679
+ throw new Error(
12680
+ `Connection rejected: ${data.reason || "unknown reason"}`
12681
+ );
12682
+ }
12683
+ throw new Error(`bots.connect: unexpected status "${data.status}"`);
12684
+ }
12685
+ }
12541
12686
  async function startGateway(config) {
12542
12687
  const { client, accountId, stateDir, log, abortSignal } = config;
12543
12688
  const ctx = { accountId };
@@ -12554,7 +12699,20 @@ async function startGateway(config) {
12554
12699
  });
12555
12700
  while (!abortSignal.aborted) {
12556
12701
  try {
12557
- const { ticket, wsUrl } = await client.getWsTicket();
12702
+ let ticket;
12703
+ let wsUrl;
12704
+ if (config.initialTicket && totalReconnects === 0) {
12705
+ ticket = config.initialTicket.ticket;
12706
+ wsUrl = config.initialTicket.wsUrl;
12707
+ } else {
12708
+ const result = await callBotsConnect(
12709
+ config,
12710
+ currentRotationToken ?? void 0
12711
+ );
12712
+ ticket = result.ticket;
12713
+ wsUrl = result.wsUrl;
12714
+ currentRotationToken = result.rotationToken;
12715
+ }
12558
12716
  resetBackoff(backoff);
12559
12717
  if (totalReconnects > 0) {
12560
12718
  log.info(
@@ -12575,17 +12733,19 @@ async function startGateway(config) {
12575
12733
  log,
12576
12734
  abortSignal,
12577
12735
  onMessage: config.onMessage,
12578
- getWorkerStatus: config.getWorkerStatus
12736
+ getWorkerStatus: config.getWorkerStatus,
12737
+ onConsolidate: config.onConsolidate,
12738
+ isV2: true
12579
12739
  });
12580
12740
  totalReconnects++;
12581
12741
  } catch (err) {
12582
12742
  totalReconnects++;
12583
12743
  const message = err instanceof Error ? err.message : String(err);
12584
- if (message.includes("API error") || message.includes("authentication failed") || message.includes("server error")) {
12744
+ if (message.includes("bots.connect failed") || message.includes("server error") || message.includes("Connection rejected")) {
12585
12745
  log.warn(
12586
- `Ticket fetch failed (attempt #${totalReconnects}): ${message}. Falling back to polling.`
12746
+ `Connection setup failed (attempt #${totalReconnects}): ${message}. Falling back to polling.`
12587
12747
  );
12588
- reportError("error", `WS ticket fetch failed: ${message}`, {
12748
+ reportError("error", `Connection setup failed: ${message}`, {
12589
12749
  component: "gateway",
12590
12750
  event: "ticket_fetch_failed",
12591
12751
  attempt: totalReconnects
@@ -12630,6 +12790,7 @@ async function runWebSocketConnection(opts) {
12630
12790
  let connectionTimer;
12631
12791
  let pongTimer;
12632
12792
  let notifyDebounceTimer;
12793
+ let proactiveReconnectTimer;
12633
12794
  let isConnected = false;
12634
12795
  const cleanup = () => {
12635
12796
  if (pingInterval !== void 0) {
@@ -12648,6 +12809,10 @@ async function runWebSocketConnection(opts) {
12648
12809
  clearTimeout(notifyDebounceTimer);
12649
12810
  notifyDebounceTimer = void 0;
12650
12811
  }
12812
+ if (proactiveReconnectTimer !== void 0) {
12813
+ clearTimeout(proactiveReconnectTimer);
12814
+ proactiveReconnectTimer = void 0;
12815
+ }
12651
12816
  };
12652
12817
  connectionTimer = setTimeout(() => {
12653
12818
  if (!isConnected) {
@@ -12676,7 +12841,11 @@ async function runWebSocketConnection(opts) {
12676
12841
  pingInterval = setInterval(() => {
12677
12842
  if (ws.readyState === wrapper_default2.OPEN) {
12678
12843
  try {
12679
- ws.send(JSON.stringify({ action: "ping" }));
12844
+ const pingPayload = { action: "ping" };
12845
+ if (currentRotationToken) {
12846
+ pingPayload.token = currentRotationToken;
12847
+ }
12848
+ ws.send(JSON.stringify(pingPayload));
12680
12849
  pongTimer = setTimeout(() => {
12681
12850
  log.warn("Pong timeout \u2014 closing WebSocket");
12682
12851
  try {
@@ -12690,6 +12859,13 @@ async function runWebSocketConnection(opts) {
12690
12859
  }
12691
12860
  }
12692
12861
  }, PING_INTERVAL_MS);
12862
+ proactiveReconnectTimer = setTimeout(() => {
12863
+ log.info("Proactive reconnect at 1h50m \u2014 closing for reconnect");
12864
+ try {
12865
+ ws.close(1e3, "proactive_reconnect");
12866
+ } catch {
12867
+ }
12868
+ }, PROACTIVE_RECONNECT_MS);
12693
12869
  if (opts.getWorkerStatus) {
12694
12870
  try {
12695
12871
  const status2 = opts.getWorkerStatus();
@@ -12762,7 +12938,21 @@ async function runWebSocketConnection(opts) {
12762
12938
  clearTimeout(pongTimer);
12763
12939
  pongTimer = void 0;
12764
12940
  }
12765
- log.debug("Received pong \u2014 connection healthy");
12941
+ const pongMsg = msg;
12942
+ if (pongMsg.rotationToken) {
12943
+ currentRotationToken = pongMsg.rotationToken;
12944
+ log.debug("Received pong \u2014 token rotated");
12945
+ } else if (pongMsg.error === "rotation_invalid") {
12946
+ log.warn(
12947
+ "Rotation token invalid \u2014 closing for reconnect via bots.connect"
12948
+ );
12949
+ try {
12950
+ ws.close(1e3, "rotation_invalid");
12951
+ } catch {
12952
+ }
12953
+ } else {
12954
+ log.debug("Received pong \u2014 connection healthy");
12955
+ }
12766
12956
  } else if (msg.action === "heartbeatRequest") {
12767
12957
  if (opts.getWorkerStatus && ws.readyState === wrapper_default2.OPEN) {
12768
12958
  try {
@@ -12828,6 +13018,34 @@ async function runWebSocketConnection(opts) {
12828
13018
  })
12829
13019
  );
12830
13020
  }
13021
+ } else if (msg.action === "consolidate") {
13022
+ if (opts.onConsolidate && ws.readyState === wrapper_default2.OPEN) {
13023
+ opts.onConsolidate().then(
13024
+ (consolidateStatus) => {
13025
+ if (ws.readyState === wrapper_default2.OPEN) {
13026
+ ws.send(
13027
+ JSON.stringify({
13028
+ action: "consolidateResponse",
13029
+ status: consolidateStatus
13030
+ })
13031
+ );
13032
+ }
13033
+ },
13034
+ (consolidateErr) => {
13035
+ const errMsg = consolidateErr instanceof Error ? consolidateErr.message : String(consolidateErr);
13036
+ log.warn(`On-demand consolidation failed: ${errMsg}`);
13037
+ if (ws.readyState === wrapper_default2.OPEN) {
13038
+ ws.send(
13039
+ JSON.stringify({
13040
+ action: "consolidateResponse",
13041
+ status: "error",
13042
+ error: errMsg
13043
+ })
13044
+ );
13045
+ }
13046
+ }
13047
+ );
13048
+ }
12831
13049
  }
12832
13050
  } catch (err) {
12833
13051
  const message = err instanceof Error ? err.message : String(err);
@@ -12942,7 +13160,7 @@ async function runPollingFallback(opts) {
12942
13160
  }
12943
13161
  return cursor;
12944
13162
  }
12945
- var PING_INTERVAL_MS, POLL_FALLBACK_INTERVAL_MS, WS_CONNECTION_TIMEOUT_MS, WS_PONG_TIMEOUT_MS, NOTIFY_DEBOUNCE_MS, HEALTH_LOG_INTERVAL_MS, MAX_POLLING_ITERATIONS, MAX_CONSECUTIVE_POLL_FAILURES, BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_FACTOR;
13163
+ var PING_INTERVAL_MS, POLL_FALLBACK_INTERVAL_MS, WS_CONNECTION_TIMEOUT_MS, WS_PONG_TIMEOUT_MS, NOTIFY_DEBOUNCE_MS, HEALTH_LOG_INTERVAL_MS, MAX_POLLING_ITERATIONS, MAX_CONSECUTIVE_POLL_FAILURES, CONNECT_POLL_INTERVAL_MS, PROACTIVE_RECONNECT_MS, BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_FACTOR, currentRotationToken;
12946
13164
  var init_ws_gateway = __esm({
12947
13165
  "src/gateway/ws-gateway.ts"() {
12948
13166
  init_health_state();
@@ -12958,9 +13176,12 @@ var init_ws_gateway = __esm({
12958
13176
  HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
12959
13177
  MAX_POLLING_ITERATIONS = 10;
12960
13178
  MAX_CONSECUTIVE_POLL_FAILURES = 5;
13179
+ CONNECT_POLL_INTERVAL_MS = 3e4;
13180
+ PROACTIVE_RECONNECT_MS = 110 * 60 * 1e3;
12961
13181
  BACKOFF_INITIAL_MS = 2e3;
12962
13182
  BACKOFF_MAX_MS = 3e4;
12963
13183
  BACKOFF_FACTOR = 1.8;
13184
+ currentRotationToken = null;
12964
13185
  }
12965
13186
  });
12966
13187
  function jsonSchemaPropertyToZod(prop) {
@@ -13174,13 +13395,13 @@ function validateDownloadUrl(url) {
13174
13395
  if (parsed.protocol !== "https:") {
13175
13396
  throw new Error("Only HTTPS URLs are allowed");
13176
13397
  }
13177
- const hostname = parsed.hostname.toLowerCase();
13178
- const bareHostname = hostname.replace(/^\[|\]$/g, "");
13398
+ const hostname2 = parsed.hostname.toLowerCase();
13399
+ const bareHostname = hostname2.replace(/^\[|\]$/g, "");
13179
13400
  if (bareHostname === "localhost" || bareHostname === "127.0.0.1" || bareHostname === "::1") {
13180
13401
  throw new Error("Local URLs are not allowed");
13181
13402
  }
13182
13403
  for (const pattern of PRIVATE_IPV4_PATTERNS) {
13183
- if (pattern.test(hostname)) {
13404
+ if (pattern.test(hostname2)) {
13184
13405
  throw new Error("Private IP addresses are not allowed");
13185
13406
  }
13186
13407
  }
@@ -13750,8 +13971,9 @@ async function routeMessage(message, client, deps, sessionStore, config) {
13750
13971
  `Auto-reopening task ${message.taskId} (attachment on ${task.status} task)`
13751
13972
  );
13752
13973
  try {
13753
- await client.reopenTask({
13974
+ await client.updateTaskStatus({
13754
13975
  taskId: message.taskId,
13976
+ status: "open",
13755
13977
  reason: "Human sent an attachment \u2014 reopening for further work"
13756
13978
  });
13757
13979
  routingTarget = "orchestrator";
@@ -14559,6 +14781,59 @@ ${userMessage}
14559
14781
  }
14560
14782
  });
14561
14783
 
14784
+ // src/auth/fingerprint.ts
14785
+ var fingerprint_exports = {};
14786
+ __export(fingerprint_exports, {
14787
+ collectFingerprint: () => collectFingerprint
14788
+ });
14789
+ function collectFingerprint(stateDir, runtimeVersion) {
14790
+ return {
14791
+ hostId: getOrCreateHostId(stateDir),
14792
+ machineId: getMachineId(),
14793
+ platform: platform(),
14794
+ arch: arch(),
14795
+ hostname: hostname(),
14796
+ cpuModel: cpus()[0]?.model ?? "unknown",
14797
+ totalMemory: totalmem(),
14798
+ nodeVersion: process.version,
14799
+ runtimeVersion
14800
+ };
14801
+ }
14802
+ function getOrCreateHostId(stateDir) {
14803
+ const hostIdPath = join(stateDir, "host-id");
14804
+ if (existsSync(hostIdPath)) {
14805
+ const existing = readFileSync(hostIdPath, "utf8").trim();
14806
+ if (existing) return existing;
14807
+ }
14808
+ const hostId = randomUUID();
14809
+ mkdirSync(dirname(hostIdPath), { recursive: true });
14810
+ writeFileSync(hostIdPath, hostId, "utf8");
14811
+ return hostId;
14812
+ }
14813
+ function getMachineId() {
14814
+ try {
14815
+ if (platform() === "linux") {
14816
+ if (existsSync("/etc/machine-id")) {
14817
+ return readFileSync("/etc/machine-id", "utf8").trim();
14818
+ }
14819
+ }
14820
+ if (platform() === "darwin") {
14821
+ const output = execSync(
14822
+ "ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID",
14823
+ { encoding: "utf8", timeout: 5e3 }
14824
+ );
14825
+ const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
14826
+ if (match?.[1]) return match[1];
14827
+ }
14828
+ } catch {
14829
+ }
14830
+ return "unknown";
14831
+ }
14832
+ var init_fingerprint = __esm({
14833
+ "src/auth/fingerprint.ts"() {
14834
+ }
14835
+ });
14836
+
14562
14837
  // src/service/entrypoint.ts
14563
14838
  var entrypoint_exports = {};
14564
14839
  __export(entrypoint_exports, {
@@ -14633,12 +14908,75 @@ function scheduleHeartbeat(createOrchServer, model, intervalMs, signal, log, cla
14633
14908
  setTimeout(() => void tick(), intervalMs);
14634
14909
  log.info(`Heartbeat scheduled every ${Math.round(intervalMs / 1e3)}s`);
14635
14910
  }
14911
+ async function initialConnect(opts) {
14912
+ const { botId, publicApiUrl, fingerprint, log, abortSignal } = opts;
14913
+ let connectToken;
14914
+ while (true) {
14915
+ if (abortSignal.aborted) throw new Error("Aborted during bots.connect");
14916
+ const body = {
14917
+ botId,
14918
+ fingerprint,
14919
+ ...connectToken ? { connectToken } : {}
14920
+ };
14921
+ const response = await fetch(`${publicApiUrl}/bots.connect`, {
14922
+ method: "POST",
14923
+ headers: { "Content-Type": "application/json" },
14924
+ body: JSON.stringify(body)
14925
+ });
14926
+ if (!response.ok) {
14927
+ const text = await response.text().catch(() => "");
14928
+ throw new Error(`bots.connect failed: ${response.status} ${text}`);
14929
+ }
14930
+ const json = await response.json();
14931
+ const data = json.result?.data;
14932
+ if (!data) throw new Error("bots.connect: invalid response structure");
14933
+ if (data.status === "approved") {
14934
+ return {
14935
+ ticket: data.ticket,
14936
+ wsUrl: data.wsUrl,
14937
+ rotationToken: data.rotationToken
14938
+ };
14939
+ }
14940
+ if (data.status === "awaiting_approval") {
14941
+ connectToken = data.connectToken;
14942
+ log.info("Awaiting human approval... polling in 30s");
14943
+ await new Promise(
14944
+ (resolve) => setTimeout(resolve, CONNECT_POLL_INTERVAL_MS2)
14945
+ );
14946
+ continue;
14947
+ }
14948
+ if (data.status === "rejected") {
14949
+ throw new Error(
14950
+ `Connection rejected: ${data.reason || "unknown reason"}`
14951
+ );
14952
+ }
14953
+ throw new Error(`bots.connect: unexpected status "${data.status}"`);
14954
+ }
14955
+ }
14636
14956
  async function startAgent(opts) {
14637
14957
  const localConfig = loadConfig();
14638
14958
  const log = opts?.log ?? createLogger("agent", localConfig.logLevel);
14639
14959
  const abortController = new AbortController();
14640
14960
  log.info("DeskFree Agent Runtime starting...");
14641
- const client = new DeskFreeClient(localConfig.botToken, localConfig.apiUrl);
14961
+ const { getRotationToken: getRotationToken2, setInitialRotationToken: setInitialRotationToken2 } = await Promise.resolve().then(() => (init_ws_gateway(), ws_gateway_exports));
14962
+ const { collectFingerprint: collectFingerprint2 } = await Promise.resolve().then(() => (init_fingerprint(), fingerprint_exports));
14963
+ const runtimeVersion = process.env["npm_package_version"] ?? "unknown";
14964
+ const publicApiUrl = localConfig.apiUrl.replace("/v1/bot", "/v1/public");
14965
+ const fingerprint = collectFingerprint2(localConfig.stateDir, runtimeVersion);
14966
+ log.info("Connecting to DeskFree...", { apiUrl: publicApiUrl });
14967
+ const connectResult = await initialConnect({
14968
+ botId: localConfig.botId,
14969
+ publicApiUrl,
14970
+ fingerprint,
14971
+ log,
14972
+ abortSignal: abortController.signal
14973
+ });
14974
+ setInitialRotationToken2(connectResult.rotationToken);
14975
+ log.info("Connected \u2014 got rotation token and WS ticket.");
14976
+ const client = new DeskFreeClient({
14977
+ apiUrl: localConfig.apiUrl,
14978
+ getToken: () => getRotationToken2()
14979
+ });
14642
14980
  const errorReporter = initErrorReporter(client, log);
14643
14981
  initializeHealth("unknown");
14644
14982
  log.info("Bootstrapping from API...", { apiUrl: localConfig.apiUrl });
@@ -14658,7 +14996,6 @@ async function startAgent(opts) {
14658
14996
  throw new Error(`Failed to bootstrap config from API: ${msg}`);
14659
14997
  }
14660
14998
  const isDocker2 = process.env["DOCKER"] === "1" || (await import('fs')).existsSync("/.dockerenv");
14661
- const runtimeVersion = process.env["npm_package_version"] ?? "unknown";
14662
14999
  const agentContext = {
14663
15000
  botName: config.botName,
14664
15001
  deploymentType: config.deploymentType,
@@ -14738,6 +15075,13 @@ async function startAgent(opts) {
14738
15075
  stateDir: config.stateDir,
14739
15076
  log,
14740
15077
  abortSignal: abortController.signal,
15078
+ botId: localConfig.botId,
15079
+ publicApiUrl,
15080
+ fingerprint,
15081
+ initialTicket: {
15082
+ ticket: connectResult.ticket,
15083
+ wsUrl: connectResult.wsUrl
15084
+ },
14741
15085
  getWorkerStatus: () => ({
14742
15086
  activeWorkers: workerManager.activeCount,
14743
15087
  queuedTasks: workerManager.queuedCount,
@@ -14762,7 +15106,8 @@ async function startAgent(opts) {
14762
15106
  log
14763
15107
  }
14764
15108
  );
14765
- }
15109
+ },
15110
+ onConsolidate: () => runConsolidation()
14766
15111
  });
14767
15112
  scheduleHeartbeat(
14768
15113
  createOrchServer,
@@ -14773,64 +15118,107 @@ async function startAgent(opts) {
14773
15118
  config.claudeCodePath,
14774
15119
  agentContext
14775
15120
  );
14776
- if (config.sleepHour !== null && config.timezone) {
14777
- scheduleDailyCycle(
14778
- "Sleep",
14779
- async () => {
14780
- let orientResult;
14781
- try {
14782
- orientResult = await client.orient({ consolidation: true });
14783
- } catch (err) {
14784
- const msg = err instanceof Error ? err.message : String(err);
14785
- log.warn(`Sleep cycle: orient() failed: ${msg}`);
14786
- return;
15121
+ let isConsolidating = false;
15122
+ async function runConsolidation() {
15123
+ if (isConsolidating) {
15124
+ log.info("Consolidation already in progress, skipping");
15125
+ return "already_running";
15126
+ }
15127
+ isConsolidating = true;
15128
+ try {
15129
+ let orientResult;
15130
+ try {
15131
+ orientResult = await client.orient({ consolidation: true });
15132
+ } catch (err) {
15133
+ const msg = err instanceof Error ? err.message : String(err);
15134
+ log.warn(`Sleep cycle: orient() failed: ${msg}`);
15135
+ return "error";
15136
+ }
15137
+ if (orientResult.entries.length === 0) {
15138
+ log.info("Sleep cycle: no unconsolidated entries to process, skipping");
15139
+ return "noop";
15140
+ }
15141
+ const unconsolidatedSection = orientResult.entries.map(
15142
+ (e) => `- [${e.entryId}] ${e.importance ? `(${e.importance}) ` : ""}${e.content}`
15143
+ ).join("\n");
15144
+ const relatedSection = orientResult.recentEntries && orientResult.recentEntries.length > 0 ? orientResult.recentEntries.map(
15145
+ (e) => `- [${e.entryId}] [${e.type ?? "unclassified"}] s:${e.strength} \u2014 ${e.content}`
15146
+ ).join("\n") : "(none)";
15147
+ const dedupSection = orientResult.dedupCandidates && orientResult.dedupCandidates.length > 0 ? orientResult.dedupCandidates.map(
15148
+ (d) => `- ${d.entryId1} \u2194 ${d.entryId2} (similarity: ${d.similarity.toFixed(3)})`
15149
+ ).join("\n") : "(none)";
15150
+ const prompt = [
15151
+ "<current_operating_memory>",
15152
+ orientResult.operatingMemory || "(empty \u2014 first consolidation)",
15153
+ "</current_operating_memory>",
15154
+ "",
15155
+ "<unconsolidated_entries>",
15156
+ unconsolidatedSection,
15157
+ "</unconsolidated_entries>",
15158
+ "",
15159
+ "<related_active_entries>",
15160
+ relatedSection,
15161
+ "</related_active_entries>",
15162
+ "",
15163
+ "<dedup_candidates>",
15164
+ dedupSection,
15165
+ "</dedup_candidates>",
15166
+ "",
15167
+ "Run your nightly consolidation cycle now."
15168
+ ].join("\n");
15169
+ log.info(
15170
+ `Sleep cycle: invoking sleep agent (${orientResult.entries.length} unconsolidated entries)...`
15171
+ );
15172
+ const workerServer = createWorkServer();
15173
+ const agentStream = runOneShotWorker({
15174
+ prompt,
15175
+ systemPrompt: buildSleepDirective(agentContext),
15176
+ workerServer,
15177
+ model: config.model
15178
+ });
15179
+ let fullText = "";
15180
+ for await (const msg of agentStream) {
15181
+ if (msg.type === "assistant" && Array.isArray(msg.message?.content)) {
15182
+ for (const block of msg.message.content) {
15183
+ if (block.type === "text") {
15184
+ fullText += block.text;
15185
+ }
15186
+ }
14787
15187
  }
14788
- if (orientResult.entries.length === 0) {
15188
+ }
15189
+ const tagMatch = fullText.match(
15190
+ /<consolidation_result>([\s\S]*?)<\/consolidation_result>/
15191
+ );
15192
+ if (tagMatch) {
15193
+ try {
15194
+ const parsed = JSON.parse(tagMatch[1]);
15195
+ const consolidateResult = await client.consolidateMemory({
15196
+ newEntries: parsed.newEntries ?? [],
15197
+ modifications: parsed.modifications ?? [],
15198
+ operatingMemory: parsed.operatingMemory ?? ""
15199
+ });
14789
15200
  log.info(
14790
- "Sleep cycle: no unconsolidated entries to process, skipping"
15201
+ `Sleep cycle: consolidation applied (${consolidateResult.opsApplied} ops, ${consolidateResult.entriesArchived} archived)`
14791
15202
  );
14792
- return;
15203
+ } catch (parseErr) {
15204
+ const parseMsg = parseErr instanceof Error ? parseErr.message : String(parseErr);
15205
+ log.warn(`Sleep cycle: failed to submit consolidation: ${parseMsg}`);
14793
15206
  }
14794
- const unconsolidatedSection = orientResult.entries.map(
14795
- (e) => `- [${e.entryId}] ${e.importance ? `(${e.importance}) ` : ""}${e.content}`
14796
- ).join("\n");
14797
- const relatedSection = orientResult.recentEntries && orientResult.recentEntries.length > 0 ? orientResult.recentEntries.map(
14798
- (e) => `- [${e.entryId}] [${e.type ?? "unclassified"}] s:${e.strength} \u2014 ${e.content}`
14799
- ).join("\n") : "(none)";
14800
- const dedupSection = orientResult.dedupCandidates && orientResult.dedupCandidates.length > 0 ? orientResult.dedupCandidates.map(
14801
- (d) => `- ${d.entryId1} \u2194 ${d.entryId2} (similarity: ${d.similarity.toFixed(3)})`
14802
- ).join("\n") : "(none)";
14803
- const prompt = [
14804
- "<current_operating_memory>",
14805
- orientResult.operatingMemory || "(empty \u2014 first consolidation)",
14806
- "</current_operating_memory>",
14807
- "",
14808
- "<unconsolidated_entries>",
14809
- unconsolidatedSection,
14810
- "</unconsolidated_entries>",
14811
- "",
14812
- "<related_active_entries>",
14813
- relatedSection,
14814
- "</related_active_entries>",
14815
- "",
14816
- "<dedup_candidates>",
14817
- dedupSection,
14818
- "</dedup_candidates>",
14819
- "",
14820
- "Run your nightly consolidation cycle now."
14821
- ].join("\n");
14822
- log.info(
14823
- `Sleep cycle: invoking sleep agent (${orientResult.entries.length} unconsolidated entries)...`
15207
+ } else {
15208
+ log.warn(
15209
+ "Sleep cycle: agent did not produce <consolidation_result> tags"
14824
15210
  );
14825
- const workerServer = createWorkServer();
14826
- const result = runOneShotWorker({
14827
- prompt,
14828
- systemPrompt: buildSleepDirective(agentContext),
14829
- workerServer,
14830
- model: config.model
14831
- });
14832
- for await (const _ of result) {
14833
- }
15211
+ }
15212
+ return "success";
15213
+ } finally {
15214
+ isConsolidating = false;
15215
+ }
15216
+ }
15217
+ if (config.sleepHour !== null && config.timezone) {
15218
+ scheduleDailyCycle(
15219
+ "Sleep",
15220
+ async () => {
15221
+ await runConsolidation();
14834
15222
  },
14835
15223
  config.sleepHour,
14836
15224
  config.timezone,
@@ -14894,6 +15282,7 @@ async function startAgent(opts) {
14894
15282
  log.info("Shutdown complete.");
14895
15283
  };
14896
15284
  }
15285
+ var CONNECT_POLL_INTERVAL_MS2;
14897
15286
  var init_entrypoint = __esm({
14898
15287
  "src/service/entrypoint.ts"() {
14899
15288
  init_orchestrator();
@@ -14910,6 +15299,7 @@ var init_entrypoint = __esm({
14910
15299
  init_sessions();
14911
15300
  init_worker_manager();
14912
15301
  init_dist();
15302
+ CONNECT_POLL_INTERVAL_MS2 = 3e4;
14913
15303
  }
14914
15304
  });
14915
15305
 
@@ -14918,29 +15308,42 @@ init_paths();
14918
15308
  var [name, cleanArgs] = parseName(process.argv.slice(2));
14919
15309
  var command = cleanArgs[0];
14920
15310
  if (command === "install") {
14921
- let token = cleanArgs[1];
14922
- if (!token) {
15311
+ const stageIdx = cleanArgs.indexOf("--stage");
15312
+ let stage;
15313
+ let installArgs = cleanArgs.slice(1);
15314
+ if (stageIdx !== -1) {
15315
+ stage = cleanArgs[stageIdx + 1];
15316
+ if (!stage || stage.startsWith("-")) {
15317
+ console.error("Error: --stage requires a value (e.g. --stage charlie)");
15318
+ process.exit(1);
15319
+ }
15320
+ installArgs = installArgs.filter(
15321
+ (_, i) => i !== stageIdx - 1 && i !== stageIdx
15322
+ );
15323
+ }
15324
+ let botId = installArgs[0];
15325
+ if (!botId) {
14923
15326
  const { createInterface } = await import('readline');
14924
15327
  const rl = createInterface({
14925
15328
  input: process.stdin,
14926
15329
  output: process.stdout
14927
15330
  });
14928
- token = await new Promise((resolve) => {
15331
+ botId = await new Promise((resolve) => {
14929
15332
  rl.question(
14930
- "Paste your bot token (from the DeskFree dashboard):\n> ",
15333
+ "Paste your bot ID (from the DeskFree dashboard):\n> ",
14931
15334
  (answer) => {
14932
15335
  rl.close();
14933
15336
  resolve(answer.trim());
14934
15337
  }
14935
15338
  );
14936
15339
  });
14937
- if (!token) {
14938
- console.error("No token provided.");
15340
+ if (!botId) {
15341
+ console.error("No bot ID provided.");
14939
15342
  process.exit(1);
14940
15343
  }
14941
15344
  }
14942
15345
  const { install: install2 } = await Promise.resolve().then(() => (init_install(), install_exports));
14943
- install2(token, name);
15346
+ install2(botId, name, stage);
14944
15347
  } else if (command === "uninstall") {
14945
15348
  const { uninstall: uninstall2 } = await Promise.resolve().then(() => (init_uninstall(), uninstall_exports));
14946
15349
  uninstall2(name);
@@ -14966,14 +15369,12 @@ if (command === "install") {
14966
15369
  process.exit(1);
14967
15370
  }, 5e3).unref();
14968
15371
  };
14969
- let token;
14970
- if (command === "start") {
14971
- token = cleanArgs[1];
14972
- } else if (command && !command.startsWith("-")) {
14973
- token = command;
14974
- }
14975
- if (token && !process.env["DESKFREE_LAUNCH"]) {
14976
- process.env["DESKFREE_LAUNCH"] = token;
15372
+ const startArgs = command === "start" ? cleanArgs.slice(1) : cleanArgs;
15373
+ if (startArgs.length >= 2) {
15374
+ process.env["STAGE"] = startArgs[0];
15375
+ process.env["BOT"] = startArgs[1];
15376
+ } else if (startArgs.length === 1) {
15377
+ process.env["BOT"] = startArgs[0];
14977
15378
  }
14978
15379
  const { startAgent: startAgent2 } = await Promise.resolve().then(() => (init_entrypoint(), entrypoint_exports));
14979
15380
  const { createLogger: createLogger2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));