@questionbase/deskfree 0.6.1 → 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';
@@ -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
@@ -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]
@@ -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);
@@ -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);
@@ -2867,18 +2877,7 @@ function createWorkerTools(client, options) {
2867
2877
  try {
2868
2878
  const content = validateStringParam(params, "content", true);
2869
2879
  const taskId = validateStringParam(params, "taskId", false);
2870
- const type = validateEnumParam(params, "type", ["notify", "ask"], true);
2871
2880
  await client.sendMessage({ content, taskId });
2872
- if (type === "ask") {
2873
- return {
2874
- content: [
2875
- {
2876
- type: "text",
2877
- text: "Ask sent \u2014 task is now awaiting human response. Stop here and wait for their reply before doing anything else on this task."
2878
- }
2879
- ]
2880
- };
2881
- }
2882
2881
  return {
2883
2882
  content: [{ type: "text", text: "Message sent successfully" }]
2884
2883
  };
@@ -3025,22 +3024,42 @@ function createWorkerTools(client, options) {
3025
3024
  return errorResult(err);
3026
3025
  }
3027
3026
  }),
3028
- createTool(WORKER_TOOLS.COMPLETE_TASK, async (params) => {
3027
+ createTool(WORKER_TOOLS.UPDATE_TASK_STATUS, async (params) => {
3029
3028
  try {
3030
3029
  const taskId = validateStringParam(params, "taskId", true);
3031
- const humanApproved = Boolean(params.humanApproved);
3032
- 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({
3033
3044
  taskId,
3034
- humanApproved
3045
+ status: status2,
3046
+ awaiting,
3047
+ reason
3035
3048
  });
3036
- try {
3037
- options?.onTaskCompleted?.(taskId);
3038
- } catch {
3049
+ if (status2 === "done") {
3050
+ try {
3051
+ options?.onTaskCompleted?.(taskId);
3052
+ } catch {
3053
+ }
3039
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}`);
3040
3059
  return formatTaskResponse(
3041
3060
  result,
3042
- `Task "${result.title}" completed successfully`,
3043
- [humanApproved ? "Status: Human approved" : "Status: Completed"]
3061
+ `Task "${result.title}" updated`,
3062
+ details
3044
3063
  );
3045
3064
  } catch (err) {
3046
3065
  return errorResult(err);
@@ -3099,7 +3118,6 @@ Human attention is finite. You have unlimited stamina \u2014 they don't. Optimiz
3099
3118
 
3100
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.
3101
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.
3102
- - **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.
3103
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.
3104
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.`;
3105
3123
  }
@@ -3151,7 +3169,7 @@ function buildWorkerDirective(ctx) {
3151
3169
  ## You're In a Task Thread
3152
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.
3153
3171
 
3154
- 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.
3155
3173
 
3156
3174
  **Context loading:**
3157
3175
  - If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
@@ -3165,15 +3183,15 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
3165
3183
  **Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
3166
3184
 
3167
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.
3168
- 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.")
3169
- - **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.
3170
- - **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.
3171
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.
3172
- 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.
3173
3191
 
3174
3192
  **Push back when warranted:**
3175
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.
3176
- - 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.
3177
3195
  - You're a teammate, not a task executor. Have an opinion when you have the context to form one.
3178
3196
 
3179
3197
  **File rules:**
@@ -3540,31 +3558,25 @@ function validateField(opts) {
3540
3558
  return patternMessage ?? `${name2} contains invalid characters`;
3541
3559
  return null;
3542
3560
  }
3543
- function isLocalDevelopmentHost(hostname) {
3544
- 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);
3545
3563
  }
3546
- function validateBotToken(value) {
3547
- const fieldError = validateField({ value, name: "Bot token" });
3564
+ function validateBotId(value) {
3565
+ const fieldError = validateField({ value, name: "Bot ID" });
3548
3566
  if (fieldError) return fieldError;
3549
3567
  const trimmed = value.trim();
3550
- if (!trimmed.startsWith("bot_")) {
3551
- 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.";
3552
3570
  }
3553
3571
  const patternError = validateField({
3554
3572
  value,
3555
- name: "Bot token",
3573
+ name: "Bot ID",
3556
3574
  minLength: 10,
3557
- maxLength: 200,
3558
- pattern: /^bot_[a-zA-Z0-9_-]+$/,
3559
- 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."
3560
3578
  });
3561
3579
  if (patternError) return patternError;
3562
- if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
3563
- return "Bot token contains whitespace characters. Please copy the token exactly as shown in DeskFree.";
3564
- }
3565
- if (trimmed === "bot_your_token_here" || trimmed === "bot_example") {
3566
- return "Please replace the placeholder with your actual bot token from DeskFree";
3567
- }
3568
3580
  return null;
3569
3581
  }
3570
3582
  function validateUrl(value, name2, allowedProtocols, protocolError) {
@@ -7351,13 +7363,13 @@ var init_dist = __esm({
7351
7363
  }
7352
7364
  };
7353
7365
  DeskFreeClient = class {
7354
- botToken;
7366
+ getToken;
7355
7367
  apiUrl;
7356
7368
  requestTimeoutMs;
7357
- constructor(botToken, apiUrl, options) {
7358
- this.botToken = botToken;
7359
- this.apiUrl = apiUrl.replace(/\/$/, "");
7360
- 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;
7361
7373
  }
7362
7374
  async request(method, procedure, input) {
7363
7375
  const url = method === "GET" && input ? `${this.apiUrl}/${procedure}?input=${encodeURIComponent(JSON.stringify(input))}` : `${this.apiUrl}/${procedure}`;
@@ -7367,7 +7379,7 @@ var init_dist = __esm({
7367
7379
  const response = await fetch(url, {
7368
7380
  method,
7369
7381
  headers: {
7370
- Authorization: `Bot ${this.botToken}`,
7382
+ Authorization: `Bearer ${this.getToken()}`,
7371
7383
  "Content-Type": "application/json"
7372
7384
  },
7373
7385
  body: method === "POST" ? JSON.stringify(input) : void 0,
@@ -7498,15 +7510,10 @@ var init_dist = __esm({
7498
7510
  this.requireNonEmpty(input.taskId, "taskId");
7499
7511
  return this.request("POST", "tasks.reportUsage", input);
7500
7512
  }
7501
- /** Complete a task. Marks it as done. */
7502
- async completeTask(input) {
7513
+ /** Update task status and/or awaiting state. */
7514
+ async updateTaskStatus(input) {
7503
7515
  this.requireNonEmpty(input.taskId, "taskId");
7504
- return this.request("POST", "tasks.complete", input);
7505
- }
7506
- /** Reopen a completed/human task back to bot status for further work. */
7507
- async reopenTask(input) {
7508
- this.requireNonEmpty(input.taskId, "taskId");
7509
- return this.request("POST", "tasks.reopen", input);
7516
+ return this.request("POST", "tasks.updateStatus", input);
7510
7517
  }
7511
7518
  /** Snooze a task until a specified time. Task is hidden from active queues until then. */
7512
7519
  async snoozeTask(input) {
@@ -7580,7 +7587,7 @@ var init_dist = __esm({
7580
7587
  {
7581
7588
  method: "GET",
7582
7589
  headers: {
7583
- Authorization: `Bot ${this.botToken}`,
7590
+ Authorization: `Bearer ${this.getToken()}`,
7584
7591
  "Content-Type": "application/json"
7585
7592
  },
7586
7593
  signal: controller.signal
@@ -7922,14 +7929,24 @@ var init_dist = __esm({
7922
7929
  })
7923
7930
  })
7924
7931
  },
7925
- REOPEN_TASK: {
7926
- name: "deskfree_reopen_task",
7927
- 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.",
7928
7935
  parameters: Type.Object({
7929
- 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
+ ),
7930
7947
  reason: Type.Optional(
7931
7948
  Type.String({
7932
- 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)"
7933
7950
  })
7934
7951
  )
7935
7952
  })
@@ -7938,9 +7955,6 @@ var init_dist = __esm({
7938
7955
  name: "deskfree_send_message",
7939
7956
  description: "Send a message to the human. Keep it short \u2014 1-3 sentences. No walls of text.",
7940
7957
  parameters: Type.Object({
7941
- type: Type.Union([Type.Literal("notify"), Type.Literal("ask")], {
7942
- description: "notify = general update (quiet). ask = needs human attention (surfaces prominently)."
7943
- }),
7944
7958
  content: Type.String({
7945
7959
  description: "Message content."
7946
7960
  }),
@@ -8072,23 +8086,10 @@ var init_dist = __esm({
8072
8086
  )
8073
8087
  })
8074
8088
  },
8075
- COMPLETE_TASK: {
8076
- name: "deskfree_complete_task",
8077
- description: "Mark a task as done. Only call when truly finished and human confirmed.",
8078
- parameters: Type.Object({
8079
- taskId: Type.String({ description: "Task UUID" }),
8080
- humanApproved: Type.Boolean({
8081
- description: "Must be true. Confirms the human reviewed and approved completion. Backend validates a human message exists after your last ask."
8082
- })
8083
- })
8084
- },
8085
8089
  SEND_MESSAGE: {
8086
8090
  name: "deskfree_send_message",
8087
8091
  description: "Send a message in the task thread. Keep it short \u2014 1-3 sentences.",
8088
8092
  parameters: Type.Object({
8089
- type: Type.Union([Type.Literal("notify"), Type.Literal("ask")], {
8090
- description: "notify = progress update (quiet, collapsible). ask = needs human attention (surfaces to main thread). Terminate after sending an ask."
8091
- }),
8092
8093
  content: Type.String({
8093
8094
  description: "Message content."
8094
8095
  }),
@@ -8229,7 +8230,28 @@ var init_dist = __esm({
8229
8230
  )
8230
8231
  })
8231
8232
  },
8232
- 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
+ },
8233
8255
  SEND_MESSAGE: SHARED_TOOLS.SEND_MESSAGE,
8234
8256
  PROPOSE: SHARED_TOOLS.PROPOSE
8235
8257
  };
@@ -8276,7 +8298,7 @@ Record immediately when: the human corrects you, expresses a preference, shares
8276
8298
  DESKFREE_WORKER_DIRECTIVE = `## DeskFree \u2014 Task Thread
8277
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.
8278
8300
 
8279
- 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.
8280
8302
 
8281
8303
  **Context loading:**
8282
8304
  - If your first message contains \`<task_context>\`, the task is already loaded. Start working immediately \u2014 do NOT call deskfree_start_task.
@@ -8290,15 +8312,15 @@ Tools: deskfree_state, deskfree_start_task, deskfree_read_file, deskfree_create_
8290
8312
  **Orient \u2192 Align \u2192 Work.** Every new task follows this rhythm:
8291
8313
 
8292
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.
8293
- 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.")
8294
- - **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.
8295
- - **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.
8296
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.
8297
- 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.
8298
8320
 
8299
8321
  **Push back when warranted:**
8300
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.
8301
- - 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.
8302
8324
  - You're a teammate, not a task executor. Have an opinion when you have the context to form one.
8303
8325
 
8304
8326
  **File rules:**
@@ -8644,43 +8666,27 @@ var init_orchestrator = __esm({
8644
8666
  ];
8645
8667
  }
8646
8668
  });
8647
- function requireEnv(key) {
8648
- const val = process.env[key];
8649
- if (!val) {
8650
- throw new Error(`Required environment variable ${key} is not set`);
8651
- }
8652
- 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`;
8653
8677
  }
8654
8678
  function loadConfig() {
8655
- let botToken;
8656
- let apiUrl;
8657
- const launch = process.env["DESKFREE_LAUNCH"];
8658
- if (launch) {
8659
- try {
8660
- const decoded = JSON.parse(
8661
- Buffer.from(launch, "base64").toString("utf8")
8662
- );
8663
- botToken = decoded.botToken;
8664
- apiUrl = decoded.apiUrl;
8665
- if (!botToken || !apiUrl) {
8666
- throw new Error(
8667
- "Missing botToken or apiUrl in DESKFREE_LAUNCH payload"
8668
- );
8669
- }
8670
- } catch (err) {
8671
- if (err instanceof SyntaxError) {
8672
- throw new Error("DESKFREE_LAUNCH is not valid base64-encoded JSON");
8673
- }
8674
- throw err;
8675
- }
8676
- } else {
8677
- botToken = requireEnv("DESKFREE_BOT_TOKEN");
8678
- 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
+ );
8679
8684
  }
8680
- const tokenError = validateBotToken(botToken);
8681
- if (tokenError !== null) {
8682
- throw new Error(`Invalid bot token: ${tokenError}`);
8685
+ const idError = validateBotId(botId);
8686
+ if (idError !== null) {
8687
+ throw new Error(`Invalid bot ID: ${idError}`);
8683
8688
  }
8689
+ const apiUrl = process.env["DESKFREE_API_URL"] ?? deriveApiUrl();
8684
8690
  const apiUrlError = validateApiUrl(apiUrl);
8685
8691
  if (apiUrlError !== null) {
8686
8692
  throw new Error(`Invalid API URL: ${apiUrlError}`);
@@ -8696,7 +8702,7 @@ function loadConfig() {
8696
8702
  throw new Error(`Invalid HEALTH_PORT: ${healthPortRaw}`);
8697
8703
  }
8698
8704
  return {
8699
- botToken,
8705
+ botId,
8700
8706
  apiUrl,
8701
8707
  stateDir: process.env["DESKFREE_STATE_DIR"] ?? DEFAULTS.stateDir,
8702
8708
  toolsDir: process.env["DESKFREE_TOOLS_DIR"] ?? DEFAULTS.toolsDir,
@@ -12584,6 +12590,25 @@ var init_wrapper = __esm({
12584
12590
  wrapper_default2 = import_websocket2.default;
12585
12591
  }
12586
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
+ }
12587
12612
  function nextBackoff(state2) {
12588
12613
  const delay = Math.min(
12589
12614
  BACKOFF_INITIAL_MS * Math.pow(BACKOFF_FACTOR, state2.attempt),
@@ -12609,6 +12634,55 @@ function sleepWithAbort(ms, signal) {
12609
12634
  signal.addEventListener("abort", onAbort, { once: true });
12610
12635
  });
12611
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
+ }
12612
12686
  async function startGateway(config) {
12613
12687
  const { client, accountId, stateDir, log, abortSignal } = config;
12614
12688
  const ctx = { accountId };
@@ -12625,7 +12699,20 @@ async function startGateway(config) {
12625
12699
  });
12626
12700
  while (!abortSignal.aborted) {
12627
12701
  try {
12628
- 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
+ }
12629
12716
  resetBackoff(backoff);
12630
12717
  if (totalReconnects > 0) {
12631
12718
  log.info(
@@ -12647,17 +12734,18 @@ async function startGateway(config) {
12647
12734
  abortSignal,
12648
12735
  onMessage: config.onMessage,
12649
12736
  getWorkerStatus: config.getWorkerStatus,
12650
- onConsolidate: config.onConsolidate
12737
+ onConsolidate: config.onConsolidate,
12738
+ isV2: true
12651
12739
  });
12652
12740
  totalReconnects++;
12653
12741
  } catch (err) {
12654
12742
  totalReconnects++;
12655
12743
  const message = err instanceof Error ? err.message : String(err);
12656
- 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")) {
12657
12745
  log.warn(
12658
- `Ticket fetch failed (attempt #${totalReconnects}): ${message}. Falling back to polling.`
12746
+ `Connection setup failed (attempt #${totalReconnects}): ${message}. Falling back to polling.`
12659
12747
  );
12660
- reportError("error", `WS ticket fetch failed: ${message}`, {
12748
+ reportError("error", `Connection setup failed: ${message}`, {
12661
12749
  component: "gateway",
12662
12750
  event: "ticket_fetch_failed",
12663
12751
  attempt: totalReconnects
@@ -12702,6 +12790,7 @@ async function runWebSocketConnection(opts) {
12702
12790
  let connectionTimer;
12703
12791
  let pongTimer;
12704
12792
  let notifyDebounceTimer;
12793
+ let proactiveReconnectTimer;
12705
12794
  let isConnected = false;
12706
12795
  const cleanup = () => {
12707
12796
  if (pingInterval !== void 0) {
@@ -12720,6 +12809,10 @@ async function runWebSocketConnection(opts) {
12720
12809
  clearTimeout(notifyDebounceTimer);
12721
12810
  notifyDebounceTimer = void 0;
12722
12811
  }
12812
+ if (proactiveReconnectTimer !== void 0) {
12813
+ clearTimeout(proactiveReconnectTimer);
12814
+ proactiveReconnectTimer = void 0;
12815
+ }
12723
12816
  };
12724
12817
  connectionTimer = setTimeout(() => {
12725
12818
  if (!isConnected) {
@@ -12748,7 +12841,11 @@ async function runWebSocketConnection(opts) {
12748
12841
  pingInterval = setInterval(() => {
12749
12842
  if (ws.readyState === wrapper_default2.OPEN) {
12750
12843
  try {
12751
- 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));
12752
12849
  pongTimer = setTimeout(() => {
12753
12850
  log.warn("Pong timeout \u2014 closing WebSocket");
12754
12851
  try {
@@ -12762,6 +12859,13 @@ async function runWebSocketConnection(opts) {
12762
12859
  }
12763
12860
  }
12764
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);
12765
12869
  if (opts.getWorkerStatus) {
12766
12870
  try {
12767
12871
  const status2 = opts.getWorkerStatus();
@@ -12834,7 +12938,21 @@ async function runWebSocketConnection(opts) {
12834
12938
  clearTimeout(pongTimer);
12835
12939
  pongTimer = void 0;
12836
12940
  }
12837
- 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
+ }
12838
12956
  } else if (msg.action === "heartbeatRequest") {
12839
12957
  if (opts.getWorkerStatus && ws.readyState === wrapper_default2.OPEN) {
12840
12958
  try {
@@ -13042,7 +13160,7 @@ async function runPollingFallback(opts) {
13042
13160
  }
13043
13161
  return cursor;
13044
13162
  }
13045
- 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;
13046
13164
  var init_ws_gateway = __esm({
13047
13165
  "src/gateway/ws-gateway.ts"() {
13048
13166
  init_health_state();
@@ -13058,9 +13176,12 @@ var init_ws_gateway = __esm({
13058
13176
  HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
13059
13177
  MAX_POLLING_ITERATIONS = 10;
13060
13178
  MAX_CONSECUTIVE_POLL_FAILURES = 5;
13179
+ CONNECT_POLL_INTERVAL_MS = 3e4;
13180
+ PROACTIVE_RECONNECT_MS = 110 * 60 * 1e3;
13061
13181
  BACKOFF_INITIAL_MS = 2e3;
13062
13182
  BACKOFF_MAX_MS = 3e4;
13063
13183
  BACKOFF_FACTOR = 1.8;
13184
+ currentRotationToken = null;
13064
13185
  }
13065
13186
  });
13066
13187
  function jsonSchemaPropertyToZod(prop) {
@@ -13274,13 +13395,13 @@ function validateDownloadUrl(url) {
13274
13395
  if (parsed.protocol !== "https:") {
13275
13396
  throw new Error("Only HTTPS URLs are allowed");
13276
13397
  }
13277
- const hostname = parsed.hostname.toLowerCase();
13278
- const bareHostname = hostname.replace(/^\[|\]$/g, "");
13398
+ const hostname2 = parsed.hostname.toLowerCase();
13399
+ const bareHostname = hostname2.replace(/^\[|\]$/g, "");
13279
13400
  if (bareHostname === "localhost" || bareHostname === "127.0.0.1" || bareHostname === "::1") {
13280
13401
  throw new Error("Local URLs are not allowed");
13281
13402
  }
13282
13403
  for (const pattern of PRIVATE_IPV4_PATTERNS) {
13283
- if (pattern.test(hostname)) {
13404
+ if (pattern.test(hostname2)) {
13284
13405
  throw new Error("Private IP addresses are not allowed");
13285
13406
  }
13286
13407
  }
@@ -13850,8 +13971,9 @@ async function routeMessage(message, client, deps, sessionStore, config) {
13850
13971
  `Auto-reopening task ${message.taskId} (attachment on ${task.status} task)`
13851
13972
  );
13852
13973
  try {
13853
- await client.reopenTask({
13974
+ await client.updateTaskStatus({
13854
13975
  taskId: message.taskId,
13976
+ status: "open",
13855
13977
  reason: "Human sent an attachment \u2014 reopening for further work"
13856
13978
  });
13857
13979
  routingTarget = "orchestrator";
@@ -14659,6 +14781,59 @@ ${userMessage}
14659
14781
  }
14660
14782
  });
14661
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
+
14662
14837
  // src/service/entrypoint.ts
14663
14838
  var entrypoint_exports = {};
14664
14839
  __export(entrypoint_exports, {
@@ -14733,12 +14908,75 @@ function scheduleHeartbeat(createOrchServer, model, intervalMs, signal, log, cla
14733
14908
  setTimeout(() => void tick(), intervalMs);
14734
14909
  log.info(`Heartbeat scheduled every ${Math.round(intervalMs / 1e3)}s`);
14735
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
+ }
14736
14956
  async function startAgent(opts) {
14737
14957
  const localConfig = loadConfig();
14738
14958
  const log = opts?.log ?? createLogger("agent", localConfig.logLevel);
14739
14959
  const abortController = new AbortController();
14740
14960
  log.info("DeskFree Agent Runtime starting...");
14741
- 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
+ });
14742
14980
  const errorReporter = initErrorReporter(client, log);
14743
14981
  initializeHealth("unknown");
14744
14982
  log.info("Bootstrapping from API...", { apiUrl: localConfig.apiUrl });
@@ -14758,7 +14996,6 @@ async function startAgent(opts) {
14758
14996
  throw new Error(`Failed to bootstrap config from API: ${msg}`);
14759
14997
  }
14760
14998
  const isDocker2 = process.env["DOCKER"] === "1" || (await import('fs')).existsSync("/.dockerenv");
14761
- const runtimeVersion = process.env["npm_package_version"] ?? "unknown";
14762
14999
  const agentContext = {
14763
15000
  botName: config.botName,
14764
15001
  deploymentType: config.deploymentType,
@@ -14838,6 +15075,13 @@ async function startAgent(opts) {
14838
15075
  stateDir: config.stateDir,
14839
15076
  log,
14840
15077
  abortSignal: abortController.signal,
15078
+ botId: localConfig.botId,
15079
+ publicApiUrl,
15080
+ fingerprint,
15081
+ initialTicket: {
15082
+ ticket: connectResult.ticket,
15083
+ wsUrl: connectResult.wsUrl
15084
+ },
14841
15085
  getWorkerStatus: () => ({
14842
15086
  activeWorkers: workerManager.activeCount,
14843
15087
  queuedTasks: workerManager.queuedCount,
@@ -14926,13 +15170,44 @@ async function startAgent(opts) {
14926
15170
  `Sleep cycle: invoking sleep agent (${orientResult.entries.length} unconsolidated entries)...`
14927
15171
  );
14928
15172
  const workerServer = createWorkServer();
14929
- const result = runOneShotWorker({
15173
+ const agentStream = runOneShotWorker({
14930
15174
  prompt,
14931
15175
  systemPrompt: buildSleepDirective(agentContext),
14932
15176
  workerServer,
14933
15177
  model: config.model
14934
15178
  });
14935
- for await (const _ of result) {
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
+ }
15187
+ }
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
+ });
15200
+ log.info(
15201
+ `Sleep cycle: consolidation applied (${consolidateResult.opsApplied} ops, ${consolidateResult.entriesArchived} archived)`
15202
+ );
15203
+ } catch (parseErr) {
15204
+ const parseMsg = parseErr instanceof Error ? parseErr.message : String(parseErr);
15205
+ log.warn(`Sleep cycle: failed to submit consolidation: ${parseMsg}`);
15206
+ }
15207
+ } else {
15208
+ log.warn(
15209
+ "Sleep cycle: agent did not produce <consolidation_result> tags"
15210
+ );
14936
15211
  }
14937
15212
  return "success";
14938
15213
  } finally {
@@ -15007,6 +15282,7 @@ async function startAgent(opts) {
15007
15282
  log.info("Shutdown complete.");
15008
15283
  };
15009
15284
  }
15285
+ var CONNECT_POLL_INTERVAL_MS2;
15010
15286
  var init_entrypoint = __esm({
15011
15287
  "src/service/entrypoint.ts"() {
15012
15288
  init_orchestrator();
@@ -15023,6 +15299,7 @@ var init_entrypoint = __esm({
15023
15299
  init_sessions();
15024
15300
  init_worker_manager();
15025
15301
  init_dist();
15302
+ CONNECT_POLL_INTERVAL_MS2 = 3e4;
15026
15303
  }
15027
15304
  });
15028
15305
 
@@ -15031,29 +15308,42 @@ init_paths();
15031
15308
  var [name, cleanArgs] = parseName(process.argv.slice(2));
15032
15309
  var command = cleanArgs[0];
15033
15310
  if (command === "install") {
15034
- let token = cleanArgs[1];
15035
- 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) {
15036
15326
  const { createInterface } = await import('readline');
15037
15327
  const rl = createInterface({
15038
15328
  input: process.stdin,
15039
15329
  output: process.stdout
15040
15330
  });
15041
- token = await new Promise((resolve) => {
15331
+ botId = await new Promise((resolve) => {
15042
15332
  rl.question(
15043
- "Paste your bot token (from the DeskFree dashboard):\n> ",
15333
+ "Paste your bot ID (from the DeskFree dashboard):\n> ",
15044
15334
  (answer) => {
15045
15335
  rl.close();
15046
15336
  resolve(answer.trim());
15047
15337
  }
15048
15338
  );
15049
15339
  });
15050
- if (!token) {
15051
- console.error("No token provided.");
15340
+ if (!botId) {
15341
+ console.error("No bot ID provided.");
15052
15342
  process.exit(1);
15053
15343
  }
15054
15344
  }
15055
15345
  const { install: install2 } = await Promise.resolve().then(() => (init_install(), install_exports));
15056
- install2(token, name);
15346
+ install2(botId, name, stage);
15057
15347
  } else if (command === "uninstall") {
15058
15348
  const { uninstall: uninstall2 } = await Promise.resolve().then(() => (init_uninstall(), uninstall_exports));
15059
15349
  uninstall2(name);
@@ -15079,14 +15369,12 @@ if (command === "install") {
15079
15369
  process.exit(1);
15080
15370
  }, 5e3).unref();
15081
15371
  };
15082
- let token;
15083
- if (command === "start") {
15084
- token = cleanArgs[1];
15085
- } else if (command && !command.startsWith("-")) {
15086
- token = command;
15087
- }
15088
- if (token && !process.env["DESKFREE_LAUNCH"]) {
15089
- 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];
15090
15378
  }
15091
15379
  const { startAgent: startAgent2 } = await Promise.resolve().then(() => (init_entrypoint(), entrypoint_exports));
15092
15380
  const { createLogger: createLogger2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));